tick system, AVX2 UTF-8 decoder, uh faster in general
This commit is contained in:
+137
-30
@@ -1,36 +1,143 @@
|
||||
use zterm::terminal::Terminal;
|
||||
use zterm::vt_parser::Parser;
|
||||
use std::time::Instant;
|
||||
use std::io::Write;
|
||||
|
||||
fn main() {
|
||||
// Generate seq 1 100000 output
|
||||
let mut data = Vec::new();
|
||||
for i in 1..=100000 {
|
||||
writeln!(&mut data, "{}", i).unwrap();
|
||||
const ASCII_PRINTABLE: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ `~!@#$%^&*()_+-=[]{}\\|;:'\",<.>/?";
|
||||
const CONTROL_CHARS: &[u8] = b"\n\t";
|
||||
|
||||
// Match Kitty's default repetitions
|
||||
const REPETITIONS: usize = 100;
|
||||
|
||||
fn random_string(len: usize, rng: &mut u64) -> Vec<u8> {
|
||||
let alphabet_len = (ASCII_PRINTABLE.len() + CONTROL_CHARS.len()) as u64;
|
||||
let mut result = Vec::with_capacity(len);
|
||||
for _ in 0..len {
|
||||
// Simple LCG random
|
||||
*rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
let idx = ((*rng >> 33) % alphabet_len) as usize;
|
||||
if idx < ASCII_PRINTABLE.len() {
|
||||
result.push(ASCII_PRINTABLE[idx]);
|
||||
} else {
|
||||
result.push(CONTROL_CHARS[idx - ASCII_PRINTABLE.len()]);
|
||||
}
|
||||
}
|
||||
println!("Data size: {} bytes", data.len());
|
||||
|
||||
// Test with different terminal sizes to see scroll impact
|
||||
for rows in [24, 100, 1000] {
|
||||
let mut terminal = Terminal::new(80, rows, 10000);
|
||||
let start = Instant::now();
|
||||
terminal.process(&data);
|
||||
let elapsed = start.elapsed();
|
||||
println!("Terminal {}x{}: {:?} ({:.2} MB/s)",
|
||||
80, rows,
|
||||
elapsed,
|
||||
(data.len() as f64 / 1024.0 / 1024.0) / elapsed.as_secs_f64()
|
||||
);
|
||||
}
|
||||
|
||||
// Test with scrollback disabled
|
||||
println!("\nWith scrollback disabled:");
|
||||
let mut terminal = Terminal::new(80, 24, 0);
|
||||
let start = Instant::now();
|
||||
terminal.process(&data);
|
||||
let elapsed = start.elapsed();
|
||||
println!("Terminal 80x24, no scrollback: {:?} ({:.2} MB/s)",
|
||||
elapsed,
|
||||
(data.len() as f64 / 1024.0 / 1024.0) / elapsed.as_secs_f64()
|
||||
);
|
||||
result
|
||||
}
|
||||
|
||||
/// Run a benchmark with multiple repetitions like Kitty does
|
||||
fn run_benchmark<F>(name: &str, data: &[u8], repetitions: usize, mut setup: F)
|
||||
where
|
||||
F: FnMut() -> (Terminal, Parser),
|
||||
{
|
||||
let data_size = data.len();
|
||||
let total_size = data_size * repetitions;
|
||||
|
||||
// Warmup run
|
||||
let (mut terminal, mut parser) = setup();
|
||||
parser.parse(data, &mut terminal);
|
||||
|
||||
// Timed runs
|
||||
let start = Instant::now();
|
||||
for _ in 0..repetitions {
|
||||
let (mut terminal, mut parser) = setup();
|
||||
parser.parse(data, &mut terminal);
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
let mb = total_size as f64 / 1024.0 / 1024.0;
|
||||
let rate = mb / elapsed.as_secs_f64();
|
||||
|
||||
println!(" {:<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() {
|
||||
println!("=== ZTerm VT Parser Benchmark ===");
|
||||
println!("Matching Kitty's kitten __benchmark__ methodology\n");
|
||||
|
||||
// Benchmark 1: Only ASCII chars (matches Kitty's simple_ascii)
|
||||
println!("--- Only ASCII chars ---");
|
||||
let target_sz = 1024 * 2048 + 13;
|
||||
let mut rng: u64 = 12345;
|
||||
let mut ascii_data = Vec::with_capacity(target_sz);
|
||||
let alphabet = [ASCII_PRINTABLE, CONTROL_CHARS].concat();
|
||||
for _ in 0..target_sz {
|
||||
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
let idx = ((rng >> 33) % alphabet.len() as u64) as usize;
|
||||
ascii_data.push(alphabet[idx]);
|
||||
}
|
||||
|
||||
run_benchmark("Only ASCII chars", &ascii_data, REPETITIONS, || {
|
||||
(Terminal::new(80, 25, 20000), Parser::new())
|
||||
});
|
||||
|
||||
// Benchmark 2: CSI codes with few chars (matches Kitty's ascii_with_csi)
|
||||
println!("\n--- CSI codes with few chars ---");
|
||||
let target_sz = 1024 * 1024 + 17;
|
||||
let mut csi_data = Vec::with_capacity(target_sz + 100);
|
||||
let mut rng: u64 = 12345; // Fixed seed for reproducibility
|
||||
|
||||
while csi_data.len() < target_sz {
|
||||
// Simple LCG random for chunk selection
|
||||
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
let q = ((rng >> 33) % 100) as u32;
|
||||
|
||||
match q {
|
||||
0..=9 => {
|
||||
// 10%: random ASCII text (1-72 chars)
|
||||
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||
let len = ((rng >> 33) % 72 + 1) as usize;
|
||||
csi_data.extend(random_string(len, &mut rng));
|
||||
}
|
||||
10..=29 => {
|
||||
// 20%: cursor movement
|
||||
csi_data.extend_from_slice(b"\x1b[m\x1b[?1h\x1b[H");
|
||||
}
|
||||
30..=39 => {
|
||||
// 10%: basic SGR attributes
|
||||
csi_data.extend_from_slice(b"\x1b[1;2;3;4:3;31m");
|
||||
}
|
||||
40..=49 => {
|
||||
// 10%: SGR with 256-color + RGB (colon-separated subparams)
|
||||
csi_data.extend_from_slice(b"\x1b[38:5:24;48:2:125:136:147m");
|
||||
}
|
||||
50..=59 => {
|
||||
// 10%: SGR with underline color
|
||||
csi_data.extend_from_slice(b"\x1b[58;5;44;2m");
|
||||
}
|
||||
60..=79 => {
|
||||
// 20%: cursor movement + erase
|
||||
csi_data.extend_from_slice(b"\x1b[m\x1b[10A\x1b[3E\x1b[2K");
|
||||
}
|
||||
_ => {
|
||||
// 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[m");
|
||||
|
||||
run_benchmark("CSI codes with few chars", &csi_data, REPETITIONS, || {
|
||||
(Terminal::new(80, 25, 20000), Parser::new())
|
||||
});
|
||||
|
||||
// Benchmark 3: Long escape codes (matches Kitty's long_escape_codes)
|
||||
println!("\n--- Long escape codes ---");
|
||||
let mut long_esc_data = Vec::new();
|
||||
let long_content: String = (0..8024).map(|i| ASCII_PRINTABLE[i % ASCII_PRINTABLE.len()] as char).collect();
|
||||
for _ in 0..1024 {
|
||||
// OSC 6 - document reporting, ignored after parsing
|
||||
long_esc_data.extend_from_slice(b"\x1b]6;");
|
||||
long_esc_data.extend_from_slice(long_content.as_bytes());
|
||||
long_esc_data.push(0x07); // BEL terminator
|
||||
}
|
||||
|
||||
run_benchmark("Long escape codes", &long_esc_data, REPETITIONS, || {
|
||||
(Terminal::new(80, 25, 20000), Parser::new())
|
||||
});
|
||||
|
||||
println!("\n=== Benchmark Complete ===");
|
||||
println!("\nNote: These benchmarks include terminal state updates but NOT GPU rendering.");
|
||||
println!("Compare with: kitten __benchmark__ (without --render flag)");
|
||||
}
|
||||
|
||||
+1610
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
//! Linear color palette for GPU rendering.
|
||||
//!
|
||||
//! Provides pre-computed sRGB to linear RGB conversion for efficient GPU color handling.
|
||||
|
||||
use crate::terminal::ColorPalette;
|
||||
|
||||
/// Pre-computed linear RGB color palette.
|
||||
/// Avoids repeated sRGB→linear conversions during rendering.
|
||||
/// The color_table contains [258][4] floats: 256 indexed colors + default fg (256) + default bg (257).
|
||||
#[derive(Clone)]
|
||||
pub struct LinearPalette {
|
||||
/// Pre-computed linear RGBA colors ready for GPU upload.
|
||||
/// Index 0-255: palette colors, 256: default_fg, 257: default_bg
|
||||
pub color_table: [[f32; 4]; 258],
|
||||
}
|
||||
|
||||
impl LinearPalette {
|
||||
/// Convert sRGB component (0.0-1.0) to linear RGB.
|
||||
#[inline]
|
||||
pub fn srgb_to_linear(c: f32) -> f32 {
|
||||
if c <= 0.04045 {
|
||||
c / 12.92
|
||||
} else {
|
||||
((c + 0.055) / 1.055).powf(2.4)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an sRGB [u8; 3] color to linear [f32; 4] with alpha=1.0.
|
||||
#[inline]
|
||||
pub fn rgb_to_linear(rgb: [u8; 3]) -> [f32; 4] {
|
||||
[
|
||||
Self::srgb_to_linear(rgb[0] as f32 / 255.0),
|
||||
Self::srgb_to_linear(rgb[1] as f32 / 255.0),
|
||||
Self::srgb_to_linear(rgb[2] as f32 / 255.0),
|
||||
1.0,
|
||||
]
|
||||
}
|
||||
|
||||
/// Create a LinearPalette from a ColorPalette.
|
||||
pub fn from_palette(palette: &ColorPalette) -> Self {
|
||||
let mut color_table = [[0.0f32; 4]; 258];
|
||||
|
||||
for i in 0..256 {
|
||||
color_table[i] = Self::rgb_to_linear(palette.colors[i]);
|
||||
}
|
||||
color_table[256] = Self::rgb_to_linear(palette.default_fg);
|
||||
color_table[257] = Self::rgb_to_linear(palette.default_bg);
|
||||
|
||||
Self { color_table }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LinearPalette {
|
||||
fn default() -> Self {
|
||||
Self::from_palette(&ColorPalette::default())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
//! Color font (emoji) rendering using FreeType + Cairo.
|
||||
//!
|
||||
//! This module provides color emoji rendering support by using FreeType to load
|
||||
//! color fonts (COLR, CBDT, sbix formats) and Cairo to render them.
|
||||
|
||||
use cairo::{Format, ImageSurface};
|
||||
use freetype::Library as FtLibrary;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::CStr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// COLOR FONT LOOKUP
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Find a color font (emoji font) that contains the given character using fontconfig.
|
||||
/// Returns the path to the font file if found.
|
||||
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};
|
||||
|
||||
log::debug!("find_color_font_for_char: looking for color font for U+{:04X} '{}'", c as u32, c);
|
||||
|
||||
unsafe {
|
||||
// Create a pattern
|
||||
let pat = FcPatternCreate();
|
||||
if pat.is_null() {
|
||||
log::debug!("find_color_font_for_char: FcPatternCreate failed");
|
||||
return None;
|
||||
}
|
||||
|
||||
// Create a charset with the target character
|
||||
let charset = FcCharSetCreate();
|
||||
if charset.is_null() {
|
||||
FcPatternDestroy(pat);
|
||||
log::debug!("find_color_font_for_char: FcCharSetCreate failed");
|
||||
return None;
|
||||
}
|
||||
|
||||
// Add the character to the charset
|
||||
FcCharSetAddChar(charset, c as u32);
|
||||
|
||||
// Add the charset to the pattern
|
||||
FcPatternAddCharSet(pat, FC_CHARSET.as_ptr() as *const i8, charset);
|
||||
|
||||
// Request a color font
|
||||
FcPatternAddBool(pat, FC_COLOR.as_ptr() as *const i8, 1); // FcTrue = 1
|
||||
|
||||
// Run substitutions
|
||||
FcConfigSubstitute(std::ptr::null_mut(), pat, FcMatchPattern);
|
||||
FcDefaultSubstitute(pat);
|
||||
|
||||
// Find matching font
|
||||
let mut result = FcResultNoMatch;
|
||||
let matched = FcFontMatch(std::ptr::null_mut(), pat, &mut result);
|
||||
|
||||
let font_path = if !matched.is_null() && result == FcResultMatch {
|
||||
// Check if the matched font is actually a color font
|
||||
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;
|
||||
|
||||
log::debug!("find_color_font_for_char: matched font, is_color={}", has_color);
|
||||
|
||||
if has_color {
|
||||
// Get the file path from the matched pattern
|
||||
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 {
|
||||
let path_cstr = CStr::from_ptr(file_ptr as *const i8);
|
||||
let path = PathBuf::from(path_cstr.to_string_lossy().into_owned());
|
||||
log::debug!("find_color_font_for_char: found color font {:?}", path);
|
||||
Some(path)
|
||||
} else {
|
||||
log::debug!("find_color_font_for_char: couldn't get file path");
|
||||
None
|
||||
}
|
||||
} else {
|
||||
log::debug!("find_color_font_for_char: matched font is not a color font");
|
||||
None
|
||||
}
|
||||
} else {
|
||||
log::debug!("find_color_font_for_char: no match found (result={:?})", result);
|
||||
None
|
||||
};
|
||||
|
||||
// Cleanup
|
||||
if !matched.is_null() {
|
||||
FcPatternDestroy(matched);
|
||||
}
|
||||
FcCharSetDestroy(charset);
|
||||
FcPatternDestroy(pat);
|
||||
|
||||
font_path
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// COLOR FONT RENDERER
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Lazy-initialized color font renderer using FreeType + Cairo.
|
||||
/// Only created when a color emoji is first encountered.
|
||||
/// Cairo is required for proper color font rendering (COLR, CBDT, sbix formats).
|
||||
pub struct ColorFontRenderer {
|
||||
/// FreeType library instance
|
||||
ft_library: FtLibrary,
|
||||
/// Loaded FreeType faces and their Cairo font faces, keyed by font path
|
||||
faces: HashMap<PathBuf, (freetype::Face, cairo::FontFace)>,
|
||||
/// Reusable Cairo surface for rendering
|
||||
surface: Option<ImageSurface>,
|
||||
/// Current surface dimensions
|
||||
surface_size: (i32, i32),
|
||||
}
|
||||
|
||||
impl ColorFontRenderer {
|
||||
pub fn new() -> Result<Self, freetype::Error> {
|
||||
let ft_library = FtLibrary::init()?;
|
||||
Ok(Self {
|
||||
ft_library,
|
||||
faces: HashMap::new(),
|
||||
surface: None,
|
||||
surface_size: (0, 0),
|
||||
})
|
||||
}
|
||||
|
||||
/// Ensure faces are loaded and return font size to set
|
||||
fn ensure_faces_loaded(&mut self, path: &PathBuf) -> bool {
|
||||
if !self.faces.contains_key(path) {
|
||||
match self.ft_library.new_face(path, 0) {
|
||||
Ok(ft_face) => {
|
||||
// Create Cairo font face from FreeType face
|
||||
match cairo::FontFace::create_from_ft(&ft_face) {
|
||||
Ok(cairo_face) => {
|
||||
self.faces.insert(path.clone(), (ft_face, cairo_face));
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to create Cairo font face for {:?}: {:?}", path, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to load color font {:?}: {:?}", path, e);
|
||||
false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a color glyph using FreeType + Cairo.
|
||||
/// Returns (width, height, RGBA bitmap, offset_x, offset_y) or None if rendering fails.
|
||||
pub fn render_color_glyph(
|
||||
&mut self,
|
||||
font_path: &PathBuf,
|
||||
c: char,
|
||||
font_size_px: f32,
|
||||
cell_width: u32,
|
||||
cell_height: u32,
|
||||
) -> Option<(u32, u32, Vec<u8>, f32, f32)> {
|
||||
log::debug!("render_color_glyph: U+{:04X} '{}' font={:?}", c as u32, c, font_path);
|
||||
|
||||
// Ensure faces are loaded
|
||||
if !self.ensure_faces_loaded(font_path) {
|
||||
log::debug!("render_color_glyph: failed to load faces");
|
||||
return None;
|
||||
}
|
||||
log::debug!("render_color_glyph: faces loaded successfully, faces count={}", self.faces.len());
|
||||
|
||||
// Get glyph index from FreeType face
|
||||
// Note: We do NOT call set_pixel_sizes here because CBDT (bitmap) fonts have fixed sizes
|
||||
// and will fail. Cairo handles font sizing internally.
|
||||
let glyph_index = {
|
||||
let face_entry = self.faces.get(font_path);
|
||||
if face_entry.is_none() {
|
||||
log::debug!("render_color_glyph: face not found in hashmap after ensure_faces_loaded!");
|
||||
return None;
|
||||
}
|
||||
let (ft_face, _) = face_entry?;
|
||||
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);
|
||||
log::debug!("render_color_glyph: FreeType glyph index for U+{:04X} = {:?}", c as u32, idx);
|
||||
if idx.is_none() {
|
||||
log::debug!("render_color_glyph: glyph index is None - char not in font!");
|
||||
return None;
|
||||
}
|
||||
idx?
|
||||
};
|
||||
|
||||
// Clone the Cairo font face (it's reference-counted)
|
||||
let cairo_face = {
|
||||
let (_, cairo_face) = self.faces.get(font_path)?;
|
||||
cairo_face.clone()
|
||||
};
|
||||
|
||||
// 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_height = cell_height as i32;
|
||||
|
||||
log::debug!("render_color_glyph: render size {}x{}", render_width, render_height);
|
||||
|
||||
// Ensure we have a large enough surface
|
||||
let surface_width = render_width.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 {
|
||||
let new_width = surface_width.max(self.surface_size.0);
|
||||
let new_height = surface_height.max(self.surface_size.1);
|
||||
match ImageSurface::create(Format::ARgb32, new_width, new_height) {
|
||||
Ok(surface) => {
|
||||
log::debug!("render_color_glyph: created Cairo surface {}x{}", new_width, new_height);
|
||||
self.surface = Some(surface);
|
||||
self.surface_size = (new_width, new_height);
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to create Cairo surface: {:?}", e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let surface = self.surface.as_mut()?;
|
||||
|
||||
// Create Cairo context
|
||||
let cr = match cairo::Context::new(surface) {
|
||||
Ok(cr) => cr,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to create Cairo context: {:?}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// Clear the surface
|
||||
cr.set_operator(cairo::Operator::Clear);
|
||||
cr.paint().ok()?;
|
||||
cr.set_operator(cairo::Operator::Over);
|
||||
|
||||
// Set the font face and initial size
|
||||
cr.set_font_face(&cairo_face);
|
||||
|
||||
// Target dimensions for the glyph (2 cells wide, 1 cell tall for emoji)
|
||||
let target_width = render_width as f64;
|
||||
let target_height = render_height as f64;
|
||||
|
||||
// Start with the requested font size and reduce until glyph fits
|
||||
// This matches Kitty's fit_cairo_glyph() approach
|
||||
let mut current_size = font_size_px as f64;
|
||||
let min_size = 2.0;
|
||||
|
||||
cr.set_font_size(current_size);
|
||||
let mut glyph = cairo::Glyph::new(glyph_index as u64, 0.0, 0.0);
|
||||
let mut text_extents = cr.glyph_extents(&[glyph]).ok()?;
|
||||
|
||||
while current_size > min_size && (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);
|
||||
if new_size >= current_size {
|
||||
current_size -= 2.0;
|
||||
} else {
|
||||
current_size = new_size;
|
||||
}
|
||||
cr.set_font_size(current_size);
|
||||
text_extents = cr.glyph_extents(&[glyph]).ok()?;
|
||||
}
|
||||
|
||||
log::debug!("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
|
||||
let font_extents = cr.font_extents().ok()?;
|
||||
log::debug!("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
|
||||
// y position should be at baseline (ascent from top)
|
||||
glyph = cairo::Glyph::new(glyph_index as u64, 0.0, font_extents.ascent());
|
||||
|
||||
// Get final glyph extents for sizing
|
||||
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}",
|
||||
text_extents.width(), 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
|
||||
cr.set_source_rgba(1.0, 1.0, 1.0, 1.0);
|
||||
|
||||
// Render the glyph
|
||||
if let Err(e) = cr.show_glyphs(&[glyph]) {
|
||||
log::warn!("render_color_glyph: show_glyphs failed: {:?}", e);
|
||||
return None;
|
||||
}
|
||||
log::debug!("render_color_glyph: cairo show_glyphs succeeded");
|
||||
|
||||
// Flush and get surface reference again
|
||||
drop(cr); // Drop the context before accessing surface data
|
||||
let surface = self.surface.as_mut()?;
|
||||
surface.flush();
|
||||
|
||||
// Calculate actual glyph bounds
|
||||
let glyph_width = text_extents.width().ceil() as u32;
|
||||
let glyph_height = text_extents.height().ceil() as u32;
|
||||
|
||||
log::debug!("render_color_glyph: glyph size {}x{}", glyph_width, glyph_height);
|
||||
|
||||
if glyph_width == 0 || glyph_height == 0 {
|
||||
log::debug!("render_color_glyph: zero size glyph, returning None");
|
||||
return None;
|
||||
}
|
||||
|
||||
// The actual rendered area - use the text extents to determine position
|
||||
let x_offset = text_extents.x_bearing();
|
||||
let y_offset = text_extents.y_bearing();
|
||||
|
||||
// Calculate source rectangle in the surface
|
||||
let src_x = x_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);
|
||||
|
||||
// Get surface data
|
||||
let stride = surface.stride() as usize;
|
||||
let surface_data = surface.data().ok()?;
|
||||
|
||||
// Extract the glyph region and convert ARGB -> RGBA
|
||||
let out_width = glyph_width.min(render_width 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 non_zero_pixels = 0u32;
|
||||
let mut has_color = false;
|
||||
|
||||
for y in 0..out_height as i32 {
|
||||
for x in 0..out_width as i32 {
|
||||
let src_pixel_x = src_x + x;
|
||||
let src_pixel_y = src_y + y;
|
||||
|
||||
if src_pixel_x >= 0 && src_pixel_x < self.surface_size.0
|
||||
&& src_pixel_y >= 0 && 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() {
|
||||
// Cairo uses ARGB in native byte order (on little-endian: BGRA in memory)
|
||||
// We need to convert to RGBA
|
||||
let b = surface_data[src_idx];
|
||||
let g = surface_data[src_idx + 1];
|
||||
let r = surface_data[src_idx + 2];
|
||||
let a = surface_data[src_idx + 3];
|
||||
|
||||
if a > 0 {
|
||||
non_zero_pixels += 1;
|
||||
// Check if this is actual color (not just white/gray)
|
||||
if r != g || g != b {
|
||||
has_color = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Un-premultiply alpha if needed (Cairo uses premultiplied alpha)
|
||||
if a > 0 && a < 255 {
|
||||
let inv_alpha = 255.0 / a as f32;
|
||||
rgba[dst_idx] = (r 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;
|
||||
} else {
|
||||
rgba[dst_idx] = r;
|
||||
rgba[dst_idx + 1] = g;
|
||||
rgba[dst_idx + 2] = b;
|
||||
rgba[dst_idx + 3] = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("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
|
||||
let has_content = rgba.chunks(4).any(|p| p[3] > 0);
|
||||
if !has_content {
|
||||
log::debug!("render_color_glyph: no visible content, returning None");
|
||||
return None;
|
||||
}
|
||||
|
||||
// Kitty convention: bitmap_top = -y_bearing (distance from baseline to glyph top)
|
||||
let offset_x = text_extents.x_bearing() as f32;
|
||||
let offset_y = -text_extents.y_bearing() as f32;
|
||||
|
||||
log::debug!("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))
|
||||
}
|
||||
}
|
||||
+99
-128
@@ -52,11 +52,15 @@ impl Keybind {
|
||||
let key_part = &lowercase[last_plus + 1..];
|
||||
let mod_part = &lowercase[..last_plus];
|
||||
// Normalize symbol names to actual characters
|
||||
let key = Self::normalize_key_name(key_part);
|
||||
let key = Self::normalize_key_name(key_part)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| key_part.to_string());
|
||||
(mod_part, key)
|
||||
} else {
|
||||
// No modifiers, just a key
|
||||
let key = Self::normalize_key_name(&lowercase);
|
||||
let key = Self::normalize_key_name(&lowercase)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| lowercase.clone());
|
||||
("", key)
|
||||
};
|
||||
|
||||
@@ -86,77 +90,78 @@ impl Keybind {
|
||||
|
||||
/// Normalizes key names to their canonical form.
|
||||
/// Supports both symbol names ("plus", "minus") and literal symbols ("+", "-").
|
||||
fn normalize_key_name(name: &str) -> String {
|
||||
match name {
|
||||
/// Returns a static str for known keys, None for unknown (caller uses input).
|
||||
fn normalize_key_name(name: &str) -> Option<&'static str> {
|
||||
Some(match name {
|
||||
// Arrow keys
|
||||
"left" | "arrowleft" | "arrow_left" => "left".to_string(),
|
||||
"right" | "arrowright" | "arrow_right" => "right".to_string(),
|
||||
"up" | "arrowup" | "arrow_up" => "up".to_string(),
|
||||
"down" | "arrowdown" | "arrow_down" => "down".to_string(),
|
||||
"left" | "arrowleft" | "arrow_left" => "left",
|
||||
"right" | "arrowright" | "arrow_right" => "right",
|
||||
"up" | "arrowup" | "arrow_up" => "up",
|
||||
"down" | "arrowdown" | "arrow_down" => "down",
|
||||
|
||||
// Other special keys
|
||||
"enter" | "return" => "enter".to_string(),
|
||||
"tab" => "tab".to_string(),
|
||||
"escape" | "esc" => "escape".to_string(),
|
||||
"backspace" | "back" => "backspace".to_string(),
|
||||
"delete" | "del" => "delete".to_string(),
|
||||
"insert" | "ins" => "insert".to_string(),
|
||||
"home" => "home".to_string(),
|
||||
"end" => "end".to_string(),
|
||||
"pageup" | "page_up" | "pgup" => "pageup".to_string(),
|
||||
"pagedown" | "page_down" | "pgdn" => "pagedown".to_string(),
|
||||
"enter" | "return" => "enter",
|
||||
"tab" => "tab",
|
||||
"escape" | "esc" => "escape",
|
||||
"backspace" | "back" => "backspace",
|
||||
"delete" | "del" => "delete",
|
||||
"insert" | "ins" => "insert",
|
||||
"home" => "home",
|
||||
"end" => "end",
|
||||
"pageup" | "page_up" | "pgup" => "pageup",
|
||||
"pagedown" | "page_down" | "pgdn" => "pagedown",
|
||||
|
||||
// Function keys
|
||||
"f1" => "f1".to_string(),
|
||||
"f2" => "f2".to_string(),
|
||||
"f3" => "f3".to_string(),
|
||||
"f4" => "f4".to_string(),
|
||||
"f5" => "f5".to_string(),
|
||||
"f6" => "f6".to_string(),
|
||||
"f7" => "f7".to_string(),
|
||||
"f8" => "f8".to_string(),
|
||||
"f9" => "f9".to_string(),
|
||||
"f10" => "f10".to_string(),
|
||||
"f11" => "f11".to_string(),
|
||||
"f12" => "f12".to_string(),
|
||||
"f1" => "f1",
|
||||
"f2" => "f2",
|
||||
"f3" => "f3",
|
||||
"f4" => "f4",
|
||||
"f5" => "f5",
|
||||
"f6" => "f6",
|
||||
"f7" => "f7",
|
||||
"f8" => "f8",
|
||||
"f9" => "f9",
|
||||
"f10" => "f10",
|
||||
"f11" => "f11",
|
||||
"f12" => "f12",
|
||||
|
||||
// Symbol name aliases
|
||||
"plus" => "+".to_string(),
|
||||
"minus" => "-".to_string(),
|
||||
"equal" | "equals" => "=".to_string(),
|
||||
"bracket_left" | "bracketleft" | "lbracket" => "[".to_string(),
|
||||
"bracket_right" | "bracketright" | "rbracket" => "]".to_string(),
|
||||
"brace_left" | "braceleft" | "lbrace" => "{".to_string(),
|
||||
"brace_right" | "braceright" | "rbrace" => "}".to_string(),
|
||||
"semicolon" => ";".to_string(),
|
||||
"colon" => ":".to_string(),
|
||||
"apostrophe" | "quote" => "'".to_string(),
|
||||
"quotedbl" | "doublequote" => "\"".to_string(),
|
||||
"comma" => ",".to_string(),
|
||||
"period" | "dot" => ".".to_string(),
|
||||
"slash" => "/".to_string(),
|
||||
"backslash" => "\\".to_string(),
|
||||
"grave" | "backtick" => "`".to_string(),
|
||||
"tilde" => "~".to_string(),
|
||||
"at" => "@".to_string(),
|
||||
"hash" | "pound" => "#".to_string(),
|
||||
"dollar" => "$".to_string(),
|
||||
"percent" => "%".to_string(),
|
||||
"caret" => "^".to_string(),
|
||||
"ampersand" => "&".to_string(),
|
||||
"asterisk" | "star" => "*".to_string(),
|
||||
"paren_left" | "parenleft" | "lparen" => "(".to_string(),
|
||||
"paren_right" | "parenright" | "rparen" => ")".to_string(),
|
||||
"underscore" => "_".to_string(),
|
||||
"pipe" | "bar" => "|".to_string(),
|
||||
"question" => "?".to_string(),
|
||||
"exclam" | "exclamation" | "bang" => "!".to_string(),
|
||||
"less" | "lessthan" => "<".to_string(),
|
||||
"greater" | "greaterthan" => ">".to_string(),
|
||||
"space" => " ".to_string(),
|
||||
// Pass through everything else as-is
|
||||
_ => name.to_string(),
|
||||
}
|
||||
"plus" => "+",
|
||||
"minus" => "-",
|
||||
"equal" | "equals" => "=",
|
||||
"bracket_left" | "bracketleft" | "lbracket" => "[",
|
||||
"bracket_right" | "bracketright" | "rbracket" => "]",
|
||||
"brace_left" | "braceleft" | "lbrace" => "{",
|
||||
"brace_right" | "braceright" | "rbrace" => "}",
|
||||
"semicolon" => ";",
|
||||
"colon" => ":",
|
||||
"apostrophe" | "quote" => "'",
|
||||
"quotedbl" | "doublequote" => "\"",
|
||||
"comma" => ",",
|
||||
"period" | "dot" => ".",
|
||||
"slash" => "/",
|
||||
"backslash" => "\\",
|
||||
"grave" | "backtick" => "`",
|
||||
"tilde" => "~",
|
||||
"at" => "@",
|
||||
"hash" | "pound" => "#",
|
||||
"dollar" => "$",
|
||||
"percent" => "%",
|
||||
"caret" => "^",
|
||||
"ampersand" => "&",
|
||||
"asterisk" | "star" => "*",
|
||||
"paren_left" | "parenleft" | "lparen" => "(",
|
||||
"paren_right" | "parenright" | "rparen" => ")",
|
||||
"underscore" => "_",
|
||||
"pipe" | "bar" => "|",
|
||||
"question" => "?",
|
||||
"exclam" | "exclamation" | "bang" => "!",
|
||||
"less" | "lessthan" => "<",
|
||||
"greater" | "greaterthan" => ">",
|
||||
"space" => " ",
|
||||
// Unknown - caller handles passthrough
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,68 +286,34 @@ impl Keybindings {
|
||||
pub fn build_action_map(&self) -> HashMap<(bool, bool, bool, bool, String), Action> {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
if let Some(parsed) = self.new_tab.parse() {
|
||||
map.insert(parsed, Action::NewTab);
|
||||
}
|
||||
if let Some(parsed) = self.next_tab.parse() {
|
||||
map.insert(parsed, Action::NextTab);
|
||||
}
|
||||
if let Some(parsed) = self.prev_tab.parse() {
|
||||
map.insert(parsed, Action::PrevTab);
|
||||
}
|
||||
if let Some(parsed) = self.tab_1.parse() {
|
||||
map.insert(parsed, Action::Tab1);
|
||||
}
|
||||
if let Some(parsed) = self.tab_2.parse() {
|
||||
map.insert(parsed, Action::Tab2);
|
||||
}
|
||||
if let Some(parsed) = self.tab_3.parse() {
|
||||
map.insert(parsed, Action::Tab3);
|
||||
}
|
||||
if let Some(parsed) = self.tab_4.parse() {
|
||||
map.insert(parsed, Action::Tab4);
|
||||
}
|
||||
if let Some(parsed) = self.tab_5.parse() {
|
||||
map.insert(parsed, Action::Tab5);
|
||||
}
|
||||
if let Some(parsed) = self.tab_6.parse() {
|
||||
map.insert(parsed, Action::Tab6);
|
||||
}
|
||||
if let Some(parsed) = self.tab_7.parse() {
|
||||
map.insert(parsed, Action::Tab7);
|
||||
}
|
||||
if let Some(parsed) = self.tab_8.parse() {
|
||||
map.insert(parsed, Action::Tab8);
|
||||
}
|
||||
if let Some(parsed) = self.tab_9.parse() {
|
||||
map.insert(parsed, Action::Tab9);
|
||||
}
|
||||
if let Some(parsed) = self.split_horizontal.parse() {
|
||||
map.insert(parsed, Action::SplitHorizontal);
|
||||
}
|
||||
if let Some(parsed) = self.split_vertical.parse() {
|
||||
map.insert(parsed, Action::SplitVertical);
|
||||
}
|
||||
if let Some(parsed) = self.close_pane.parse() {
|
||||
map.insert(parsed, Action::ClosePane);
|
||||
}
|
||||
if let Some(parsed) = self.focus_pane_up.parse() {
|
||||
map.insert(parsed, Action::FocusPaneUp);
|
||||
}
|
||||
if let Some(parsed) = self.focus_pane_down.parse() {
|
||||
map.insert(parsed, Action::FocusPaneDown);
|
||||
}
|
||||
if let Some(parsed) = self.focus_pane_left.parse() {
|
||||
map.insert(parsed, Action::FocusPaneLeft);
|
||||
}
|
||||
if let Some(parsed) = self.focus_pane_right.parse() {
|
||||
map.insert(parsed, Action::FocusPaneRight);
|
||||
}
|
||||
if let Some(parsed) = self.copy.parse() {
|
||||
map.insert(parsed, Action::Copy);
|
||||
}
|
||||
if let Some(parsed) = self.paste.parse() {
|
||||
map.insert(parsed, Action::Paste);
|
||||
let bindings: &[(&Keybind, Action)] = &[
|
||||
(&self.new_tab, Action::NewTab),
|
||||
(&self.next_tab, Action::NextTab),
|
||||
(&self.prev_tab, Action::PrevTab),
|
||||
(&self.tab_1, Action::Tab1),
|
||||
(&self.tab_2, Action::Tab2),
|
||||
(&self.tab_3, Action::Tab3),
|
||||
(&self.tab_4, Action::Tab4),
|
||||
(&self.tab_5, Action::Tab5),
|
||||
(&self.tab_6, Action::Tab6),
|
||||
(&self.tab_7, Action::Tab7),
|
||||
(&self.tab_8, Action::Tab8),
|
||||
(&self.tab_9, Action::Tab9),
|
||||
(&self.split_horizontal, Action::SplitHorizontal),
|
||||
(&self.split_vertical, Action::SplitVertical),
|
||||
(&self.close_pane, Action::ClosePane),
|
||||
(&self.focus_pane_up, Action::FocusPaneUp),
|
||||
(&self.focus_pane_down, Action::FocusPaneDown),
|
||||
(&self.focus_pane_left, Action::FocusPaneLeft),
|
||||
(&self.focus_pane_right, Action::FocusPaneRight),
|
||||
(&self.copy, Action::Copy),
|
||||
(&self.paste, Action::Paste),
|
||||
];
|
||||
|
||||
for (keybind, action) in bindings {
|
||||
if let Some(parsed) = keybind.parse() {
|
||||
map.insert(parsed, *action);
|
||||
}
|
||||
}
|
||||
|
||||
map
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//! Edge glow animation for visual feedback.
|
||||
//!
|
||||
//! Creates an organic glow effect when navigation fails: a single light node appears at center,
|
||||
//! then splits into two that travel outward to the corners while fading.
|
||||
|
||||
use crate::terminal::Direction;
|
||||
|
||||
/// Edge glow animation state for visual feedback when navigation fails.
|
||||
/// Creates an organic glow effect: a single light node appears at center,
|
||||
/// then splits into two that travel outward to the corners while fading.
|
||||
/// Animation logic is handled in the shader (shader.wgsl).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct EdgeGlow {
|
||||
/// Which edge to glow (based on the direction the user tried to navigate).
|
||||
pub direction: Direction,
|
||||
/// When the animation started.
|
||||
pub start_time: std::time::Instant,
|
||||
/// Pane bounds - left edge in pixels.
|
||||
pub pane_x: f32,
|
||||
/// Pane bounds - top edge in pixels.
|
||||
pub pane_y: f32,
|
||||
/// Pane bounds - width in pixels.
|
||||
pub pane_width: f32,
|
||||
/// Pane bounds - height in pixels.
|
||||
pub pane_height: f32,
|
||||
}
|
||||
|
||||
impl EdgeGlow {
|
||||
/// Duration of the glow animation in milliseconds.
|
||||
pub const DURATION_MS: u64 = 500;
|
||||
|
||||
/// 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 {
|
||||
Self {
|
||||
direction,
|
||||
start_time: std::time::Instant::now(),
|
||||
pane_x,
|
||||
pane_y,
|
||||
pane_width,
|
||||
pane_height,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current animation progress (0.0 to 1.0).
|
||||
pub fn progress(&self) -> f32 {
|
||||
let elapsed = self.start_time.elapsed().as_millis() as f32;
|
||||
let duration = Self::DURATION_MS as f32;
|
||||
(elapsed / duration).min(1.0)
|
||||
}
|
||||
|
||||
/// Check if the animation has completed.
|
||||
pub fn is_finished(&self) -> bool {
|
||||
self.progress() >= 1.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
//! Font loading and discovery using fontconfig.
|
||||
//!
|
||||
//! This module provides font loading utilities including:
|
||||
//! - Finding fonts by family name with style variants (regular, bold, italic, bold-italic)
|
||||
//! - Finding fonts that contain specific characters (for fallback)
|
||||
//! - Loading font data for use with ab_glyph and rustybuzz
|
||||
|
||||
use ab_glyph::FontRef;
|
||||
use fontconfig::Fontconfig;
|
||||
use std::ffi::CStr;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// FONT VARIANT
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// A font variant with its data and parsed references.
|
||||
pub struct FontVariant {
|
||||
/// Owned font data (kept alive for the lifetime of the font references).
|
||||
#[allow(dead_code)]
|
||||
data: Box<[u8]>,
|
||||
/// ab_glyph font reference for rasterization.
|
||||
font: FontRef<'static>,
|
||||
/// rustybuzz face for text shaping.
|
||||
face: rustybuzz::Face<'static>,
|
||||
}
|
||||
|
||||
impl FontVariant {
|
||||
/// Get a reference to the ab_glyph font.
|
||||
pub fn font(&self) -> &FontRef<'static> {
|
||||
&self.font
|
||||
}
|
||||
|
||||
/// Get a reference to the rustybuzz face.
|
||||
pub fn face(&self) -> &rustybuzz::Face<'static> {
|
||||
&self.face
|
||||
}
|
||||
|
||||
/// Clone the font reference (ab_glyph FontRef is Clone).
|
||||
pub fn clone_font(&self) -> FontRef<'static> {
|
||||
self.font.clone()
|
||||
}
|
||||
|
||||
/// Clone the font data.
|
||||
pub fn clone_data(&self) -> Box<[u8]> {
|
||||
self.data.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// FONT DISCOVERY
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Find a font that contains the given character using fontconfig.
|
||||
/// Returns the path to the font file if found.
|
||||
///
|
||||
/// Note: For emoji, use `find_color_font_for_char` from the color_font module instead,
|
||||
/// which explicitly requests color fonts.
|
||||
pub fn find_font_for_char(_fc: &Fontconfig, c: char) -> Option<PathBuf> {
|
||||
use fontconfig_sys as fcsys;
|
||||
use fcsys::*;
|
||||
|
||||
unsafe {
|
||||
// Create a pattern
|
||||
let pat = FcPatternCreate();
|
||||
if pat.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Create a charset with the target character
|
||||
let charset = FcCharSetCreate();
|
||||
if charset.is_null() {
|
||||
FcPatternDestroy(pat);
|
||||
return None;
|
||||
}
|
||||
|
||||
// Add the character to the charset
|
||||
FcCharSetAddChar(charset, c as u32);
|
||||
|
||||
// Add the charset to the pattern
|
||||
let fc_charset_cstr = CStr::from_bytes_with_nul(b"charset\0").unwrap();
|
||||
FcPatternAddCharSet(pat, fc_charset_cstr.as_ptr(), charset);
|
||||
|
||||
// Run substitutions
|
||||
FcConfigSubstitute(std::ptr::null_mut(), pat, FcMatchPattern);
|
||||
FcDefaultSubstitute(pat);
|
||||
|
||||
// Find matching font
|
||||
let mut result = FcResultNoMatch;
|
||||
let matched = FcFontMatch(std::ptr::null_mut(), pat, &mut result);
|
||||
|
||||
let font_result = if !matched.is_null() && result == FcResultMatch {
|
||||
// Get the file path from the matched pattern
|
||||
let mut file_ptr: *mut FcChar8 = std::ptr::null_mut();
|
||||
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
|
||||
{
|
||||
let path_cstr = CStr::from_ptr(file_ptr as *const i8);
|
||||
Some(PathBuf::from(path_cstr.to_string_lossy().into_owned()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Cleanup
|
||||
if !matched.is_null() {
|
||||
FcPatternDestroy(matched);
|
||||
}
|
||||
FcCharSetDestroy(charset);
|
||||
FcPatternDestroy(pat);
|
||||
|
||||
font_result
|
||||
}
|
||||
}
|
||||
|
||||
/// Find font files for a font family using fontconfig.
|
||||
/// Returns paths for (regular, bold, italic, bold_italic).
|
||||
/// Any variant that can't be found will be None.
|
||||
pub fn find_font_family_variants(family: &str) -> [Option<PathBuf>; 4] {
|
||||
use fontconfig_sys as fcsys;
|
||||
use fcsys::*;
|
||||
use fcsys::constants::{FC_FAMILY, FC_WEIGHT, FC_SLANT, FC_FILE};
|
||||
use std::ffi::CString;
|
||||
|
||||
let mut results: [Option<PathBuf>; 4] = [None, None, None, None];
|
||||
|
||||
// Style queries: (weight, slant) pairs for each variant
|
||||
// FC_WEIGHT_REGULAR = 80, FC_WEIGHT_BOLD = 200
|
||||
// FC_SLANT_ROMAN = 0, FC_SLANT_ITALIC = 100
|
||||
let styles: [(i32, i32); 4] = [
|
||||
(80, 0), // Regular
|
||||
(200, 0), // Bold
|
||||
(80, 100), // Italic
|
||||
(200, 100), // BoldItalic
|
||||
];
|
||||
|
||||
unsafe {
|
||||
let family_cstr = match CString::new(family) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return results,
|
||||
};
|
||||
|
||||
for (idx, (weight, slant)) in styles.iter().enumerate() {
|
||||
let pat = FcPatternCreate();
|
||||
if pat.is_null() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set family name
|
||||
FcPatternAddString(pat, FC_FAMILY.as_ptr() as *const i8, family_cstr.as_ptr() as *const u8);
|
||||
// Set weight
|
||||
FcPatternAddInteger(pat, FC_WEIGHT.as_ptr() as *const i8, *weight);
|
||||
// Set slant
|
||||
FcPatternAddInteger(pat, FC_SLANT.as_ptr() as *const i8, *slant);
|
||||
|
||||
FcConfigSubstitute(std::ptr::null_mut(), pat, FcMatchPattern);
|
||||
FcDefaultSubstitute(pat);
|
||||
|
||||
let mut result: FcResult = FcResultMatch;
|
||||
let matched = FcFontMatch(std::ptr::null_mut(), pat, &mut result);
|
||||
|
||||
if result == FcResultMatch && !matched.is_null() {
|
||||
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 !file_ptr.is_null() {
|
||||
let path_cstr = std::ffi::CStr::from_ptr(file_ptr as *const i8);
|
||||
if let Ok(path_str) = path_cstr.to_str() {
|
||||
results[idx] = Some(PathBuf::from(path_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
FcPatternDestroy(matched);
|
||||
}
|
||||
|
||||
FcPatternDestroy(pat);
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// FONT LOADING
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Try to load a font file and create both ab_glyph and rustybuzz handles.
|
||||
/// Returns None if the file doesn't exist or can't be parsed.
|
||||
pub fn load_font_variant(path: &std::path::Path) -> Option<FontVariant> {
|
||||
let data = std::fs::read(path).ok()?.into_boxed_slice();
|
||||
|
||||
// Parse with ab_glyph
|
||||
let font: FontRef<'static> = {
|
||||
let font = FontRef::try_from_slice(&data).ok()?;
|
||||
// SAFETY: We keep data alive in the FontVariant struct
|
||||
unsafe { std::mem::transmute(font) }
|
||||
};
|
||||
|
||||
// Parse with rustybuzz
|
||||
let face: rustybuzz::Face<'static> = {
|
||||
let face = rustybuzz::Face::from_slice(&data, 0)?;
|
||||
// SAFETY: We keep data alive in the FontVariant struct
|
||||
unsafe { std::mem::transmute(face) }
|
||||
};
|
||||
|
||||
Some(FontVariant { data, font, face })
|
||||
}
|
||||
|
||||
/// Load font variants for a font family.
|
||||
/// Returns array of font variants, with index 0 being the regular font.
|
||||
/// Falls back to hardcoded paths if fontconfig fails.
|
||||
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
|
||||
if let Some(family) = font_family {
|
||||
let paths = find_font_family_variants(family);
|
||||
log::info!("Font family '{}' resolved to:", family);
|
||||
for (i, path) in paths.iter().enumerate() {
|
||||
let style = match i {
|
||||
0 => "Regular",
|
||||
1 => "Bold",
|
||||
2 => "Italic",
|
||||
3 => "BoldItalic",
|
||||
_ => "Unknown",
|
||||
};
|
||||
if let Some(p) = path {
|
||||
log::info!(" {}: {:?}", style, p);
|
||||
}
|
||||
}
|
||||
|
||||
// Load the regular font (required)
|
||||
if let Some(regular_path) = &paths[0] {
|
||||
if let Some(regular) = load_font_variant(regular_path) {
|
||||
let primary_font = regular.clone_font();
|
||||
let font_data = regular.clone_data();
|
||||
|
||||
// Load other variants
|
||||
let variants: [Option<FontVariant>; 4] = [
|
||||
Some(regular),
|
||||
paths[1].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)),
|
||||
];
|
||||
|
||||
return (font_data, primary_font, variants);
|
||||
}
|
||||
}
|
||||
log::warn!("Failed to load font family '{}', falling back to defaults", family);
|
||||
}
|
||||
|
||||
// Fallback: try hardcoded paths
|
||||
let fallback_fonts = [
|
||||
("/usr/share/fonts/TTF/0xProtoNerdFont-Regular.ttf",
|
||||
"/usr/share/fonts/TTF/0xProtoNerdFont-Bold.ttf",
|
||||
"/usr/share/fonts/TTF/0xProtoNerdFont-Italic.ttf",
|
||||
"/usr/share/fonts/TTF/0xProtoNerdFont-BoldItalic.ttf"),
|
||||
("/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Regular.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/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 {
|
||||
let regular_path = std::path::Path::new(regular);
|
||||
if let Some(regular_variant) = load_font_variant(regular_path) {
|
||||
let primary_font = regular_variant.clone_font();
|
||||
let font_data = regular_variant.clone_data();
|
||||
|
||||
let variants: [Option<FontVariant>; 4] = [
|
||||
Some(regular_variant),
|
||||
load_font_variant(std::path::Path::new(bold)),
|
||||
load_font_variant(std::path::Path::new(italic)),
|
||||
load_font_variant(std::path::Path::new(bold_italic)),
|
||||
];
|
||||
|
||||
log::info!("Loaded font from fallback paths:");
|
||||
log::info!(" Regular: {}", regular);
|
||||
if variants[1].is_some() { log::info!(" Bold: {}", bold); }
|
||||
if variants[2].is_some() { log::info!(" Italic: {}", italic); }
|
||||
if variants[3].is_some() { log::info!(" BoldItalic: {}", bold_italic); }
|
||||
|
||||
return (font_data, primary_font, variants);
|
||||
}
|
||||
}
|
||||
|
||||
// Last resort: try NotoSansMono
|
||||
let noto_regular = std::path::Path::new("/usr/share/fonts/noto/NotoSansMono-Regular.ttf");
|
||||
if let Some(regular_variant) = load_font_variant(noto_regular) {
|
||||
let primary_font = regular_variant.clone_font();
|
||||
let font_data = regular_variant.clone_data();
|
||||
let variants: [Option<FontVariant>; 4] = [Some(regular_variant), None, None, None];
|
||||
log::info!("Loaded NotoSansMono as fallback");
|
||||
return (font_data, primary_font, variants);
|
||||
}
|
||||
|
||||
panic!("Failed to load any monospace font");
|
||||
}
|
||||
@@ -88,7 +88,7 @@ fn vs_main(in: VertexInput) -> VertexOutput {
|
||||
}
|
||||
|
||||
@group(0) @binding(0)
|
||||
var atlas_texture: texture_2d_array<f32>;
|
||||
var atlas_textures: binding_array<texture_2d<f32>>;
|
||||
@group(0) @binding(1)
|
||||
var atlas_sampler: sampler;
|
||||
|
||||
@@ -103,7 +103,7 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
}
|
||||
|
||||
// Sample from RGBA atlas (layer 0 for legacy rendering)
|
||||
let glyph_sample = textureSample(atlas_texture, atlas_sampler, in.uv, 0);
|
||||
let glyph_sample = textureSample(atlas_textures[0], atlas_sampler, in.uv);
|
||||
|
||||
// Detect color glyphs: regular glyphs are stored as white (1,1,1) with alpha
|
||||
// Color glyphs have actual RGB colors. Check if any RGB channel is not white.
|
||||
@@ -697,7 +697,7 @@ fn fs_cell(in: CellVertexOutput) -> @location(0) vec4<f32> {
|
||||
} else {
|
||||
// Non-block cursors (bar, underline) - sample from pre-rendered cursor sprite
|
||||
// The cursor_uv was calculated in the vertex shader
|
||||
let cursor_sample = textureSample(atlas_texture, atlas_sampler, in.cursor_uv, in.cursor_layer);
|
||||
let cursor_sample = textureSample(atlas_textures[in.cursor_layer], atlas_sampler, in.cursor_uv);
|
||||
let cursor_alpha = cursor_sample.a;
|
||||
|
||||
if cursor_alpha > 0.0 {
|
||||
@@ -720,7 +720,7 @@ fn fs_cell(in: CellVertexOutput) -> @location(0) vec4<f32> {
|
||||
let has_glyph = in.uv.x != 0.0 || in.uv.y != 0.0;
|
||||
|
||||
if has_glyph {
|
||||
let glyph_sample = textureSample(atlas_texture, atlas_sampler, in.uv, in.glyph_layer);
|
||||
let glyph_sample = textureSample(atlas_textures[in.glyph_layer], atlas_sampler, in.uv);
|
||||
|
||||
if in.is_colored_glyph == 1u {
|
||||
// Colored glyph (emoji) - use atlas color directly
|
||||
@@ -744,7 +744,7 @@ fn fs_cell(in: CellVertexOutput) -> @location(0) vec4<f32> {
|
||||
|
||||
// Sample and blend underline decoration if present
|
||||
if in.has_underline > 0u {
|
||||
let underline_sample = textureSample(atlas_texture, atlas_sampler, in.underline_uv, in.underline_layer);
|
||||
let underline_sample = textureSample(atlas_textures[in.underline_layer], atlas_sampler, in.underline_uv);
|
||||
let underline_alpha = underline_sample.a;
|
||||
|
||||
if underline_alpha > 0.0 {
|
||||
@@ -758,7 +758,7 @@ fn fs_cell(in: CellVertexOutput) -> @location(0) vec4<f32> {
|
||||
|
||||
// Sample and blend strikethrough decoration if present
|
||||
if in.has_strikethrough > 0u {
|
||||
let strike_sample = textureSample(atlas_texture, atlas_sampler, in.strike_uv, in.strike_layer);
|
||||
let strike_sample = textureSample(atlas_textures[in.strike_layer], atlas_sampler, in.strike_uv);
|
||||
let strike_alpha = strike_sample.a;
|
||||
|
||||
if strike_alpha > 0.0 {
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
//! GPU data structures for terminal rendering.
|
||||
//!
|
||||
//! Contains vertex formats, uniform structures, and constants for wgpu rendering.
|
||||
//! All structures use `#[repr(C)]` and implement `bytemuck::Pod` for GPU compatibility.
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CONSTANTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Size of the glyph atlas texture (like Kitty's max_texture_size).
|
||||
/// 8192x8192 provides massive capacity before needing additional layers.
|
||||
pub const ATLAS_SIZE: u32 = 8192;
|
||||
|
||||
/// Maximum number of atlas layers (like Kitty's max_array_len).
|
||||
/// With 8192x8192 per layer, this provides virtually unlimited glyph storage.
|
||||
pub const MAX_ATLAS_LAYERS: u32 = 64;
|
||||
|
||||
/// Bytes per pixel in the RGBA atlas (4 for RGBA8).
|
||||
pub const ATLAS_BPP: u32 = 4;
|
||||
|
||||
/// Maximum number of simultaneous edge glows.
|
||||
pub const MAX_EDGE_GLOWS: usize = 16;
|
||||
|
||||
/// Color type constants for packed color encoding.
|
||||
pub const COLOR_TYPE_DEFAULT: u32 = 0;
|
||||
pub const COLOR_TYPE_INDEXED: u32 = 1;
|
||||
pub const COLOR_TYPE_RGB: u32 = 2;
|
||||
|
||||
/// Attribute bit flags.
|
||||
pub const ATTR_BOLD: u32 = 0x8;
|
||||
pub const ATTR_ITALIC: u32 = 0x10;
|
||||
pub const ATTR_REVERSE: u32 = 0x20;
|
||||
pub const ATTR_STRIKE: u32 = 0x40;
|
||||
pub const ATTR_DIM: u32 = 0x80;
|
||||
pub const ATTR_UNDERLINE: u32 = 0x1; // Part of decoration mask
|
||||
pub const ATTR_SELECTED: u32 = 0x100; // Cell is selected (for selection highlighting)
|
||||
|
||||
/// Flag for colored glyphs (emoji).
|
||||
pub const COLORED_GLYPH_FLAG: u32 = 0x80000000;
|
||||
|
||||
/// Pre-rendered cursor sprite indices (like Kitty's cursor_shape_map).
|
||||
/// These sprites are created at fixed indices in the sprite array after initialization.
|
||||
/// 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_UNDERLINE: u32 = 2; // Underline cursor (horizontal line at bottom)
|
||||
pub const CURSOR_SPRITE_HOLLOW: u32 = 3; // Hollow/unfocused cursor (outline rectangle)
|
||||
|
||||
/// Pre-rendered decoration sprite indices (like Kitty's decoration sprites).
|
||||
/// These are created after cursor sprites and used for text decorations.
|
||||
/// The shader uses these to render underlines, strikethrough, etc.
|
||||
pub const DECORATION_SPRITE_STRIKETHROUGH: u32 = 4; // Strikethrough line
|
||||
pub const DECORATION_SPRITE_UNDERLINE: u32 = 5; // Single 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_DOTTED: u32 = 8; // Dotted underline
|
||||
pub const DECORATION_SPRITE_DASHED: u32 = 9; // Dashed underline
|
||||
|
||||
/// First available sprite index for regular glyphs (after reserved cursor and decoration sprites)
|
||||
pub const FIRST_GLYPH_SPRITE: u32 = 10;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// VERTEX STRUCTURES
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Vertex for rendering textured quads.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||
pub struct GlyphVertex {
|
||||
pub position: [f32; 2],
|
||||
pub uv: [f32; 2],
|
||||
pub color: [f32; 4],
|
||||
pub bg_color: [f32; 4],
|
||||
}
|
||||
|
||||
impl GlyphVertex {
|
||||
pub const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
|
||||
0 => Float32x2, // position
|
||||
1 => Float32x2, // uv
|
||||
2 => Float32x4, // color (fg)
|
||||
3 => Float32x4, // bg_color
|
||||
];
|
||||
|
||||
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
|
||||
wgpu::VertexBufferLayout {
|
||||
array_stride: std::mem::size_of::<GlyphVertex>() as wgpu::BufferAddress,
|
||||
step_mode: wgpu::VertexStepMode::Vertex,
|
||||
attributes: &Self::ATTRIBS,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// EDGE GLOW STRUCTURES
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Per-glow instance data (48 bytes, aligned to 16 bytes).
|
||||
/// Must match GlowInstance in shader.wgsl exactly.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||
pub struct GlowInstance {
|
||||
pub direction: u32,
|
||||
pub progress: f32,
|
||||
pub color_r: f32,
|
||||
pub color_g: f32,
|
||||
pub color_b: f32,
|
||||
// Pane bounds in pixels
|
||||
pub pane_x: f32,
|
||||
pub pane_y: f32,
|
||||
pub pane_width: f32,
|
||||
pub pane_height: f32,
|
||||
pub _padding1: f32,
|
||||
pub _padding2: f32,
|
||||
pub _padding3: f32,
|
||||
}
|
||||
|
||||
/// GPU-compatible edge glow uniform data.
|
||||
/// Must match the layout in shader.wgsl exactly.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||
pub struct EdgeGlowUniforms {
|
||||
pub screen_width: f32,
|
||||
pub screen_height: f32,
|
||||
pub terminal_y_offset: f32,
|
||||
pub glow_intensity: f32,
|
||||
pub glow_count: u32,
|
||||
pub _padding: [u32; 3], // Pad to 16-byte alignment before array
|
||||
pub glows: [GlowInstance; MAX_EDGE_GLOWS],
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// IMAGE STRUCTURES
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// GPU-compatible image uniform data.
|
||||
/// Must match the layout in image_shader.wgsl exactly.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||
pub struct ImageUniforms {
|
||||
pub screen_width: f32,
|
||||
pub screen_height: f32,
|
||||
pub pos_x: f32,
|
||||
pub pos_y: f32,
|
||||
pub display_width: f32,
|
||||
pub display_height: f32,
|
||||
pub src_x: f32,
|
||||
pub src_y: f32,
|
||||
pub src_width: f32,
|
||||
pub src_height: f32,
|
||||
pub _padding1: f32,
|
||||
pub _padding2: f32,
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// INSTANCED CELL RENDERING STRUCTURES
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// GPU cell data for instanced rendering.
|
||||
/// Matches GPUCell in glyph_shader.wgsl exactly.
|
||||
///
|
||||
/// Like Kitty, we store a sprite_idx that references pre-rendered glyphs in the atlas.
|
||||
/// This allows us to update GPU buffers with a simple memcpy when content changes,
|
||||
/// rather than rebuilding vertex buffers every frame.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||
pub struct GPUCell {
|
||||
/// Foreground color (packed: type in low byte, then RGB or index)
|
||||
pub fg: u32,
|
||||
/// Background color (packed: type in low byte, then RGB or index)
|
||||
pub bg: u32,
|
||||
/// Decoration foreground color (for underlines, etc.)
|
||||
pub decoration_fg: u32,
|
||||
/// Sprite index in the sprite info array. High bit set = colored glyph.
|
||||
/// 0 = no glyph (space or empty)
|
||||
pub sprite_idx: u32,
|
||||
/// Cell attributes (bold, italic, reverse, etc.)
|
||||
pub attrs: u32,
|
||||
}
|
||||
|
||||
/// Sprite info for glyph positioning.
|
||||
/// Matches SpriteInfo in glyph_shader.wgsl exactly.
|
||||
///
|
||||
/// In Kitty's model, sprites are always cell-sized and glyphs are pre-positioned
|
||||
/// within the sprite at the correct baseline. The shader just maps the sprite
|
||||
/// to the cell 1:1, with no offset math needed.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||
pub struct SpriteInfo {
|
||||
/// UV coordinates in atlas (x, y, width, height) - normalized 0-1
|
||||
pub uv: [f32; 4],
|
||||
/// Atlas layer index (z-coordinate for texture array) and padding
|
||||
/// layer is the first f32, second f32 is unused padding
|
||||
pub layer: f32,
|
||||
pub _padding: f32,
|
||||
/// Size in pixels (width, height) - always matches cell dimensions
|
||||
pub size: [f32; 2],
|
||||
}
|
||||
|
||||
/// Font cell metrics with integer dimensions (like Kitty's FontCellMetrics).
|
||||
/// Using integers ensures pixel-perfect alignment and avoids floating-point precision issues.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct FontCellMetrics {
|
||||
/// Cell width in pixels (computed using ceil from font advance).
|
||||
pub cell_width: u32,
|
||||
/// Cell height in pixels (computed using ceil from font height).
|
||||
pub cell_height: u32,
|
||||
/// Baseline offset from top of cell in pixels.
|
||||
pub baseline: u32,
|
||||
/// Y position for underline (from top of cell, in pixels).
|
||||
/// Computed from font metrics: ascender - underline_position.
|
||||
pub underline_position: u32,
|
||||
/// Thickness of underline in pixels.
|
||||
pub underline_thickness: u32,
|
||||
/// Y position for strikethrough (from top of cell, in pixels).
|
||||
/// Typically around 65% of baseline from top.
|
||||
pub strikethrough_position: u32,
|
||||
/// Thickness of strikethrough in pixels.
|
||||
pub strikethrough_thickness: u32,
|
||||
}
|
||||
|
||||
/// Grid parameters uniform for instanced rendering.
|
||||
/// Matches GridParams in glyph_shader.wgsl exactly.
|
||||
/// Uses Kitty-style NDC positioning: viewport is set per-pane, so shader
|
||||
/// works in pure NDC space without needing pixel offsets.
|
||||
/// Cell dimensions are integers like Kitty for pixel-perfect rendering.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||
pub struct GridParams {
|
||||
pub cols: u32,
|
||||
pub rows: u32,
|
||||
pub cell_width: u32,
|
||||
pub cell_height: u32,
|
||||
pub cursor_col: i32,
|
||||
pub cursor_row: i32,
|
||||
pub cursor_style: u32,
|
||||
pub background_opacity: f32,
|
||||
// Selection range (-1 values mean no selection)
|
||||
pub selection_start_col: i32,
|
||||
pub selection_start_row: i32,
|
||||
pub selection_end_col: i32,
|
||||
pub selection_end_row: i32,
|
||||
}
|
||||
|
||||
/// GPU quad instance for instanced rectangle rendering.
|
||||
/// Matches Quad in glyph_shader.wgsl exactly.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||
pub struct Quad {
|
||||
/// X position in pixels
|
||||
pub x: f32,
|
||||
/// Y position in pixels
|
||||
pub y: f32,
|
||||
/// Width in pixels
|
||||
pub width: f32,
|
||||
/// Height in pixels
|
||||
pub height: f32,
|
||||
/// Color (linear RGBA)
|
||||
pub color: [f32; 4],
|
||||
}
|
||||
|
||||
/// Parameters for quad rendering.
|
||||
/// Matches QuadParams in glyph_shader.wgsl exactly.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||
pub struct QuadParams {
|
||||
pub screen_width: f32,
|
||||
pub screen_height: f32,
|
||||
pub _padding: [f32; 2],
|
||||
}
|
||||
|
||||
/// Parameters for statusline rendering.
|
||||
/// Matches StatuslineParams in statusline_shader.wgsl exactly.
|
||||
#[repr(C)]
|
||||
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||
pub struct StatuslineParams {
|
||||
/// Number of characters in statusline
|
||||
pub char_count: u32,
|
||||
/// Cell width in pixels
|
||||
pub cell_width: f32,
|
||||
/// Cell height in pixels
|
||||
pub cell_height: f32,
|
||||
/// Screen width in pixels
|
||||
pub screen_width: f32,
|
||||
/// Screen height in pixels
|
||||
pub screen_height: f32,
|
||||
/// Y offset from top of screen in pixels
|
||||
pub y_offset: f32,
|
||||
/// Padding for alignment (to match shader struct layout)
|
||||
pub _padding: [f32; 2],
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
//! Image rendering for the Kitty Graphics Protocol.
|
||||
//!
|
||||
//! This module handles GPU-accelerated rendering of images in the terminal,
|
||||
//! supporting the Kitty Graphics Protocol for inline image display.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use crate::gpu_types::ImageUniforms;
|
||||
use crate::graphics::{ImageData, ImagePlacement, ImageStorage};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// GPU IMAGE
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Cached GPU texture for an image.
|
||||
pub struct GpuImage {
|
||||
pub texture: wgpu::Texture,
|
||||
pub view: wgpu::TextureView,
|
||||
pub uniform_buffer: wgpu::Buffer,
|
||||
pub bind_group: wgpu::BindGroup,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// IMAGE RENDERER
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Manages GPU resources for image rendering.
|
||||
/// Handles uploading, caching, and preparing images for rendering.
|
||||
pub struct ImageRenderer {
|
||||
/// Bind group layout for image rendering.
|
||||
bind_group_layout: wgpu::BindGroupLayout,
|
||||
/// Sampler for image textures.
|
||||
sampler: wgpu::Sampler,
|
||||
/// Cached GPU textures for images, keyed by image ID.
|
||||
textures: HashMap<u32, GpuImage>,
|
||||
}
|
||||
|
||||
impl ImageRenderer {
|
||||
/// Create a new ImageRenderer with the necessary GPU resources.
|
||||
pub fn new(device: &wgpu::Device) -> Self {
|
||||
// Create sampler for images (linear filtering for smooth scaling)
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("Image Sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Create bind group layout for images
|
||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("Image Bind Group Layout"),
|
||||
entries: &[
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Uniform,
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
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,
|
||||
},
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Self {
|
||||
bind_group_layout,
|
||||
sampler,
|
||||
textures: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the bind group layout for creating the image pipeline.
|
||||
pub fn bind_group_layout(&self) -> &wgpu::BindGroupLayout {
|
||||
&self.bind_group_layout
|
||||
}
|
||||
|
||||
/// Get a GPU image by ID.
|
||||
pub fn get(&self, image_id: &u32) -> Option<&GpuImage> {
|
||||
self.textures.get(image_id)
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
// Get current frame data (handles animation frames automatically)
|
||||
let data = image.current_frame_data();
|
||||
|
||||
// Check if we already have this image
|
||||
if let Some(existing) = self.textures.get(&image.id) {
|
||||
if existing.width == image.width && existing.height == image.height {
|
||||
// Same dimensions, just update the data
|
||||
queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &existing.texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
data,
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(image.width * 4),
|
||||
rows_per_image: Some(image.height),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Different dimensions, need to recreate
|
||||
}
|
||||
|
||||
// Create new texture
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some(&format!("Image {}", image.id)),
|
||||
size: wgpu::Extent3d {
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
|
||||
// Upload the data
|
||||
queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::ZERO,
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
data,
|
||||
wgpu::TexelCopyBufferLayout {
|
||||
offset: 0,
|
||||
bytes_per_row: Some(image.width * 4),
|
||||
rows_per_image: Some(image.height),
|
||||
},
|
||||
wgpu::Extent3d {
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
depth_or_array_layers: 1,
|
||||
},
|
||||
);
|
||||
|
||||
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 {
|
||||
label: Some(&format!("Image {} Bind Group", image.id)),
|
||||
layout: &self.bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: uniform_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(&view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
self.textures.insert(image.id, GpuImage {
|
||||
texture,
|
||||
view,
|
||||
uniform_buffer,
|
||||
bind_group,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
});
|
||||
|
||||
log::debug!(
|
||||
"Uploaded image {} ({}x{}) to GPU",
|
||||
image.id,
|
||||
image.width,
|
||||
image.height
|
||||
);
|
||||
}
|
||||
|
||||
/// Remove an image from the GPU.
|
||||
pub fn remove_image(&mut self, image_id: u32) {
|
||||
if self.textures.remove(&image_id).is_some() {
|
||||
log::debug!("Removed image {} from GPU", image_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync images from terminal's image storage to GPU.
|
||||
/// Uploads new/changed images and removes deleted ones.
|
||||
/// Also updates animation frames.
|
||||
pub fn sync_images(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, storage: &mut ImageStorage) {
|
||||
// Update animations and get list of changed image IDs
|
||||
let changed_ids = storage.update_animations();
|
||||
|
||||
// Re-upload frames that changed due to animation
|
||||
for id in &changed_ids {
|
||||
if let Some(image) = storage.get_image(*id) {
|
||||
self.upload_image(device, queue, image);
|
||||
}
|
||||
}
|
||||
|
||||
if !storage.dirty && changed_ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload all images (upload_image handles deduplication)
|
||||
for image in storage.images().values() {
|
||||
self.upload_image(device, queue, 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();
|
||||
}
|
||||
|
||||
/// Prepare image renders for a pane.
|
||||
/// Returns a Vec of (image_id, uniforms) for deferred rendering.
|
||||
pub fn prepare_image_renders(
|
||||
&self,
|
||||
placements: &[ImagePlacement],
|
||||
pane_x: f32,
|
||||
pane_y: f32,
|
||||
cell_width: f32,
|
||||
cell_height: f32,
|
||||
screen_width: f32,
|
||||
screen_height: f32,
|
||||
scrollback_len: usize,
|
||||
scroll_offset: usize,
|
||||
visible_rows: usize,
|
||||
) -> Vec<(u32, ImageUniforms)> {
|
||||
let mut renders = Vec::new();
|
||||
|
||||
for placement in placements {
|
||||
// Check if we have the GPU texture for this image
|
||||
let gpu_image = match self.textures.get(&placement.image_id) {
|
||||
Some(img) => img,
|
||||
None => continue, // Skip if not uploaded yet
|
||||
};
|
||||
|
||||
// Convert absolute row to visible screen row
|
||||
// placement.row is absolute (scrollback_len_at_placement + cursor_row)
|
||||
// visible_row = absolute_row - scrollback_len + scroll_offset
|
||||
let absolute_row = placement.row as isize;
|
||||
let visible_row = absolute_row - scrollback_len as isize + scroll_offset as isize;
|
||||
|
||||
// Check if image is visible on screen
|
||||
// Image spans from visible_row to visible_row + placement.rows
|
||||
let image_bottom = visible_row + placement.rows as isize;
|
||||
if image_bottom < 0 || visible_row >= visible_rows as isize {
|
||||
continue; // Image is completely off-screen
|
||||
}
|
||||
|
||||
// Calculate display position in pixels
|
||||
let pos_x = pane_x + (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!(
|
||||
"Image render: pane_x={} col={} cell_width={} x_offset={} => pos_x={}",
|
||||
pane_x, placement.col, cell_width, placement.x_offset, pos_x
|
||||
);
|
||||
|
||||
// Calculate display size in pixels
|
||||
let display_width = placement.cols as f32 * cell_width;
|
||||
let display_height = placement.rows as f32 * cell_height;
|
||||
|
||||
// Calculate source rectangle in normalized coordinates
|
||||
let src_x = placement.src_x as f32 / gpu_image.width as f32;
|
||||
let src_y = placement.src_y as f32 / gpu_image.height as f32;
|
||||
let src_width = if placement.src_width == 0 {
|
||||
1.0 - src_x
|
||||
} else {
|
||||
placement.src_width as f32 / gpu_image.width as f32
|
||||
};
|
||||
let src_height = if placement.src_height == 0 {
|
||||
1.0 - src_y
|
||||
} else {
|
||||
placement.src_height as f32 / gpu_image.height as f32
|
||||
};
|
||||
|
||||
let uniforms = ImageUniforms {
|
||||
screen_width,
|
||||
screen_height,
|
||||
pos_x,
|
||||
pos_y,
|
||||
display_width,
|
||||
display_height,
|
||||
src_x,
|
||||
src_y,
|
||||
src_width,
|
||||
src_height,
|
||||
_padding1: 0.0,
|
||||
_padding2: 0.0,
|
||||
};
|
||||
|
||||
renders.push((placement.image_id, uniforms));
|
||||
}
|
||||
|
||||
renders
|
||||
}
|
||||
}
|
||||
+11
@@ -2,10 +2,21 @@
|
||||
//!
|
||||
//! Single-process architecture: one process owns PTY, terminal state, and rendering.
|
||||
|
||||
pub mod box_drawing;
|
||||
pub mod color;
|
||||
pub mod color_font;
|
||||
pub mod config;
|
||||
pub mod font_loader;
|
||||
pub mod edge_glow;
|
||||
pub mod gpu_types;
|
||||
pub mod graphics;
|
||||
pub mod image_renderer;
|
||||
pub mod keyboard;
|
||||
pub mod pane_resources;
|
||||
pub mod pipeline;
|
||||
pub mod pty;
|
||||
pub mod renderer;
|
||||
pub mod statusline;
|
||||
pub mod terminal;
|
||||
pub mod simd_utf8;
|
||||
pub mod vt_parser;
|
||||
|
||||
+559
-529
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
//! Per-pane GPU resources for multi-pane terminal rendering.
|
||||
//!
|
||||
//! This module provides GPU resource management for individual terminal panes,
|
||||
//! following Kitty's VAO-per-window approach where each pane gets its own
|
||||
//! buffers and bind group for independent rendering.
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PER-PANE GPU RESOURCES (Like Kitty's VAO per window)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// GPU resources for a single pane.
|
||||
/// Like Kitty's VAO, each pane gets its own buffers and bind group.
|
||||
/// This allows uploading each pane's cell data independently before rendering.
|
||||
pub struct PaneGpuResources {
|
||||
/// Cell storage buffer - contains GPUCell array for this pane's visible cells.
|
||||
pub cell_buffer: wgpu::Buffer,
|
||||
/// Grid parameters uniform buffer for this pane.
|
||||
pub grid_params_buffer: wgpu::Buffer,
|
||||
/// Bind group for instanced rendering (@group(1)) - references this pane's buffers.
|
||||
pub bind_group: wgpu::BindGroup,
|
||||
/// Buffer capacity (max cells) - used to detect when buffer needs resizing.
|
||||
pub capacity: usize,
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//! Render pipeline builder for wgpu.
|
||||
//!
|
||||
//! This module provides a builder pattern for creating wgpu render pipelines
|
||||
//! with common settings, reducing boilerplate when creating multiple pipelines.
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PIPELINE BUILDER
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// Builder for creating render pipelines with common settings.
|
||||
/// Captures the device, shader, layout, and format that are shared across many pipelines.
|
||||
pub struct PipelineBuilder<'a> {
|
||||
device: &'a wgpu::Device,
|
||||
shader: &'a wgpu::ShaderModule,
|
||||
layout: &'a wgpu::PipelineLayout,
|
||||
format: wgpu::TextureFormat,
|
||||
}
|
||||
|
||||
impl<'a> PipelineBuilder<'a> {
|
||||
/// Create a new pipeline builder with shared settings.
|
||||
pub fn new(
|
||||
device: &'a wgpu::Device,
|
||||
shader: &'a wgpu::ShaderModule,
|
||||
layout: &'a wgpu::PipelineLayout,
|
||||
format: wgpu::TextureFormat,
|
||||
) -> Self {
|
||||
Self { device, shader, layout, format }
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
self.build_full(label, vs_entry, fs_entry, blend, wgpu::PrimitiveTopology::TriangleStrip, &[])
|
||||
}
|
||||
|
||||
/// Build a pipeline with custom topology and vertex buffers.
|
||||
pub fn build_full(
|
||||
&self,
|
||||
label: &str,
|
||||
vs_entry: &str,
|
||||
fs_entry: &str,
|
||||
blend: wgpu::BlendState,
|
||||
topology: wgpu::PrimitiveTopology,
|
||||
vertex_buffers: &[wgpu::VertexBufferLayout<'_>],
|
||||
) -> wgpu::RenderPipeline {
|
||||
self.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some(label),
|
||||
layout: Some(self.layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: self.shader,
|
||||
entry_point: Some(vs_entry),
|
||||
buffers: vertex_buffers,
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
},
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: self.shader,
|
||||
entry_point: Some(fs_entry),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format: self.format,
|
||||
blend: Some(blend),
|
||||
write_mask: wgpu::ColorWrites::ALL,
|
||||
})],
|
||||
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||
}),
|
||||
primitive: wgpu::PrimitiveState {
|
||||
topology,
|
||||
strip_index_format: None,
|
||||
front_face: wgpu::FrontFace::Ccw,
|
||||
cull_mode: None,
|
||||
polygon_mode: wgpu::PolygonMode::Fill,
|
||||
unclipped_depth: false,
|
||||
conservative: false,
|
||||
},
|
||||
depth_stencil: None,
|
||||
multisample: wgpu::MultisampleState::default(),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
+511
-3971
File diff suppressed because it is too large
Load Diff
+1476
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
||||
//! Statusline types and rendering.
|
||||
//!
|
||||
//! Provides data structures for building structured statusline content with
|
||||
//! powerline-style sections and components.
|
||||
|
||||
/// Color specification for statusline components.
|
||||
/// Uses the terminal's indexed color palette (0-255), RGB, or default fg.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StatuslineColor {
|
||||
/// Use the default foreground color.
|
||||
Default,
|
||||
/// Use an indexed color from the 256-color palette (0-15 for ANSI colors).
|
||||
Indexed(u8),
|
||||
/// Use an RGB color.
|
||||
Rgb(u8, u8, u8),
|
||||
}
|
||||
|
||||
impl Default for StatuslineColor {
|
||||
fn default() -> Self {
|
||||
StatuslineColor::Default
|
||||
}
|
||||
}
|
||||
|
||||
/// A single component/segment of the statusline.
|
||||
/// Components are rendered left-to-right with optional separators.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StatuslineComponent {
|
||||
/// The text content of this component.
|
||||
pub text: String,
|
||||
/// Foreground color for this component.
|
||||
pub fg: StatuslineColor,
|
||||
/// Whether this text should be bold.
|
||||
pub bold: bool,
|
||||
}
|
||||
|
||||
impl StatuslineComponent {
|
||||
/// Create a new statusline component with default styling.
|
||||
pub fn new(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
fg: StatuslineColor::Default,
|
||||
bold: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the foreground color using an indexed palette color.
|
||||
pub fn fg(mut self, color_index: u8) -> Self {
|
||||
self.fg = StatuslineColor::Indexed(color_index);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the foreground color using RGB values.
|
||||
pub fn rgb_fg(mut self, r: u8, g: u8, b: u8) -> Self {
|
||||
self.fg = StatuslineColor::Rgb(r, g, b);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set bold styling.
|
||||
pub fn bold(mut self) -> Self {
|
||||
self.bold = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create a separator component (e.g., "/", " > ", etc.).
|
||||
pub fn separator(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
fg: StatuslineColor::Indexed(8), // Dim gray by default
|
||||
bold: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A section of the statusline with its own background color.
|
||||
/// Sections are rendered left-to-right and end with a powerline transition arrow.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StatuslineSection {
|
||||
/// The components within this section.
|
||||
pub components: Vec<StatuslineComponent>,
|
||||
/// Background color for this section.
|
||||
pub bg: StatuslineColor,
|
||||
}
|
||||
|
||||
impl StatuslineSection {
|
||||
/// Create a new section with the given indexed background color.
|
||||
pub fn new(bg_color: u8) -> Self {
|
||||
Self {
|
||||
components: Vec::new(),
|
||||
bg: StatuslineColor::Indexed(bg_color),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new section with an RGB background color.
|
||||
pub fn with_rgb_bg(r: u8, g: u8, b: u8) -> Self {
|
||||
Self {
|
||||
components: Vec::new(),
|
||||
bg: StatuslineColor::Rgb(r, g, b),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new section with the default (transparent) background.
|
||||
pub fn transparent() -> Self {
|
||||
Self {
|
||||
components: Vec::new(),
|
||||
bg: StatuslineColor::Default,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a component to this section.
|
||||
pub fn push(mut self, component: StatuslineComponent) -> Self {
|
||||
self.components.push(component);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple components to this section.
|
||||
pub fn with_components(mut self, components: Vec<StatuslineComponent>) -> Self {
|
||||
self.components = components;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Content to display in the statusline.
|
||||
/// Either structured sections (for ZTerm's default CWD/git display) or raw ANSI
|
||||
/// content (from neovim or other programs that provide their own statusline).
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum StatuslineContent {
|
||||
/// Structured sections with powerline-style transitions.
|
||||
Sections(Vec<StatuslineSection>),
|
||||
/// Raw ANSI-formatted string (rendered as-is without section styling).
|
||||
Raw(String),
|
||||
}
|
||||
|
||||
impl Default for StatuslineContent {
|
||||
fn default() -> Self {
|
||||
StatuslineContent::Sections(Vec::new())
|
||||
}
|
||||
}
|
||||
@@ -103,7 +103,7 @@ struct ColorTable {
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@group(0) @binding(0)
|
||||
var atlas_texture: texture_2d_array<f32>;
|
||||
var atlas_textures: binding_array<texture_2d<f32>>;
|
||||
@group(0) @binding(1)
|
||||
var atlas_sampler: sampler;
|
||||
|
||||
@@ -312,8 +312,8 @@ fn fs_statusline(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||
return in.bg_color;
|
||||
}
|
||||
|
||||
// Sample glyph from atlas (using layer for texture array)
|
||||
let glyph_sample = textureSample(atlas_texture, atlas_sampler, in.uv, in.glyph_layer);
|
||||
// Sample glyph from atlas (using layer to index texture array)
|
||||
let glyph_sample = textureSample(atlas_textures[in.glyph_layer], atlas_sampler, in.uv);
|
||||
|
||||
if in.is_colored_glyph == 1u {
|
||||
// Colored glyph (emoji) - use atlas color directly
|
||||
|
||||
+324
-317
@@ -2,7 +2,7 @@
|
||||
|
||||
use crate::graphics::{GraphicsCommand, ImageStorage};
|
||||
use crate::keyboard::{query_response, KeyboardState};
|
||||
use crate::vt_parser::{CsiParams, Handler, Parser};
|
||||
use crate::vt_parser::{CsiParams, Handler};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
/// Commands that the terminal can send to the application.
|
||||
@@ -265,50 +265,80 @@ struct AlternateScreen {
|
||||
}
|
||||
|
||||
/// Timing stats for performance debugging.
|
||||
/// Only populated when the `render_timing` feature is enabled.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ProcessingStats {
|
||||
#[cfg(feature = "render_timing")]
|
||||
/// Total time spent in scroll_up operations (nanoseconds).
|
||||
pub scroll_up_ns: u64,
|
||||
#[cfg(feature = "render_timing")]
|
||||
/// Number of scroll_up calls.
|
||||
pub scroll_up_count: u32,
|
||||
#[cfg(feature = "render_timing")]
|
||||
/// Total time spent in scrollback operations (nanoseconds).
|
||||
pub scrollback_ns: u64,
|
||||
#[cfg(feature = "render_timing")]
|
||||
/// Time in VecDeque pop_front.
|
||||
pub pop_front_ns: u64,
|
||||
#[cfg(feature = "render_timing")]
|
||||
/// Time in VecDeque push_back.
|
||||
pub push_back_ns: u64,
|
||||
#[cfg(feature = "render_timing")]
|
||||
/// Time in mem::swap.
|
||||
pub swap_ns: u64,
|
||||
#[cfg(feature = "render_timing")]
|
||||
/// Total time spent in line clearing (nanoseconds).
|
||||
pub clear_line_ns: u64,
|
||||
#[cfg(feature = "render_timing")]
|
||||
/// Total time spent in text handler (nanoseconds).
|
||||
pub text_handler_ns: u64,
|
||||
#[cfg(feature = "render_timing")]
|
||||
/// Total time spent in CSI handler (nanoseconds).
|
||||
pub csi_handler_ns: u64,
|
||||
#[cfg(feature = "render_timing")]
|
||||
/// Number of CSI sequences processed.
|
||||
pub csi_count: u32,
|
||||
#[cfg(feature = "render_timing")]
|
||||
/// Number of characters processed.
|
||||
pub chars_processed: u32,
|
||||
#[cfg(feature = "render_timing")]
|
||||
/// Total time spent in VT parser (consume_input) - nanoseconds.
|
||||
pub vt_parser_ns: u64,
|
||||
#[cfg(feature = "render_timing")]
|
||||
/// Number of consume_input calls.
|
||||
pub consume_input_count: u32,
|
||||
}
|
||||
|
||||
impl ProcessingStats {
|
||||
#[cfg(feature = "render_timing")]
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "render_timing"))]
|
||||
pub fn reset(&mut self) {}
|
||||
|
||||
#[cfg(feature = "render_timing")]
|
||||
pub fn log_if_slow(&self, threshold_ms: u64) {
|
||||
let total_ms = (self.scroll_up_ns + self.text_handler_ns) / 1_000_000;
|
||||
let total_ms = (self.scroll_up_ns + self.text_handler_ns + self.csi_handler_ns) / 1_000_000;
|
||||
if total_ms >= threshold_ms {
|
||||
let vt_only_ns = self.vt_parser_ns.saturating_sub(self.text_handler_ns + self.csi_handler_ns);
|
||||
log::info!(
|
||||
"TIMING: scroll_up={:.2}ms ({}x), scrollback={:.2}ms [pop={:.2}ms swap={:.2}ms push={:.2}ms], clear={:.2}ms, text={:.2}ms, chars={}",
|
||||
self.scroll_up_ns as f64 / 1_000_000.0,
|
||||
self.scroll_up_count,
|
||||
self.scrollback_ns as f64 / 1_000_000.0,
|
||||
self.pop_front_ns as f64 / 1_000_000.0,
|
||||
self.swap_ns as f64 / 1_000_000.0,
|
||||
self.push_back_ns as f64 / 1_000_000.0,
|
||||
self.clear_line_ns as f64 / 1_000_000.0,
|
||||
"[PARSE_DETAIL] text={:.2}ms ({}chars) csi={:.2}ms ({}x) vt_only={:.2}ms ({}calls) scroll={:.2}ms ({}x)",
|
||||
self.text_handler_ns as f64 / 1_000_000.0,
|
||||
self.chars_processed,
|
||||
self.csi_handler_ns as f64 / 1_000_000.0,
|
||||
self.csi_count,
|
||||
vt_only_ns as f64 / 1_000_000.0,
|
||||
self.consume_input_count,
|
||||
self.scroll_up_ns as f64 / 1_000_000.0,
|
||||
self.scroll_up_count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "render_timing"))]
|
||||
pub fn log_if_slow(&self, _threshold_ms: u64) {}
|
||||
}
|
||||
|
||||
/// Kitty-style ring buffer for scrollback history.
|
||||
@@ -496,11 +526,6 @@ pub struct Terminal {
|
||||
pub focus_reporting: bool,
|
||||
/// Synchronized output mode (for reducing flicker).
|
||||
synchronized_output: bool,
|
||||
/// Pool of pre-allocated empty lines to avoid allocation during scrolling.
|
||||
/// When we need a new line, we pop from this pool instead of allocating.
|
||||
line_pool: Vec<Vec<Cell>>,
|
||||
/// VT parser for escape sequence handling.
|
||||
parser: Option<Parser>,
|
||||
/// Performance timing stats (for debugging).
|
||||
pub stats: ProcessingStats,
|
||||
/// Command queue for terminal-to-application communication.
|
||||
@@ -517,21 +542,12 @@ pub struct Terminal {
|
||||
impl Terminal {
|
||||
/// Default scrollback limit (10,000 lines for better cache performance).
|
||||
pub const DEFAULT_SCROLLBACK_LIMIT: usize = 10_000;
|
||||
|
||||
/// Size of the line pool for recycling allocations.
|
||||
/// This avoids allocation during the first N scrolls before scrollback is full.
|
||||
const LINE_POOL_SIZE: usize = 64;
|
||||
|
||||
/// Creates a new terminal with the given dimensions and scrollback limit.
|
||||
pub fn new(cols: usize, rows: usize, scrollback_limit: usize) -> Self {
|
||||
log::info!("Terminal::new: cols={}, rows={}, scroll_bottom={}", cols, rows, rows.saturating_sub(1));
|
||||
let grid = vec![vec![Cell::default(); cols]; rows];
|
||||
let line_map: Vec<usize> = (0..rows).collect();
|
||||
|
||||
// Pre-allocate a pool of empty lines to avoid allocation during scrolling
|
||||
let line_pool: Vec<Vec<Cell>> = (0..Self::LINE_POOL_SIZE)
|
||||
.map(|_| vec![Cell::default(); cols])
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
grid,
|
||||
@@ -567,8 +583,6 @@ impl Terminal {
|
||||
bracketed_paste: false,
|
||||
focus_reporting: false,
|
||||
synchronized_output: false,
|
||||
line_pool,
|
||||
parser: Some(Parser::new()),
|
||||
stats: ProcessingStats::default(),
|
||||
command_queue: Vec::new(),
|
||||
image_storage: ImageStorage::new(),
|
||||
@@ -577,16 +591,6 @@ impl Terminal {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a line to the pool for reuse (if pool isn't full).
|
||||
#[allow(dead_code)]
|
||||
#[inline]
|
||||
fn return_line_to_pool(&mut self, line: Vec<Cell>) {
|
||||
if self.line_pool.len() < Self::LINE_POOL_SIZE {
|
||||
self.line_pool.push(line);
|
||||
}
|
||||
// Otherwise, let the line be dropped
|
||||
}
|
||||
|
||||
/// Mark a specific line as dirty (needs redrawing).
|
||||
#[inline]
|
||||
pub fn mark_line_dirty(&mut self, line: usize) {
|
||||
@@ -642,6 +646,32 @@ impl Terminal {
|
||||
self.synchronized_output
|
||||
}
|
||||
|
||||
/// Advance cursor to next row, scrolling if necessary.
|
||||
/// This is the common pattern: increment row, scroll if past scroll_bottom.
|
||||
#[inline]
|
||||
fn advance_row(&mut self) {
|
||||
self.cursor_row += 1;
|
||||
if self.cursor_row > self.scroll_bottom {
|
||||
self.scroll_up(1);
|
||||
self.cursor_row = self.scroll_bottom;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a cell with current text attributes.
|
||||
#[inline]
|
||||
fn make_cell(&self, character: char, wide_continuation: bool) -> Cell {
|
||||
Cell {
|
||||
character,
|
||||
fg_color: self.current_fg,
|
||||
bg_color: self.current_bg,
|
||||
bold: self.current_bold,
|
||||
italic: self.current_italic,
|
||||
underline_style: self.current_underline_style,
|
||||
strikethrough: self.current_strikethrough,
|
||||
wide_continuation,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the actual grid row index for a visual row.
|
||||
#[inline]
|
||||
pub fn grid_row(&self, visual_row: usize) -> usize {
|
||||
@@ -667,7 +697,7 @@ impl Terminal {
|
||||
let blank = self.blank_cell();
|
||||
let row = &mut self.grid[grid_row];
|
||||
// Ensure row has correct width (may differ after swap with scrollback post-resize)
|
||||
row.resize(self.cols, blank.clone());
|
||||
row.resize(self.cols, blank);
|
||||
row.fill(blank);
|
||||
}
|
||||
|
||||
@@ -695,16 +725,8 @@ impl Terminal {
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes raw bytes from the PTY using the internal VT parser.
|
||||
/// Uses Kitty-style architecture: UTF-8 decode until ESC, then parse escape sequences.
|
||||
pub fn process(&mut self, bytes: &[u8]) {
|
||||
// We need to temporarily take ownership of the parser to satisfy the borrow checker,
|
||||
// since parse() needs &mut self for both parser and handler (Terminal).
|
||||
// Use Option::take to avoid creating a new default parser each time.
|
||||
if let Some(mut parser) = self.parser.take() {
|
||||
parser.parse(bytes, self);
|
||||
self.parser = Some(parser);
|
||||
}
|
||||
/// Mark terminal as dirty (needs redraw). Called after parsing.
|
||||
pub fn mark_dirty(&mut self) {
|
||||
self.dirty = true;
|
||||
}
|
||||
|
||||
@@ -836,7 +858,8 @@ impl Terminal {
|
||||
let region_size = self.scroll_bottom - self.scroll_top + 1;
|
||||
let n = n.min(region_size);
|
||||
|
||||
self.stats.scroll_up_count += n as u32;
|
||||
#[cfg(feature = "render_timing")]
|
||||
{ self.stats.scroll_up_count += n as u32; }
|
||||
|
||||
for _ in 0..n {
|
||||
// Save the top line's grid index before rotation
|
||||
@@ -869,14 +892,36 @@ impl Terminal {
|
||||
self.mark_region_dirty(self.scroll_top, self.scroll_bottom);
|
||||
}
|
||||
|
||||
/// Mark a range of lines as dirty efficiently.
|
||||
/// Mark a range of lines as dirty efficiently using bitmask operations.
|
||||
#[inline]
|
||||
fn mark_region_dirty(&mut self, start: usize, end: usize) {
|
||||
// For small regions (< 64 lines), this is faster than individual calls
|
||||
for line in start..=end.min(255) {
|
||||
let word = line / 64;
|
||||
let bit = line % 64;
|
||||
self.dirty_lines[word] |= 1u64 << bit;
|
||||
let end = end.min(255);
|
||||
if start > end {
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each 64-bit word that overlaps with [start, end]
|
||||
let start_word = start / 64;
|
||||
let end_word = end / 64;
|
||||
|
||||
for word_idx in start_word..=end_word.min(3) {
|
||||
let word_start = word_idx * 64;
|
||||
let word_end = word_start + 63;
|
||||
|
||||
// Calculate bit range within this word
|
||||
let bit_start = if start > word_start { start - word_start } else { 0 };
|
||||
let bit_end = if end < word_end { end - word_start } else { 63 };
|
||||
|
||||
// Create mask for bits [bit_start, bit_end]
|
||||
// mask = ((1 << (bit_end - bit_start + 1)) - 1) << bit_start
|
||||
let num_bits = bit_end - bit_start + 1;
|
||||
let mask = if num_bits >= 64 {
|
||||
!0u64
|
||||
} else {
|
||||
((1u64 << num_bits) - 1) << bit_start
|
||||
};
|
||||
|
||||
self.dirty_lines[word_idx] |= mask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1097,6 +1142,39 @@ impl Terminal {
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
/// Get a single visible row by index without allocation.
|
||||
/// Returns None if row_idx is out of bounds.
|
||||
#[inline]
|
||||
pub fn get_visible_row(&self, row_idx: usize) -> Option<&Vec<Cell>> {
|
||||
if row_idx >= self.rows {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.scroll_offset == 0 {
|
||||
// No scrollback viewing, just return from grid via line_map
|
||||
Some(&self.grid[self.line_map[row_idx]])
|
||||
} else {
|
||||
// We're viewing scrollback
|
||||
let scrollback_len = self.scrollback.len();
|
||||
let lines_from_scrollback = self.scroll_offset.min(self.rows);
|
||||
|
||||
if row_idx < lines_from_scrollback {
|
||||
// This row comes from scrollback
|
||||
let scrollback_idx = scrollback_len - self.scroll_offset + row_idx;
|
||||
self.scrollback.get(scrollback_idx)
|
||||
.or_else(|| Some(&self.grid[self.line_map[row_idx]]))
|
||||
} else {
|
||||
// This row comes from the grid
|
||||
let grid_visual_idx = row_idx - lines_from_scrollback;
|
||||
if grid_visual_idx < self.rows {
|
||||
Some(&self.grid[self.line_map[grid_visual_idx]])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts n blank lines at the cursor position, scrolling lines below down.
|
||||
/// Uses line_map rotation for efficiency.
|
||||
@@ -1119,11 +1197,11 @@ impl Terminal {
|
||||
|
||||
// Clear the recycled line (now at cursor position)
|
||||
self.clear_grid_row(recycled_grid_row);
|
||||
|
||||
// Mark affected lines dirty
|
||||
for line in self.cursor_row..=self.scroll_bottom {
|
||||
self.mark_line_dirty(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark affected lines dirty once after all rotations
|
||||
for line in self.cursor_row..=self.scroll_bottom {
|
||||
self.mark_line_dirty(line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1148,11 +1226,11 @@ impl Terminal {
|
||||
|
||||
// Clear the recycled line (now at bottom of scroll region)
|
||||
self.clear_grid_row(recycled_grid_row);
|
||||
|
||||
// Mark affected lines dirty
|
||||
for line in self.cursor_row..=self.scroll_bottom {
|
||||
self.mark_line_dirty(line);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark affected lines dirty once after all rotations
|
||||
for line in self.cursor_row..=self.scroll_bottom {
|
||||
self.mark_line_dirty(line);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1162,14 +1240,10 @@ impl Terminal {
|
||||
let blank = self.blank_cell();
|
||||
let row = &mut self.grid[grid_row];
|
||||
let n = n.min(self.cols - self.cursor_col);
|
||||
// Remove n characters from the end
|
||||
for _ in 0..n {
|
||||
row.pop();
|
||||
}
|
||||
// Insert n blank characters at cursor position
|
||||
for _ in 0..n {
|
||||
row.insert(self.cursor_col, blank);
|
||||
}
|
||||
// Truncate n characters from the end
|
||||
row.truncate(self.cols - n);
|
||||
// Insert n blank characters at cursor position (single O(cols) operation)
|
||||
row.splice(self.cursor_col..self.cursor_col, std::iter::repeat(blank).take(n));
|
||||
self.mark_line_dirty(self.cursor_row);
|
||||
}
|
||||
|
||||
@@ -1179,16 +1253,11 @@ impl Terminal {
|
||||
let blank = self.blank_cell();
|
||||
let row = &mut self.grid[grid_row];
|
||||
let n = n.min(self.cols - self.cursor_col);
|
||||
// Remove n characters at cursor position
|
||||
for _ in 0..n {
|
||||
if self.cursor_col < row.len() {
|
||||
row.remove(self.cursor_col);
|
||||
}
|
||||
}
|
||||
let end = (self.cursor_col + n).min(row.len());
|
||||
// Remove n characters at cursor position (single O(cols) operation)
|
||||
row.drain(self.cursor_col..end);
|
||||
// Pad with blank characters at the end
|
||||
while row.len() < self.cols {
|
||||
row.push(blank);
|
||||
}
|
||||
row.resize(self.cols, blank);
|
||||
self.mark_line_dirty(self.cursor_row);
|
||||
}
|
||||
|
||||
@@ -1197,21 +1266,18 @@ impl Terminal {
|
||||
let grid_row = self.line_map[self.cursor_row];
|
||||
let n = n.min(self.cols - self.cursor_col);
|
||||
let blank = self.blank_cell();
|
||||
for i in 0..n {
|
||||
if self.cursor_col + i < self.cols {
|
||||
self.grid[grid_row][self.cursor_col + i] = blank;
|
||||
}
|
||||
}
|
||||
// Fill range with blanks (bounds already guaranteed by min above)
|
||||
self.grid[grid_row][self.cursor_col..self.cursor_col + n].fill(blank);
|
||||
self.mark_line_dirty(self.cursor_row);
|
||||
}
|
||||
|
||||
/// Clears the current line from cursor to end.
|
||||
#[inline]
|
||||
fn clear_line_from_cursor(&mut self) {
|
||||
let grid_row = self.line_map[self.cursor_row];
|
||||
let blank = self.blank_cell();
|
||||
for col in self.cursor_col..self.cols {
|
||||
self.grid[grid_row][col] = blank;
|
||||
}
|
||||
// Use slice fill for efficiency
|
||||
self.grid[grid_row][self.cursor_col..].fill(blank);
|
||||
self.mark_line_dirty(self.cursor_row);
|
||||
}
|
||||
|
||||
@@ -1243,9 +1309,12 @@ impl Terminal {
|
||||
}
|
||||
|
||||
impl Handler for Terminal {
|
||||
/// Handle a chunk of decoded text (Unicode codepoints).
|
||||
/// Handle a chunk of decoded text (Unicode codepoints as u32).
|
||||
/// This includes control characters (0x00-0x1F except ESC).
|
||||
fn text(&mut self, chars: &[char]) {
|
||||
fn text(&mut self, codepoints: &[u32]) {
|
||||
#[cfg(feature = "render_timing")]
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
// Cache the current line to avoid repeated line_map lookups
|
||||
let mut cached_row = self.cursor_row;
|
||||
let mut grid_row = self.line_map[cached_row];
|
||||
@@ -1253,25 +1322,27 @@ impl Handler for Terminal {
|
||||
// Mark the initial line as dirty (like Kitty's init_text_loop_line)
|
||||
self.mark_line_dirty(cached_row);
|
||||
|
||||
for &c in chars {
|
||||
match c {
|
||||
for &cp in codepoints {
|
||||
// Fast path for ASCII control characters and printable ASCII
|
||||
// These are the most common cases, so check them first using u32 directly
|
||||
match cp {
|
||||
// Bell
|
||||
'\x07' => {
|
||||
0x07 => {
|
||||
// BEL - ignore for now (could trigger visual bell)
|
||||
}
|
||||
// Backspace
|
||||
'\x08' => {
|
||||
0x08 => {
|
||||
if self.cursor_col > 0 {
|
||||
self.cursor_col -= 1;
|
||||
}
|
||||
}
|
||||
// Tab
|
||||
'\x09' => {
|
||||
0x09 => {
|
||||
let next_tab = (self.cursor_col / 8 + 1) * 8;
|
||||
self.cursor_col = next_tab.min(self.cols - 1);
|
||||
}
|
||||
// Line feed, Vertical tab, Form feed
|
||||
'\x0A' | '\x0B' | '\x0C' => {
|
||||
0x0A | 0x0B | 0x0C => {
|
||||
let old_row = self.cursor_row;
|
||||
self.cursor_row += 1;
|
||||
if self.cursor_row > self.scroll_bottom {
|
||||
@@ -1286,21 +1357,17 @@ impl Handler for Terminal {
|
||||
self.mark_line_dirty(cached_row);
|
||||
}
|
||||
// Carriage return
|
||||
'\x0D' => {
|
||||
0x0D => {
|
||||
self.cursor_col = 0;
|
||||
}
|
||||
// Fast path for printable ASCII (0x20-0x7E) - like Kitty
|
||||
// ASCII is always width 1, never zero-width, never wide
|
||||
c if c >= ' ' && c <= '~' => {
|
||||
cp if cp >= 0x20 && cp <= 0x7E => {
|
||||
// Handle wrap
|
||||
if self.cursor_col >= self.cols {
|
||||
if self.auto_wrap {
|
||||
self.cursor_col = 0;
|
||||
self.cursor_row += 1;
|
||||
if self.cursor_row > self.scroll_bottom {
|
||||
self.scroll_up(1);
|
||||
self.cursor_row = self.scroll_bottom;
|
||||
}
|
||||
self.advance_row();
|
||||
cached_row = self.cursor_row;
|
||||
grid_row = self.line_map[cached_row];
|
||||
self.mark_line_dirty(cached_row);
|
||||
@@ -1310,111 +1377,21 @@ impl Handler for Terminal {
|
||||
}
|
||||
|
||||
// Write character directly - no wide char handling needed for ASCII
|
||||
self.grid[grid_row][self.cursor_col] = Cell {
|
||||
character: c,
|
||||
fg_color: self.current_fg,
|
||||
bg_color: self.current_bg,
|
||||
bold: self.current_bold,
|
||||
italic: self.current_italic,
|
||||
underline_style: self.current_underline_style,
|
||||
strikethrough: self.current_strikethrough,
|
||||
wide_continuation: false,
|
||||
};
|
||||
// SAFETY: cp is in 0x20..=0x7E which are valid ASCII chars
|
||||
let c = unsafe { char::from_u32_unchecked(cp) };
|
||||
self.grid[grid_row][self.cursor_col] = self.make_cell(c, false);
|
||||
self.cursor_col += 1;
|
||||
}
|
||||
// Slow path for non-ASCII printable characters (including all Unicode)
|
||||
c if c > '~' => {
|
||||
// Determine character width using Unicode Standard Annex #11
|
||||
let char_width = c.width().unwrap_or(1);
|
||||
|
||||
// Skip zero-width characters (combining marks, etc.)
|
||||
if char_width == 0 {
|
||||
// TODO: Handle combining characters
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle wrap
|
||||
if self.cursor_col >= self.cols {
|
||||
if self.auto_wrap {
|
||||
self.cursor_col = 0;
|
||||
self.cursor_row += 1;
|
||||
if self.cursor_row > self.scroll_bottom {
|
||||
self.scroll_up(1);
|
||||
self.cursor_row = self.scroll_bottom;
|
||||
}
|
||||
// Update cache after line change
|
||||
cached_row = self.cursor_row;
|
||||
grid_row = self.line_map[cached_row];
|
||||
// Mark the new line as dirty
|
||||
self.mark_line_dirty(cached_row);
|
||||
} else {
|
||||
self.cursor_col = self.cols - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// For double-width characters at end of line, wrap first
|
||||
if char_width == 2 && self.cursor_col == self.cols - 1 {
|
||||
if self.auto_wrap {
|
||||
self.grid[grid_row][self.cursor_col] = Cell::default();
|
||||
self.cursor_col = 0;
|
||||
self.cursor_row += 1;
|
||||
if self.cursor_row > self.scroll_bottom {
|
||||
self.scroll_up(1);
|
||||
self.cursor_row = self.scroll_bottom;
|
||||
}
|
||||
cached_row = self.cursor_row;
|
||||
grid_row = self.line_map[cached_row];
|
||||
// Mark the new line as dirty
|
||||
self.mark_line_dirty(cached_row);
|
||||
} else {
|
||||
continue; // Can't fit
|
||||
}
|
||||
}
|
||||
|
||||
// Write character directly using cached grid_row
|
||||
// Safety: ensure grid row has correct width (may differ after scrollback swap)
|
||||
if self.grid[grid_row].len() != self.cols {
|
||||
self.grid[grid_row].resize(self.cols, Cell::default());
|
||||
}
|
||||
|
||||
// Handle overwriting wide character cells
|
||||
if self.grid[grid_row][self.cursor_col].wide_continuation && self.cursor_col > 0 {
|
||||
self.grid[grid_row][self.cursor_col - 1] = Cell::default();
|
||||
}
|
||||
if char_width == 1 && self.cursor_col + 1 < self.cols
|
||||
&& self.grid[grid_row][self.cursor_col + 1].wide_continuation {
|
||||
self.grid[grid_row][self.cursor_col + 1] = Cell::default();
|
||||
}
|
||||
|
||||
self.grid[grid_row][self.cursor_col] = Cell {
|
||||
character: c,
|
||||
fg_color: self.current_fg,
|
||||
bg_color: self.current_bg,
|
||||
bold: self.current_bold,
|
||||
italic: self.current_italic,
|
||||
underline_style: self.current_underline_style,
|
||||
strikethrough: self.current_strikethrough,
|
||||
wide_continuation: false,
|
||||
};
|
||||
self.cursor_col += 1;
|
||||
|
||||
// For double-width, write continuation cell
|
||||
if char_width == 2 && self.cursor_col < self.cols {
|
||||
if self.cursor_col + 1 < self.cols
|
||||
&& self.grid[grid_row][self.cursor_col + 1].wide_continuation {
|
||||
self.grid[grid_row][self.cursor_col + 1] = Cell::default();
|
||||
}
|
||||
self.grid[grid_row][self.cursor_col] = Cell {
|
||||
character: c,
|
||||
fg_color: self.current_fg,
|
||||
bg_color: self.current_bg,
|
||||
bold: self.current_bold,
|
||||
italic: self.current_italic,
|
||||
underline_style: self.current_underline_style,
|
||||
strikethrough: self.current_strikethrough,
|
||||
wide_continuation: false,
|
||||
};
|
||||
self.cursor_col += 1;
|
||||
// Delegates to print_char() which handles wide characters, wrapping, etc.
|
||||
cp if cp > 0x7E => {
|
||||
// Convert to char, using replacement character for invalid codepoints
|
||||
let c = char::from_u32(cp).unwrap_or('\u{FFFD}');
|
||||
self.print_char(c);
|
||||
// Update cached values since print_char may have scrolled or wrapped
|
||||
if cached_row != self.cursor_row {
|
||||
cached_row = self.cursor_row;
|
||||
grid_row = self.line_map[cached_row];
|
||||
}
|
||||
}
|
||||
// Other control chars - ignore
|
||||
@@ -1422,6 +1399,12 @@ impl Handler for Terminal {
|
||||
}
|
||||
}
|
||||
// Dirty lines are marked incrementally above - no need for mark_all_lines_dirty()
|
||||
|
||||
#[cfg(feature = "render_timing")]
|
||||
{
|
||||
self.stats.text_handler_ns += start.elapsed().as_nanos() as u64;
|
||||
self.stats.chars_processed += codepoints.len() as u32;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle control characters embedded in escape sequences.
|
||||
@@ -1437,11 +1420,7 @@ impl Handler for Terminal {
|
||||
self.cursor_col = next_tab.min(self.cols - 1);
|
||||
}
|
||||
0x0A | 0x0B | 0x0C => {
|
||||
self.cursor_row += 1;
|
||||
if self.cursor_row > self.scroll_bottom {
|
||||
self.scroll_up(1);
|
||||
self.cursor_row = self.scroll_bottom;
|
||||
}
|
||||
self.advance_row();
|
||||
}
|
||||
0x0D => {
|
||||
self.cursor_col = 0;
|
||||
@@ -1617,7 +1596,11 @@ impl Handler for Terminal {
|
||||
}
|
||||
|
||||
/// Handle a complete CSI sequence.
|
||||
#[inline]
|
||||
fn csi(&mut self, params: &CsiParams) {
|
||||
#[cfg(feature = "render_timing")]
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let action = params.final_char as char;
|
||||
let primary = params.primary;
|
||||
let secondary = params.secondary;
|
||||
@@ -1766,13 +1749,40 @@ impl Handler for Terminal {
|
||||
self.insert_characters(n);
|
||||
}
|
||||
// Repeat preceding character (REP)
|
||||
// Optimized like Kitty: batch writes for ASCII, avoid per-char overhead
|
||||
'b' => {
|
||||
let n = params.get(0, 1).max(1) as usize;
|
||||
if self.cursor_col > 0 {
|
||||
let n = (params.get(0, 1).max(1) as usize).min(65535); // Like Kitty's CSI_REP_MAX_REPETITIONS
|
||||
if self.cursor_col > 0 && n > 0 {
|
||||
let grid_row = self.line_map[self.cursor_row];
|
||||
let last_char = self.grid[grid_row][self.cursor_col - 1].character;
|
||||
for _ in 0..n {
|
||||
self.print_char(last_char);
|
||||
let last_cp = last_char as u32;
|
||||
|
||||
// Fast path for ASCII: direct grid write, no width lookup
|
||||
if last_cp >= 0x20 && last_cp <= 0x7E {
|
||||
let cell = self.make_cell(last_char, false);
|
||||
self.mark_line_dirty(self.cursor_row);
|
||||
|
||||
for _ in 0..n {
|
||||
// Handle wrap
|
||||
if self.cursor_col >= self.cols {
|
||||
if self.auto_wrap {
|
||||
self.cursor_col = 0;
|
||||
self.advance_row();
|
||||
self.mark_line_dirty(self.cursor_row);
|
||||
} else {
|
||||
self.cursor_col = self.cols - 1;
|
||||
}
|
||||
}
|
||||
// Direct write - recompute grid_row in case of scroll
|
||||
let gr = self.line_map[self.cursor_row];
|
||||
self.grid[gr][self.cursor_col] = cell.clone();
|
||||
self.cursor_col += 1;
|
||||
}
|
||||
} else {
|
||||
// Slow path for non-ASCII: use print_char for proper width handling
|
||||
for _ in 0..n {
|
||||
self.print_char(last_char);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1893,6 +1903,12 @@ impl Handler for Terminal {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "render_timing")]
|
||||
{
|
||||
self.stats.csi_handler_ns += start.elapsed().as_nanos() as u64;
|
||||
self.stats.csi_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn save_cursor(&mut self) {
|
||||
@@ -2012,6 +2028,15 @@ impl Handler for Terminal {
|
||||
self.mark_line_dirty(visual_row);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "render_timing")]
|
||||
fn add_vt_parser_ns(&mut self, ns: u64) {
|
||||
self.stats.vt_parser_ns += ns;
|
||||
self.stats.consume_input_count += 1;
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "render_timing"))]
|
||||
fn add_vt_parser_ns(&mut self, _ns: u64) {}
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
@@ -2035,11 +2060,7 @@ impl Terminal {
|
||||
if self.cursor_col >= self.cols {
|
||||
if self.auto_wrap {
|
||||
self.cursor_col = 0;
|
||||
self.cursor_row += 1;
|
||||
if self.cursor_row > self.scroll_bottom {
|
||||
self.scroll_up(1);
|
||||
self.cursor_row = self.scroll_bottom;
|
||||
}
|
||||
self.advance_row();
|
||||
} else {
|
||||
self.cursor_col = self.cols - 1;
|
||||
}
|
||||
@@ -2053,11 +2074,7 @@ impl Terminal {
|
||||
let grid_row = self.line_map[self.cursor_row];
|
||||
self.grid[grid_row][self.cursor_col] = Cell::default();
|
||||
self.cursor_col = 0;
|
||||
self.cursor_row += 1;
|
||||
if self.cursor_row > self.scroll_bottom {
|
||||
self.scroll_up(1);
|
||||
self.cursor_row = self.scroll_bottom;
|
||||
}
|
||||
self.advance_row();
|
||||
} else {
|
||||
// Can't fit, don't print
|
||||
return;
|
||||
@@ -2080,16 +2097,7 @@ impl Terminal {
|
||||
}
|
||||
|
||||
// Write the character to the first cell
|
||||
self.grid[grid_row][self.cursor_col] = Cell {
|
||||
character: c,
|
||||
fg_color: self.current_fg,
|
||||
bg_color: self.current_bg,
|
||||
bold: self.current_bold,
|
||||
italic: self.current_italic,
|
||||
underline_style: self.current_underline_style,
|
||||
strikethrough: self.current_strikethrough,
|
||||
wide_continuation: false,
|
||||
};
|
||||
self.grid[grid_row][self.cursor_col] = self.make_cell(c, false);
|
||||
self.mark_line_dirty(self.cursor_row);
|
||||
self.cursor_col += 1;
|
||||
|
||||
@@ -2102,53 +2110,79 @@ impl Terminal {
|
||||
self.grid[grid_row][self.cursor_col + 1] = Cell::default();
|
||||
}
|
||||
|
||||
self.grid[grid_row][self.cursor_col] = Cell {
|
||||
character: ' ', // Placeholder - renderer will skip this
|
||||
fg_color: self.current_fg,
|
||||
bg_color: self.current_bg,
|
||||
bold: self.current_bold,
|
||||
italic: self.current_italic,
|
||||
underline_style: self.current_underline_style,
|
||||
strikethrough: self.current_strikethrough,
|
||||
wide_continuation: true,
|
||||
};
|
||||
self.grid[grid_row][self.cursor_col] = self.make_cell(' ', true);
|
||||
self.cursor_col += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse extended color (SGR 38/48) and return the color and number of params consumed.
|
||||
/// Returns (Color, params_consumed) or None if parsing failed.
|
||||
///
|
||||
/// SAFETY: Caller must ensure i < params.num_params
|
||||
#[inline(always)]
|
||||
fn parse_extended_color(params: &CsiParams, i: usize) -> Option<(Color, usize)> {
|
||||
let num = params.num_params;
|
||||
let p = ¶ms.params;
|
||||
let is_sub = ¶ms.is_sub_param;
|
||||
|
||||
// Check for sub-parameter format (38:2:r:g:b or 38:5:idx)
|
||||
if i + 1 < num && is_sub[i + 1] {
|
||||
let mode = p[i + 1];
|
||||
if mode == 5 && i + 2 < num {
|
||||
return Some((Color::Indexed(p[i + 2] as u8), 2));
|
||||
} else if mode == 2 && i + 4 < num {
|
||||
return Some((Color::Rgb(
|
||||
p[i + 2] as u8,
|
||||
p[i + 3] as u8,
|
||||
p[i + 4] as u8,
|
||||
), 4));
|
||||
}
|
||||
} else if i + 2 < num {
|
||||
// Regular format (38;2;r;g;b or 38;5;idx)
|
||||
let mode = p[i + 1];
|
||||
if mode == 5 {
|
||||
return Some((Color::Indexed(p[i + 2] as u8), 2));
|
||||
} else if mode == 2 && i + 4 < num {
|
||||
return Some((Color::Rgb(
|
||||
p[i + 2] as u8,
|
||||
p[i + 3] as u8,
|
||||
p[i + 4] as u8,
|
||||
), 4));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Handle SGR (Select Graphic Rendition) parameters.
|
||||
/// This is a hot path - called for every color/style change in terminal output.
|
||||
#[inline(always)]
|
||||
fn handle_sgr(&mut self, params: &CsiParams) {
|
||||
if params.num_params == 0 {
|
||||
self.current_fg = Color::Default;
|
||||
self.current_bg = Color::Default;
|
||||
self.current_bold = false;
|
||||
self.current_italic = false;
|
||||
self.current_underline_style = 0;
|
||||
self.current_strikethrough = false;
|
||||
let num = params.num_params;
|
||||
|
||||
// Fast path: SGR 0 (reset) with no params or explicit 0
|
||||
if num == 0 {
|
||||
self.reset_sgr_attributes();
|
||||
return;
|
||||
}
|
||||
|
||||
let p = ¶ms.params;
|
||||
let is_sub = ¶ms.is_sub_param;
|
||||
let mut i = 0;
|
||||
while i < params.num_params {
|
||||
let code = params.params[i];
|
||||
|
||||
while i < num {
|
||||
// SAFETY: i < num <= MAX_CSI_PARAMS, so index is always valid
|
||||
let code = p[i];
|
||||
|
||||
match code {
|
||||
0 => {
|
||||
self.current_fg = Color::Default;
|
||||
self.current_bg = Color::Default;
|
||||
self.current_bold = false;
|
||||
self.current_italic = false;
|
||||
self.current_underline_style = 0;
|
||||
self.current_strikethrough = false;
|
||||
}
|
||||
0 => self.reset_sgr_attributes(),
|
||||
1 => self.current_bold = true,
|
||||
// 2 => dim (not currently rendered)
|
||||
3 => self.current_italic = true,
|
||||
4 => {
|
||||
// Check for sub-parameter (4:x format for underline style)
|
||||
if i + 1 < params.num_params && params.is_sub_param[i + 1] {
|
||||
let style = params.params[i + 1];
|
||||
if i + 1 < num && is_sub[i + 1] {
|
||||
// 0=none, 1=single, 2=double, 3=curly, 4=dotted, 5=dashed
|
||||
self.current_underline_style = (style as u8).min(5);
|
||||
self.current_underline_style = (p[i + 1] as u8).min(5);
|
||||
i += 1;
|
||||
} else {
|
||||
// Plain SGR 4 = single underline
|
||||
@@ -2157,84 +2191,55 @@ impl Terminal {
|
||||
}
|
||||
7 => std::mem::swap(&mut self.current_fg, &mut self.current_bg),
|
||||
9 => self.current_strikethrough = true,
|
||||
21 => self.current_underline_style = 2, // Double underline
|
||||
22 => self.current_bold = false,
|
||||
23 => self.current_italic = false,
|
||||
24 => self.current_underline_style = 0,
|
||||
27 => std::mem::swap(&mut self.current_fg, &mut self.current_bg),
|
||||
29 => self.current_strikethrough = false,
|
||||
// Standard foreground colors (30-37)
|
||||
30..=37 => self.current_fg = Color::Indexed((code - 30) as u8),
|
||||
38 => {
|
||||
// Extended foreground color
|
||||
if i + 1 < params.num_params && params.is_sub_param[i + 1] {
|
||||
let mode = params.params[i + 1];
|
||||
if mode == 5 && i + 2 < params.num_params {
|
||||
self.current_fg = Color::Indexed(params.params[i + 2] as u8);
|
||||
i += 2;
|
||||
} else if mode == 2 && i + 4 < params.num_params {
|
||||
self.current_fg = Color::Rgb(
|
||||
params.params[i + 2] as u8,
|
||||
params.params[i + 3] as u8,
|
||||
params.params[i + 4] as u8,
|
||||
);
|
||||
i += 4;
|
||||
}
|
||||
} else if i + 2 < params.num_params {
|
||||
let mode = params.params[i + 1];
|
||||
if mode == 5 {
|
||||
self.current_fg = Color::Indexed(params.params[i + 2] as u8);
|
||||
i += 2;
|
||||
} else if mode == 2 && i + 4 < params.num_params {
|
||||
self.current_fg = Color::Rgb(
|
||||
params.params[i + 2] as u8,
|
||||
params.params[i + 3] as u8,
|
||||
params.params[i + 4] as u8,
|
||||
);
|
||||
i += 4;
|
||||
}
|
||||
if let Some((color, consumed)) = Self::parse_extended_color(params, i) {
|
||||
self.current_fg = color;
|
||||
i += consumed;
|
||||
}
|
||||
}
|
||||
39 => self.current_fg = Color::Default,
|
||||
// Standard background colors (40-47)
|
||||
40..=47 => self.current_bg = Color::Indexed((code - 40) as u8),
|
||||
48 => {
|
||||
// Extended background color
|
||||
if i + 1 < params.num_params && params.is_sub_param[i + 1] {
|
||||
let mode = params.params[i + 1];
|
||||
if mode == 5 && i + 2 < params.num_params {
|
||||
self.current_bg = Color::Indexed(params.params[i + 2] as u8);
|
||||
i += 2;
|
||||
} else if mode == 2 && i + 4 < params.num_params {
|
||||
self.current_bg = Color::Rgb(
|
||||
params.params[i + 2] as u8,
|
||||
params.params[i + 3] as u8,
|
||||
params.params[i + 4] as u8,
|
||||
);
|
||||
i += 4;
|
||||
}
|
||||
} else if i + 2 < params.num_params {
|
||||
let mode = params.params[i + 1];
|
||||
if mode == 5 {
|
||||
self.current_bg = Color::Indexed(params.params[i + 2] as u8);
|
||||
i += 2;
|
||||
} else if mode == 2 && i + 4 < params.num_params {
|
||||
self.current_bg = Color::Rgb(
|
||||
params.params[i + 2] as u8,
|
||||
params.params[i + 3] as u8,
|
||||
params.params[i + 4] as u8,
|
||||
);
|
||||
i += 4;
|
||||
}
|
||||
if let Some((color, consumed)) = Self::parse_extended_color(params, i) {
|
||||
self.current_bg = color;
|
||||
i += consumed;
|
||||
}
|
||||
}
|
||||
49 => self.current_bg = Color::Default,
|
||||
// Bright foreground colors (90-97)
|
||||
90..=97 => self.current_fg = Color::Indexed((code - 90 + 8) as u8),
|
||||
// Bright background colors (100-107)
|
||||
100..=107 => self.current_bg = Color::Indexed((code - 100 + 8) as u8),
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset all SGR attributes to defaults.
|
||||
#[inline(always)]
|
||||
fn reset_sgr_attributes(&mut self) {
|
||||
self.current_fg = Color::Default;
|
||||
self.current_bg = Color::Default;
|
||||
self.current_bold = false;
|
||||
self.current_italic = false;
|
||||
self.current_underline_style = 0;
|
||||
self.current_strikethrough = false;
|
||||
}
|
||||
|
||||
/// Handle Kitty keyboard protocol CSI sequences.
|
||||
#[inline]
|
||||
fn handle_keyboard_protocol_csi(&mut self, params: &CsiParams) {
|
||||
match params.primary {
|
||||
b'?' => {
|
||||
@@ -2266,6 +2271,7 @@ impl Terminal {
|
||||
}
|
||||
|
||||
/// Handle DEC private mode set (CSI ? Ps h).
|
||||
#[inline]
|
||||
fn handle_dec_private_mode_set(&mut self, params: &CsiParams) {
|
||||
for i in 0..params.num_params {
|
||||
match params.params[i] {
|
||||
@@ -2334,6 +2340,7 @@ impl Terminal {
|
||||
}
|
||||
|
||||
/// Handle DEC private mode reset (CSI ? Ps l).
|
||||
#[inline]
|
||||
fn handle_dec_private_mode_reset(&mut self, params: &CsiParams) {
|
||||
for i in 0..params.num_params {
|
||||
match params.params[i] {
|
||||
|
||||
+951
-224
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user