font rendering
This commit is contained in:
+14
-1
@@ -36,7 +36,7 @@ libc = "0.2"
|
|||||||
bitflags = "2"
|
bitflags = "2"
|
||||||
|
|
||||||
# Font rasterization and shaping
|
# Font rasterization and shaping
|
||||||
fontdue = "0.9"
|
ab_glyph = "0.2"
|
||||||
rustybuzz = "0.20"
|
rustybuzz = "0.20"
|
||||||
ttf-parser = "0.25"
|
ttf-parser = "0.25"
|
||||||
fontconfig = "0.10"
|
fontconfig = "0.10"
|
||||||
@@ -47,9 +47,22 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
bincode = "1"
|
bincode = "1"
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
|
notify = "7"
|
||||||
|
|
||||||
# Shared memory for fast IPC
|
# Shared memory for fast IPC
|
||||||
memmap2 = "0.9"
|
memmap2 = "0.9"
|
||||||
|
|
||||||
# Fast byte searching
|
# Fast byte searching
|
||||||
memchr = "2"
|
memchr = "2"
|
||||||
|
|
||||||
|
# Image processing (Kitty graphics protocol)
|
||||||
|
image = { version = "0.25", default-features = false, features = ["png", "gif"] }
|
||||||
|
flate2 = "1"
|
||||||
|
|
||||||
|
# Video decoding for WebM support (video only, no audio)
|
||||||
|
# Requires system FFmpeg libraries (ffmpeg 5.x - 8.x supported)
|
||||||
|
ffmpeg-next = { version = "8.0", optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["webm"]
|
||||||
|
webm = ["ffmpeg-next"]
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
//! Test ligature detection - compare individual vs combined shaping
|
|
||||||
use rustybuzz::{Face, UnicodeBuffer, Feature};
|
|
||||||
use ttf_parser::Tag;
|
|
||||||
use fontdue::Font;
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let path = "/usr/share/fonts/TTF/0xProtoNerdFontMono-Regular.ttf";
|
|
||||||
println!("Using font: {}", path);
|
|
||||||
|
|
||||||
let font_data = fs::read(path).expect("Failed to read font");
|
|
||||||
let face = Face::from_slice(&font_data, 0).expect("Failed to parse font");
|
|
||||||
let fontdue_font = Font::from_bytes(&font_data[..], fontdue::FontSettings::default()).unwrap();
|
|
||||||
|
|
||||||
let font_size = 16.0;
|
|
||||||
let units_per_em = face.units_per_em() as f32;
|
|
||||||
|
|
||||||
println!("Font units per em: {}", units_per_em);
|
|
||||||
|
|
||||||
// Get cell width from a regular character
|
|
||||||
let (hyphen_metrics, _) = fontdue_font.rasterize('-', font_size);
|
|
||||||
let cell_width = hyphen_metrics.advance_width;
|
|
||||||
println!("Cell width (from '-'): {:.2}px", cell_width);
|
|
||||||
|
|
||||||
let features = vec![
|
|
||||||
Feature::new(Tag::from_bytes(b"liga"), 1, ..),
|
|
||||||
Feature::new(Tag::from_bytes(b"calt"), 1, ..),
|
|
||||||
Feature::new(Tag::from_bytes(b"dlig"), 1, ..),
|
|
||||||
];
|
|
||||||
|
|
||||||
let test_strings = ["->", "=>", "==", "!=", ">=", "<="];
|
|
||||||
|
|
||||||
for s in &test_strings {
|
|
||||||
// Shape combined string
|
|
||||||
let mut buffer = UnicodeBuffer::new();
|
|
||||||
buffer.push_str(s);
|
|
||||||
let combined = rustybuzz::shape(&face, &features, buffer);
|
|
||||||
let combined_infos = combined.glyph_infos();
|
|
||||||
let combined_positions = combined.glyph_positions();
|
|
||||||
|
|
||||||
// Shape each character individually
|
|
||||||
let mut individual_glyphs = Vec::new();
|
|
||||||
for c in s.chars() {
|
|
||||||
let mut buf = UnicodeBuffer::new();
|
|
||||||
buf.push_str(&c.to_string());
|
|
||||||
let shaped = rustybuzz::shape(&face, &features, buf);
|
|
||||||
individual_glyphs.push(shaped.glyph_infos()[0].glyph_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("\n'{}' analysis:", s);
|
|
||||||
println!(" Combined glyphs: {:?}", combined_infos.iter().map(|i| i.glyph_id).collect::<Vec<_>>());
|
|
||||||
println!(" Individual glyphs: {:?}", individual_glyphs);
|
|
||||||
|
|
||||||
// Show advances for each glyph
|
|
||||||
for (i, (info, pos)) in combined_infos.iter().zip(combined_positions.iter()).enumerate() {
|
|
||||||
let advance_px = pos.x_advance as f32 * font_size / units_per_em;
|
|
||||||
println!(" Glyph {}: id={}, advance={} units ({:.2}px)", i, info.glyph_id, pos.x_advance, advance_px);
|
|
||||||
|
|
||||||
// Rasterize and show metrics
|
|
||||||
let (metrics, _) = fontdue_font.rasterize_indexed(info.glyph_id as u16, font_size);
|
|
||||||
println!(" Rasterized: {}x{} px, xmin={}, ymin={}, advance_width={:.2}",
|
|
||||||
metrics.width, metrics.height, metrics.xmin, metrics.ymin, metrics.advance_width);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if any glyph was substituted
|
|
||||||
let has_substitution = combined_infos.iter().zip(individual_glyphs.iter())
|
|
||||||
.any(|(combined, &individual)| combined.glyph_id != individual);
|
|
||||||
println!(" Has substitution: {}", has_substitution);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also test what Kitty does - check glyph names
|
|
||||||
println!("\n=== Checking glyph names via ttf-parser ===");
|
|
||||||
let ttf_face = ttf_parser::Face::parse(&font_data, 0).unwrap();
|
|
||||||
|
|
||||||
// Shape "->" and get glyph names
|
|
||||||
let mut buffer = UnicodeBuffer::new();
|
|
||||||
buffer.push_str("->");
|
|
||||||
let combined = rustybuzz::shape(&face, &features, buffer);
|
|
||||||
for info in combined.glyph_infos() {
|
|
||||||
let glyph_id = ttf_parser::GlyphId(info.glyph_id as u16);
|
|
||||||
if let Some(name) = ttf_face.glyph_name(glyph_id) {
|
|
||||||
println!(" Glyph {} name: {}", info.glyph_id, name);
|
|
||||||
} else {
|
|
||||||
println!(" Glyph {} has no name", info.glyph_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -367,6 +367,9 @@ pub struct Config {
|
|||||||
pub inactive_pane_fade_ms: u64,
|
pub inactive_pane_fade_ms: u64,
|
||||||
/// Dim factor for inactive panes (0.0 = fully dimmed/black, 1.0 = no dimming).
|
/// Dim factor for inactive panes (0.0 = fully dimmed/black, 1.0 = no dimming).
|
||||||
pub inactive_pane_dim: f32,
|
pub inactive_pane_dim: f32,
|
||||||
|
/// Intensity of the edge glow effect when pane navigation fails (0.0 = disabled, 1.0 = full intensity).
|
||||||
|
/// The edge glow provides visual feedback when you try to navigate to a pane that doesn't exist.
|
||||||
|
pub edge_glow_intensity: f32,
|
||||||
/// Process names that should receive pane navigation keys instead of zterm handling them.
|
/// Process names that should receive pane navigation keys instead of zterm handling them.
|
||||||
/// When the foreground process matches one of these names, Alt+Arrow keys are passed
|
/// When the foreground process matches one of these names, Alt+Arrow keys are passed
|
||||||
/// to the application (e.g., for Neovim buffer navigation) instead of switching panes.
|
/// to the application (e.g., for Neovim buffer navigation) instead of switching panes.
|
||||||
@@ -385,6 +388,7 @@ impl Default for Config {
|
|||||||
scrollback_lines: 50_000,
|
scrollback_lines: 50_000,
|
||||||
inactive_pane_fade_ms: 150,
|
inactive_pane_fade_ms: 150,
|
||||||
inactive_pane_dim: 0.6,
|
inactive_pane_dim: 0.6,
|
||||||
|
edge_glow_intensity: 1.0,
|
||||||
pass_keys_to_programs: vec!["nvim".to_string(), "vim".to_string()],
|
pass_keys_to_programs: vec!["nvim".to_string(), "vim".to_string()],
|
||||||
keybindings: Keybindings::default(),
|
keybindings: Keybindings::default(),
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-11
@@ -1,5 +1,63 @@
|
|||||||
// Glyph rendering shader for terminal emulator
|
// Glyph rendering shader for terminal emulator
|
||||||
// Supports both legacy quad-based rendering and new instanced cell rendering
|
// Supports both legacy quad-based rendering and new instanced cell rendering
|
||||||
|
// Uses Kitty-style "legacy" gamma-incorrect text blending for crisp rendering
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// GAMMA CONVERSION FUNCTIONS (for legacy text rendering)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// Luminance weights for perceived brightness (ITU-R BT.709)
|
||||||
|
const Y: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);
|
||||||
|
|
||||||
|
// Convert linear RGB to sRGB
|
||||||
|
fn linear2srgb(x: f32) -> f32 {
|
||||||
|
if x <= 0.0031308 {
|
||||||
|
return 12.92 * x;
|
||||||
|
} else {
|
||||||
|
return 1.055 * pow(x, 1.0 / 2.4) - 0.055;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert sRGB to linear RGB
|
||||||
|
fn srgb2linear(x: f32) -> f32 {
|
||||||
|
if x <= 0.04045 {
|
||||||
|
return x / 12.92;
|
||||||
|
} else {
|
||||||
|
return pow((x + 0.055) / 1.055, 2.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kitty's legacy gamma-incorrect text blending
|
||||||
|
// This simulates how text was blended before gamma-correct rendering became standard.
|
||||||
|
// It makes dark text on light backgrounds appear thicker and light text on dark
|
||||||
|
// backgrounds appear thinner, which many users prefer for readability.
|
||||||
|
//
|
||||||
|
// The input colors are in sRGB space. We convert to linear for the luminance
|
||||||
|
// calculation, then simulate gamma-incorrect blending.
|
||||||
|
fn foreground_contrast_legacy(over_srgb: vec3<f32>, over_alpha: f32, under_srgb: vec3<f32>) -> f32 {
|
||||||
|
// Convert sRGB colors to linear for luminance calculation
|
||||||
|
let over_linear = vec3<f32>(srgb2linear(over_srgb.r), srgb2linear(over_srgb.g), srgb2linear(over_srgb.b));
|
||||||
|
let under_linear = vec3<f32>(srgb2linear(under_srgb.r), srgb2linear(under_srgb.g), srgb2linear(under_srgb.b));
|
||||||
|
|
||||||
|
let under_luminance = dot(under_linear, Y);
|
||||||
|
let over_luminance = dot(over_linear, Y);
|
||||||
|
|
||||||
|
// Avoid division by zero when luminances are equal
|
||||||
|
let luminance_diff = over_luminance - under_luminance;
|
||||||
|
if abs(luminance_diff) < 0.001 {
|
||||||
|
return over_alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kitty's formula: simulate gamma-incorrect blending
|
||||||
|
// This is the solution to:
|
||||||
|
// linear2srgb(over * alpha2 + under * (1 - alpha2)) = linear2srgb(over) * alpha + linear2srgb(under) * (1 - alpha)
|
||||||
|
// ^ gamma correct blending with new alpha ^ gamma incorrect blending with old alpha
|
||||||
|
let blended_srgb = linear2srgb(over_luminance) * over_alpha + linear2srgb(under_luminance) * (1.0 - over_alpha);
|
||||||
|
let blended_linear = srgb2linear(blended_srgb);
|
||||||
|
let new_alpha = (blended_linear - under_luminance) / luminance_diff;
|
||||||
|
|
||||||
|
return clamp(new_alpha, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// LEGACY QUAD-BASED RENDERING (for backwards compatibility)
|
// LEGACY QUAD-BASED RENDERING (for backwards compatibility)
|
||||||
@@ -47,9 +105,11 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
|||||||
// Sample the glyph alpha from the atlas
|
// Sample the glyph alpha from the atlas
|
||||||
let glyph_alpha = textureSample(atlas_texture, atlas_sampler, in.uv).r;
|
let glyph_alpha = textureSample(atlas_texture, atlas_sampler, in.uv).r;
|
||||||
|
|
||||||
// Output foreground color with glyph alpha for blending
|
// Apply legacy gamma-incorrect blending for crisp text
|
||||||
// The background was already rendered, so we just blend the glyph on top
|
let adjusted_alpha = foreground_contrast_legacy(in.color.rgb, glyph_alpha, in.bg_color.rgb);
|
||||||
return vec4<f32>(in.color.rgb, in.color.a * glyph_alpha);
|
|
||||||
|
// Output foreground color with adjusted alpha for blending
|
||||||
|
return vec4<f32>(in.color.rgb, in.color.a * adjusted_alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -232,9 +292,7 @@ fn vs_cell_bg(
|
|||||||
bg = tmp;
|
bg = tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to linear for sRGB surface
|
// Keep colors in sRGB space for legacy blending
|
||||||
fg = vec4<f32>(srgb_to_linear(fg.r), srgb_to_linear(fg.g), srgb_to_linear(fg.b), fg.a);
|
|
||||||
bg = vec4<f32>(srgb_to_linear(bg.r), srgb_to_linear(bg.g), srgb_to_linear(bg.b), bg.a);
|
|
||||||
|
|
||||||
var out: CellVertexOutput;
|
var out: CellVertexOutput;
|
||||||
out.clip_position = vec4<f32>(ndc_pos, 0.0, 1.0);
|
out.clip_position = vec4<f32>(ndc_pos, 0.0, 1.0);
|
||||||
@@ -324,14 +382,13 @@ fn vs_cell_glyph(
|
|||||||
bg = tmp;
|
bg = tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to linear
|
// Keep colors in sRGB space for legacy blending (conversion happens in fragment shader)
|
||||||
fg = vec4<f32>(srgb_to_linear(fg.r), srgb_to_linear(fg.g), srgb_to_linear(fg.b), fg.a);
|
|
||||||
|
|
||||||
var out: CellVertexOutput;
|
var out: CellVertexOutput;
|
||||||
out.clip_position = vec4<f32>(ndc_pos, 0.0, 1.0);
|
out.clip_position = vec4<f32>(ndc_pos, 0.0, 1.0);
|
||||||
out.uv = uvs[vertex_index];
|
out.uv = uvs[vertex_index];
|
||||||
out.fg_color = fg;
|
out.fg_color = fg;
|
||||||
out.bg_color = vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
out.bg_color = bg; // Pass background for legacy gamma blending
|
||||||
out.is_background = 0u;
|
out.is_background = 0u;
|
||||||
out.is_colored_glyph = select(0u, 1u, is_colored);
|
out.is_colored_glyph = select(0u, 1u, is_colored);
|
||||||
|
|
||||||
@@ -356,6 +413,9 @@ fn fs_cell(in: CellVertexOutput) -> @location(0) vec4<f32> {
|
|||||||
return vec4<f32>(in.fg_color.rgb, glyph_alpha);
|
return vec4<f32>(in.fg_color.rgb, glyph_alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal glyph - tint with foreground color
|
// Apply legacy gamma-incorrect blending for crisp text
|
||||||
return vec4<f32>(in.fg_color.rgb, in.fg_color.a * glyph_alpha);
|
let adjusted_alpha = foreground_contrast_legacy(in.fg_color.rgb, glyph_alpha, in.bg_color.rgb);
|
||||||
|
|
||||||
|
// Normal glyph - tint with foreground color using adjusted alpha
|
||||||
|
return vec4<f32>(in.fg_color.rgb, in.fg_color.a * adjusted_alpha);
|
||||||
}
|
}
|
||||||
|
|||||||
+1826
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
|||||||
|
// Image rendering shader for Kitty graphics protocol
|
||||||
|
// Renders RGBA images with proper alpha blending
|
||||||
|
|
||||||
|
struct ImageUniforms {
|
||||||
|
// Screen dimensions in pixels
|
||||||
|
screen_width: f32,
|
||||||
|
screen_height: f32,
|
||||||
|
// Image position in pixels (top-left corner)
|
||||||
|
pos_x: f32,
|
||||||
|
pos_y: f32,
|
||||||
|
// Image display size in pixels
|
||||||
|
display_width: f32,
|
||||||
|
display_height: f32,
|
||||||
|
// Source rectangle in normalized coordinates (0-1)
|
||||||
|
src_x: f32,
|
||||||
|
src_y: f32,
|
||||||
|
src_width: f32,
|
||||||
|
src_height: f32,
|
||||||
|
// Padding for alignment
|
||||||
|
_padding1: f32,
|
||||||
|
_padding2: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> uniforms: ImageUniforms;
|
||||||
|
|
||||||
|
@group(0) @binding(1)
|
||||||
|
var image_texture: texture_2d<f32>;
|
||||||
|
|
||||||
|
@group(0) @binding(2)
|
||||||
|
var image_sampler: sampler;
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) uv: vec2<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert pixel coordinate to NDC
|
||||||
|
fn pixel_to_ndc(pixel: vec2<f32>, screen: vec2<f32>) -> vec2<f32> {
|
||||||
|
return vec2<f32>(
|
||||||
|
(pixel.x / screen.x) * 2.0 - 1.0,
|
||||||
|
1.0 - (pixel.y / screen.y) * 2.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
|
||||||
|
// Calculate quad corners in pixel space
|
||||||
|
let x0 = uniforms.pos_x;
|
||||||
|
let y0 = uniforms.pos_y;
|
||||||
|
let x1 = uniforms.pos_x + uniforms.display_width;
|
||||||
|
let y1 = uniforms.pos_y + uniforms.display_height;
|
||||||
|
|
||||||
|
// Quad vertex positions (0=top-left, 1=top-right, 2=bottom-left, 3=bottom-right)
|
||||||
|
// Using triangle strip order
|
||||||
|
var positions: array<vec2<f32>, 4>;
|
||||||
|
positions[0] = vec2<f32>(x0, y0);
|
||||||
|
positions[1] = vec2<f32>(x1, y0);
|
||||||
|
positions[2] = vec2<f32>(x0, y1);
|
||||||
|
positions[3] = vec2<f32>(x1, y1);
|
||||||
|
|
||||||
|
// UV coordinates mapping to source rectangle
|
||||||
|
var uvs: array<vec2<f32>, 4>;
|
||||||
|
let u0 = uniforms.src_x;
|
||||||
|
let v0 = uniforms.src_y;
|
||||||
|
let u1 = uniforms.src_x + uniforms.src_width;
|
||||||
|
let v1 = uniforms.src_y + uniforms.src_height;
|
||||||
|
|
||||||
|
uvs[0] = vec2<f32>(u0, v0);
|
||||||
|
uvs[1] = vec2<f32>(u1, v0);
|
||||||
|
uvs[2] = vec2<f32>(u0, v1);
|
||||||
|
uvs[3] = vec2<f32>(u1, v1);
|
||||||
|
|
||||||
|
let screen_size = vec2<f32>(uniforms.screen_width, uniforms.screen_height);
|
||||||
|
let ndc_pos = pixel_to_ndc(positions[vertex_index], screen_size);
|
||||||
|
|
||||||
|
out.clip_position = vec4<f32>(ndc_pos, 0.0, 1.0);
|
||||||
|
out.uv = uvs[vertex_index];
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
// Sample the image texture
|
||||||
|
let color = textureSample(image_texture, image_sampler, in.uv);
|
||||||
|
|
||||||
|
// Return with premultiplied alpha for proper blending
|
||||||
|
return vec4<f32>(color.rgb * color.a, color.a);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
//! Single-process architecture: one process owns PTY, terminal state, and rendering.
|
//! Single-process architecture: one process owns PTY, terminal state, and rendering.
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod graphics;
|
||||||
pub mod keyboard;
|
pub mod keyboard;
|
||||||
pub mod pty;
|
pub mod pty;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
|
|||||||
+186
-22
@@ -17,6 +17,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
use polling::{Event, Events, Poller};
|
use polling::{Event, Events, Poller};
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
use winit::dpi::{PhysicalPosition, PhysicalSize};
|
use winit::dpi::{PhysicalPosition, PhysicalSize};
|
||||||
@@ -54,9 +55,18 @@ struct DoubleBuffer {
|
|||||||
|
|
||||||
impl SharedPtyBuffer {
|
impl SharedPtyBuffer {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
|
// Use with_capacity to avoid zeroing memory - we only need the allocation
|
||||||
|
let mut buf1 = Vec::with_capacity(PTY_BUF_SIZE);
|
||||||
|
let mut buf2 = Vec::with_capacity(PTY_BUF_SIZE);
|
||||||
|
// SAFETY: We're setting length to capacity. The data is uninitialized but
|
||||||
|
// we only read from portions that have been written to (tracked by write_len).
|
||||||
|
unsafe {
|
||||||
|
buf1.set_len(PTY_BUF_SIZE);
|
||||||
|
buf2.set_len(PTY_BUF_SIZE);
|
||||||
|
}
|
||||||
Self {
|
Self {
|
||||||
inner: Mutex::new(DoubleBuffer {
|
inner: Mutex::new(DoubleBuffer {
|
||||||
bufs: [vec![0u8; PTY_BUF_SIZE], vec![0u8; PTY_BUF_SIZE]],
|
bufs: [buf1, buf2],
|
||||||
write_idx: 0,
|
write_idx: 0,
|
||||||
write_len: 0,
|
write_len: 0,
|
||||||
}),
|
}),
|
||||||
@@ -158,8 +168,15 @@ impl Pane {
|
|||||||
let terminal = Terminal::new(cols, rows, scrollback_lines);
|
let terminal = Terminal::new(cols, rows, scrollback_lines);
|
||||||
let pty = Pty::spawn(None).map_err(|e| format!("Failed to spawn PTY: {}", e))?;
|
let pty = Pty::spawn(None).map_err(|e| format!("Failed to spawn PTY: {}", e))?;
|
||||||
|
|
||||||
// Set terminal size
|
// Set terminal size (use default cell size estimate for initial pixel dimensions)
|
||||||
if let Err(e) = pty.resize(cols as u16, rows as u16) {
|
let default_cell_width = 10u16;
|
||||||
|
let default_cell_height = 20u16;
|
||||||
|
if let Err(e) = pty.resize(
|
||||||
|
cols as u16,
|
||||||
|
rows as u16,
|
||||||
|
cols as u16 * default_cell_width,
|
||||||
|
rows as u16 * default_cell_height,
|
||||||
|
) {
|
||||||
log::warn!("Failed to set initial PTY size: {}", e);
|
log::warn!("Failed to set initial PTY size: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,9 +197,9 @@ impl Pane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resize the terminal and PTY.
|
/// Resize the terminal and PTY.
|
||||||
fn resize(&mut self, cols: usize, rows: usize) {
|
fn resize(&mut self, cols: usize, rows: usize, width_px: u16, height_px: u16) {
|
||||||
self.terminal.resize(cols, rows);
|
self.terminal.resize(cols, rows);
|
||||||
if let Err(e) = self.pty.resize(cols as u16, rows as u16) {
|
if let Err(e) = self.pty.resize(cols as u16, rows as u16, width_px, height_px) {
|
||||||
log::warn!("Failed to resize PTY: {}", e);
|
log::warn!("Failed to resize PTY: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -588,7 +605,11 @@ impl Tab {
|
|||||||
|
|
||||||
for (pane_id, geom) in geometries {
|
for (pane_id, geom) in geometries {
|
||||||
if let Some(pane) = self.panes.get_mut(&pane_id) {
|
if let Some(pane) = self.panes.get_mut(&pane_id) {
|
||||||
pane.resize(geom.cols, geom.rows);
|
// Report pixel dimensions as exact cell grid size (cols * cell_width, rows * cell_height)
|
||||||
|
// This ensures applications like kitten icat calculate image placement correctly
|
||||||
|
let pixel_width = (geom.cols as f32 * cell_width) as u16;
|
||||||
|
let pixel_height = (geom.rows as f32 * cell_height) as u16;
|
||||||
|
pane.resize(geom.cols, geom.rows, pixel_width, pixel_height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -784,6 +805,8 @@ enum UserEvent {
|
|||||||
ShowWindow,
|
ShowWindow,
|
||||||
/// PTY has data available for a specific pane.
|
/// PTY has data available for a specific pane.
|
||||||
PtyReadable(PaneId),
|
PtyReadable(PaneId),
|
||||||
|
/// Config file was modified and should be reloaded.
|
||||||
|
ConfigReloaded,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Main application state.
|
/// Main application state.
|
||||||
@@ -816,8 +839,8 @@ struct App {
|
|||||||
last_frame_log: std::time::Instant,
|
last_frame_log: std::time::Instant,
|
||||||
/// Whether window should be created on next opportunity.
|
/// Whether window should be created on next opportunity.
|
||||||
should_create_window: bool,
|
should_create_window: bool,
|
||||||
/// Edge glow animation state (for when navigation fails).
|
/// Edge glow animations (for when navigation fails). Multiple can be active simultaneously.
|
||||||
edge_glow: Option<EdgeGlow>,
|
edge_glows: Vec<EdgeGlow>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const PTY_KEY: usize = 1;
|
const PTY_KEY: usize = 1;
|
||||||
@@ -848,7 +871,7 @@ impl App {
|
|||||||
frame_count: 0,
|
frame_count: 0,
|
||||||
last_frame_log: std::time::Instant::now(),
|
last_frame_log: std::time::Instant::now(),
|
||||||
should_create_window: false,
|
should_create_window: false,
|
||||||
edge_glow: None,
|
edge_glows: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -856,6 +879,50 @@ impl App {
|
|||||||
self.event_loop_proxy = Some(proxy);
|
self.event_loop_proxy = Some(proxy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reload configuration from disk and apply changes.
|
||||||
|
fn reload_config(&mut self) {
|
||||||
|
log::info!("Reloading configuration...");
|
||||||
|
let new_config = Config::load();
|
||||||
|
|
||||||
|
// Check what changed and apply updates
|
||||||
|
let font_size_changed = (new_config.font_size - self.config.font_size).abs() > 0.01;
|
||||||
|
let opacity_changed = (new_config.background_opacity - self.config.background_opacity).abs() > 0.01;
|
||||||
|
let tab_bar_changed = new_config.tab_bar_position != self.config.tab_bar_position;
|
||||||
|
|
||||||
|
// Update the config
|
||||||
|
self.config = new_config;
|
||||||
|
|
||||||
|
// Rebuild action map for keybindings
|
||||||
|
self.action_map = self.config.keybindings.build_action_map();
|
||||||
|
|
||||||
|
// Apply renderer changes if we have a renderer
|
||||||
|
if let Some(renderer) = &mut self.renderer {
|
||||||
|
if opacity_changed {
|
||||||
|
renderer.set_background_opacity(self.config.background_opacity);
|
||||||
|
log::info!("Updated background opacity to {}", self.config.background_opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if tab_bar_changed {
|
||||||
|
renderer.set_tab_bar_position(self.config.tab_bar_position);
|
||||||
|
log::info!("Updated tab bar position to {:?}", self.config.tab_bar_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if font_size_changed {
|
||||||
|
renderer.set_font_size(self.config.font_size);
|
||||||
|
log::info!("Updated font size to {}", self.config.font_size);
|
||||||
|
// Font size change requires resize to recalculate cell dimensions
|
||||||
|
self.resize_all_panes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request redraw to apply visual changes
|
||||||
|
if let Some(window) = &self.window {
|
||||||
|
window.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Configuration reloaded successfully");
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new tab and start its I/O thread.
|
/// Create a new tab and start its I/O thread.
|
||||||
/// Returns the index of the new tab.
|
/// Returns the index of the new tab.
|
||||||
fn create_tab(&mut self, cols: usize, rows: usize) -> Option<usize> {
|
fn create_tab(&mut self, cols: usize, rows: usize) -> Option<usize> {
|
||||||
@@ -1046,6 +1113,11 @@ impl App {
|
|||||||
|
|
||||||
for tab in &mut self.tabs {
|
for tab in &mut self.tabs {
|
||||||
tab.resize(width, height, cell_width, cell_height, border_width);
|
tab.resize(width, height, cell_width, cell_height, border_width);
|
||||||
|
|
||||||
|
// Update cell size on all terminals (needed for Kitty graphics protocol)
|
||||||
|
for pane in tab.panes.values_mut() {
|
||||||
|
pane.terminal.set_cell_size(cell_width, cell_height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1368,6 +1440,13 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn focus_pane(&mut self, direction: Direction) {
|
fn focus_pane(&mut self, direction: Direction) {
|
||||||
|
// Get current active pane geometry before attempting navigation
|
||||||
|
let active_pane_geom = if let Some(tab) = self.tabs.get(self.active_tab) {
|
||||||
|
tab.split_root.find_geometry(tab.active_pane)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let navigated = if let Some(tab) = self.tabs.get_mut(self.active_tab) {
|
let navigated = if let Some(tab) = self.tabs.get_mut(self.active_tab) {
|
||||||
let old_pane = tab.active_pane;
|
let old_pane = tab.active_pane;
|
||||||
tab.focus_neighbor(direction);
|
tab.focus_neighbor(direction);
|
||||||
@@ -1378,7 +1457,16 @@ impl App {
|
|||||||
|
|
||||||
if !navigated {
|
if !navigated {
|
||||||
// No neighbor in that direction - trigger edge glow animation
|
// No neighbor in that direction - trigger edge glow animation
|
||||||
self.edge_glow = Some(EdgeGlow::new(direction));
|
// Add to existing glows (don't replace) so multiple can be visible
|
||||||
|
if let Some(geom) = active_pane_geom {
|
||||||
|
self.edge_glows.push(EdgeGlow::new(
|
||||||
|
direction,
|
||||||
|
geom.x,
|
||||||
|
geom.y,
|
||||||
|
geom.width,
|
||||||
|
geom.height,
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(window) = &self.window {
|
if let Some(window) = &self.window {
|
||||||
@@ -1605,9 +1693,11 @@ impl App {
|
|||||||
|
|
||||||
impl ApplicationHandler<UserEvent> for App {
|
impl ApplicationHandler<UserEvent> for App {
|
||||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
if self.window.is_none() {
|
if self.window.is_none() {
|
||||||
self.create_window(event_loop);
|
self.create_window(event_loop);
|
||||||
}
|
}
|
||||||
|
log::info!("App resumed (window creation): {:?}", start.elapsed());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
|
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
|
||||||
@@ -1633,6 +1723,9 @@ impl ApplicationHandler<UserEvent> for App {
|
|||||||
log::info!("PTY process took {:?}", process_time);
|
log::info!("PTY process took {:?}", process_time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
UserEvent::ConfigReloaded => {
|
||||||
|
self.reload_config();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1851,13 +1944,16 @@ impl ApplicationHandler<UserEvent> for App {
|
|||||||
let geometries = tab.collect_pane_geometries();
|
let geometries = tab.collect_pane_geometries();
|
||||||
let active_pane_id = tab.active_pane;
|
let active_pane_id = tab.active_pane;
|
||||||
|
|
||||||
// First pass: calculate dim factors (needs mutable access)
|
// First pass: sync images and calculate dim factors (needs mutable access)
|
||||||
let mut dim_factors: Vec<(PaneId, f32)> = Vec::new();
|
let mut dim_factors: Vec<(PaneId, f32)> = Vec::new();
|
||||||
for (pane_id, _) in &geometries {
|
for (pane_id, _) in &geometries {
|
||||||
if let Some(pane) = tab.panes.get_mut(pane_id) {
|
if let Some(pane) = tab.panes.get_mut(pane_id) {
|
||||||
let is_active = *pane_id == active_pane_id;
|
let is_active = *pane_id == active_pane_id;
|
||||||
let dim_factor = pane.calculate_dim_factor(is_active, fade_duration_ms, inactive_dim);
|
let dim_factor = pane.calculate_dim_factor(is_active, fade_duration_ms, inactive_dim);
|
||||||
dim_factors.push((*pane_id, dim_factor));
|
dim_factors.push((*pane_id, dim_factor));
|
||||||
|
|
||||||
|
// Sync terminal images to GPU (Kitty graphics protocol)
|
||||||
|
renderer.sync_images(&mut pane.terminal.image_storage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1914,11 +2010,15 @@ impl ApplicationHandler<UserEvent> for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle edge glow animation
|
// Handle edge glow animations
|
||||||
let edge_glow_ref = self.edge_glow.as_ref();
|
let glow_in_progress = !self.edge_glows.is_empty();
|
||||||
let glow_in_progress = edge_glow_ref.map(|g| !g.is_finished()).unwrap_or(false);
|
|
||||||
|
|
||||||
match renderer.render_panes(&pane_render_data, num_tabs, active_tab_idx, edge_glow_ref) {
|
// Check if any pane has animated images
|
||||||
|
let image_animation_in_progress = tab.panes.values().any(|pane| {
|
||||||
|
pane.terminal.image_storage.has_animations()
|
||||||
|
});
|
||||||
|
|
||||||
|
match renderer.render_panes(&pane_render_data, num_tabs, active_tab_idx, &self.edge_glows, self.config.edge_glow_intensity) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(wgpu::SurfaceError::Lost) => {
|
Err(wgpu::SurfaceError::Lost) => {
|
||||||
renderer.resize(renderer.width, renderer.height);
|
renderer.resize(renderer.width, renderer.height);
|
||||||
@@ -1932,8 +2032,8 @@ impl ApplicationHandler<UserEvent> for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request redraw if edge glow is animating
|
// Request redraw if edge glow or image animation is in progress
|
||||||
if glow_in_progress {
|
if glow_in_progress || image_animation_in_progress {
|
||||||
if let Some(window) = &self.window {
|
if let Some(window) = &self.window {
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -1941,10 +2041,8 @@ impl ApplicationHandler<UserEvent> for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up finished edge glow animation
|
// Clean up finished edge glow animations
|
||||||
if self.edge_glow.as_ref().map(|g| g.is_finished()).unwrap_or(false) {
|
self.edge_glows.retain(|g| !g.is_finished());
|
||||||
self.edge_glow = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let render_time = render_start.elapsed();
|
let render_time = render_start.elapsed();
|
||||||
let frame_time = frame_start.elapsed();
|
let frame_time = frame_start.elapsed();
|
||||||
@@ -2000,6 +2098,69 @@ impl Drop for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set up a file watcher to monitor the config file for changes.
|
||||||
|
/// Returns the watcher (must be kept alive for watching to continue).
|
||||||
|
fn setup_config_watcher(proxy: EventLoopProxy<UserEvent>) -> Option<RecommendedWatcher> {
|
||||||
|
let config_path = match Config::config_path() {
|
||||||
|
Some(path) => path,
|
||||||
|
None => {
|
||||||
|
log::warn!("Could not determine config path, config hot-reload disabled");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch the parent directory since the file might be replaced atomically
|
||||||
|
let watch_path = match config_path.parent() {
|
||||||
|
Some(parent) => parent.to_path_buf(),
|
||||||
|
None => {
|
||||||
|
log::warn!("Could not determine config directory, config hot-reload disabled");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let config_filename = config_path.file_name().map(|s| s.to_os_string());
|
||||||
|
|
||||||
|
let mut watcher = match notify::recommended_watcher(move |res: Result<notify::Event, notify::Error>| {
|
||||||
|
match res {
|
||||||
|
Ok(event) => {
|
||||||
|
// Only trigger on modify/create events for the config file
|
||||||
|
use notify::EventKind;
|
||||||
|
match event.kind {
|
||||||
|
EventKind::Modify(_) | EventKind::Create(_) => {
|
||||||
|
// Check if the event is for our config file
|
||||||
|
let is_config_file = event.paths.iter().any(|p| {
|
||||||
|
p.file_name().map(|s| s.to_os_string()) == config_filename
|
||||||
|
});
|
||||||
|
|
||||||
|
if is_config_file {
|
||||||
|
log::debug!("Config file changed, triggering reload");
|
||||||
|
let _ = proxy.send_event(UserEvent::ConfigReloaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Config watcher error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Ok(w) => w,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to create config watcher: {:?}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = watcher.watch(&watch_path, RecursiveMode::NonRecursive) {
|
||||||
|
log::warn!("Failed to watch config directory {:?}: {:?}", watch_path, e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Config hot-reload enabled, watching {:?}", watch_path);
|
||||||
|
Some(watcher)
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||||
|
|
||||||
@@ -2035,9 +2196,12 @@ fn main() {
|
|||||||
|
|
||||||
// Store proxy for signal handler (uses the global static defined below)
|
// Store proxy for signal handler (uses the global static defined below)
|
||||||
unsafe {
|
unsafe {
|
||||||
EVENT_PROXY = Some(proxy);
|
EVENT_PROXY = Some(proxy.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up config file watcher for hot-reloading
|
||||||
|
let _config_watcher = setup_config_watcher(proxy);
|
||||||
|
|
||||||
event_loop.run_app(&mut app).expect("Event loop error");
|
event_loop.run_app(&mut app).expect("Event loop error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -150,12 +150,12 @@ impl Pty {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Resizes the PTY window.
|
/// Resizes the PTY window.
|
||||||
pub fn resize(&self, cols: u16, rows: u16) -> Result<(), PtyError> {
|
pub fn resize(&self, cols: u16, rows: u16, xpixel: u16, ypixel: u16) -> Result<(), PtyError> {
|
||||||
let winsize = libc::winsize {
|
let winsize = libc::winsize {
|
||||||
ws_row: rows,
|
ws_row: rows,
|
||||||
ws_col: cols,
|
ws_col: cols,
|
||||||
ws_xpixel: 0,
|
ws_xpixel: xpixel,
|
||||||
ws_ypixel: 0,
|
ws_ypixel: ypixel,
|
||||||
};
|
};
|
||||||
|
|
||||||
let fd = std::os::fd::AsRawFd::as_raw_fd(&self.master);
|
let fd = std::os::fd::AsRawFd::as_raw_fd(&self.master);
|
||||||
|
|||||||
+1256
-356
File diff suppressed because it is too large
Load Diff
+186
-87
@@ -1,28 +1,49 @@
|
|||||||
// Edge Glow Shader
|
// Edge Glow Shader
|
||||||
// Renders a soft glow effect at terminal edges for failed pane navigation feedback.
|
// Renders natural-looking light effects at terminal edges for failed pane navigation feedback.
|
||||||
// The glow appears as a light node at center that splits into two and travels to corners.
|
// Supports multiple simultaneous lights that blend together.
|
||||||
|
// Features: bright hot center, colored mid-range, soft outer halo with bloom.
|
||||||
|
|
||||||
// Uniform buffer with glow parameters
|
// Maximum number of simultaneous glows
|
||||||
|
const MAX_GLOWS: u32 = 16u;
|
||||||
|
|
||||||
|
// Per-glow parameters (48 bytes each, aligned to 16 bytes)
|
||||||
|
struct GlowInstance {
|
||||||
|
// Direction: 0=Up, 1=Down, 2=Left, 3=Right
|
||||||
|
direction: u32,
|
||||||
|
// Animation progress (0.0 to 1.0)
|
||||||
|
progress: f32,
|
||||||
|
// Glow color (linear RGB)
|
||||||
|
color_r: f32,
|
||||||
|
color_g: f32,
|
||||||
|
color_b: f32,
|
||||||
|
// Pane bounds in pixels
|
||||||
|
pane_x: f32,
|
||||||
|
pane_y: f32,
|
||||||
|
pane_width: f32,
|
||||||
|
pane_height: f32,
|
||||||
|
// Padding to align to 16 bytes
|
||||||
|
_padding1: f32,
|
||||||
|
_padding2: f32,
|
||||||
|
_padding3: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global parameters + array of glow instances
|
||||||
struct EdgeGlowParams {
|
struct EdgeGlowParams {
|
||||||
// Screen dimensions in pixels
|
// Screen dimensions in pixels
|
||||||
screen_width: f32,
|
screen_width: f32,
|
||||||
screen_height: f32,
|
screen_height: f32,
|
||||||
// Terminal area offset (for tab bar)
|
// Terminal area offset (for tab bar)
|
||||||
terminal_y_offset: f32,
|
terminal_y_offset: f32,
|
||||||
// Direction: 0=Up, 1=Down, 2=Left, 3=Right
|
// Glow intensity multiplier (0.0 = disabled, 1.0 = full)
|
||||||
direction: u32,
|
glow_intensity: f32,
|
||||||
// Animation progress (0.0 to 1.0)
|
// Number of active glows
|
||||||
progress: f32,
|
glow_count: u32,
|
||||||
// Glow color (linear RGB) - stored as separate floats to avoid vec3 alignment issues
|
// Padding to align to 16 bytes before array
|
||||||
color_r: f32,
|
|
||||||
color_g: f32,
|
|
||||||
color_b: f32,
|
|
||||||
// Whether glow is enabled (1 = yes, 0 = no)
|
|
||||||
enabled: u32,
|
|
||||||
// Padding to align to 16 bytes
|
|
||||||
_padding1: u32,
|
_padding1: u32,
|
||||||
_padding2: u32,
|
_padding2: u32,
|
||||||
_padding3: u32,
|
_padding3: u32,
|
||||||
|
// Array of glow instances
|
||||||
|
glows: array<GlowInstance, 16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@group(0) @binding(0)
|
@group(0) @binding(0)
|
||||||
@@ -34,16 +55,10 @@ struct VertexOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fullscreen triangle vertex shader
|
// Fullscreen triangle vertex shader
|
||||||
// Uses vertex_index 0,1,2 to create a triangle that covers the screen
|
|
||||||
@vertex
|
@vertex
|
||||||
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
|
|
||||||
// Generate fullscreen triangle vertices
|
|
||||||
// This creates a triangle that covers [-1,1] in clip space
|
|
||||||
let x = f32(i32(vertex_index) - 1);
|
|
||||||
let y = f32(i32(vertex_index & 1u) * 2 - 1);
|
|
||||||
|
|
||||||
// Positions for a fullscreen triangle
|
// Positions for a fullscreen triangle
|
||||||
var pos: vec2<f32>;
|
var pos: vec2<f32>;
|
||||||
switch vertex_index {
|
switch vertex_index {
|
||||||
@@ -54,7 +69,6 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
out.clip_position = vec4<f32>(pos, 0.0, 1.0);
|
out.clip_position = vec4<f32>(pos, 0.0, 1.0);
|
||||||
// Convert to 0-1 UV (flip Y since clip space Y is up, pixel Y is down)
|
|
||||||
out.uv = vec2<f32>((pos.x + 1.0) * 0.5, (1.0 - pos.y) * 0.5);
|
out.uv = vec2<f32>((pos.x + 1.0) * 0.5, (1.0 - pos.y) * 0.5);
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
@@ -63,19 +77,8 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
|
|||||||
// Constants
|
// Constants
|
||||||
const PI: f32 = 3.14159265359;
|
const PI: f32 = 3.14159265359;
|
||||||
const PHASE1_END: f32 = 0.15; // Phase 1 ends at 15% progress
|
const PHASE1_END: f32 = 0.15; // Phase 1 ends at 15% progress
|
||||||
const GLOW_RADIUS: f32 = 90.0; // Base radius of glow
|
const GLOW_RADIUS: f32 = 80.0; // Core radius of the light
|
||||||
const GLOW_ASPECT: f32 = 2.0; // Stretch factor along edge (ellipse)
|
const GLOW_ASPECT: f32 = 2.5; // Stretch factor along edge (ellipse)
|
||||||
|
|
||||||
// Smooth gaussian-like falloff
|
|
||||||
fn glow_falloff(dist: f32, radius: f32) -> f32 {
|
|
||||||
let normalized = dist / radius;
|
|
||||||
if normalized > 1.0 {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
// Smooth falloff: (1 - x^2)^3 gives nice soft edges
|
|
||||||
let t = 1.0 - normalized * normalized;
|
|
||||||
return t * t * t;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ease-out cubic
|
// Ease-out cubic
|
||||||
fn ease_out_cubic(t: f32) -> f32 {
|
fn ease_out_cubic(t: f32) -> f32 {
|
||||||
@@ -83,8 +86,9 @@ fn ease_out_cubic(t: f32) -> f32 {
|
|||||||
return 1.0 - t1 * t1 * t1;
|
return 1.0 - t1 * t1 * t1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate distance from point to glow center, accounting for ellipse shape
|
// Calculate normalized distance from point to glow center (elliptical)
|
||||||
fn ellipse_distance(point: vec2<f32>, center: vec2<f32>, radius_along: f32, radius_perp: f32, is_horizontal: bool) -> f32 {
|
// Returns 0 at center, 1 at edge of core, >1 outside
|
||||||
|
fn ellipse_dist_normalized(point: vec2<f32>, center: vec2<f32>, radius_along: f32, radius_perp: f32, is_horizontal: bool) -> f32 {
|
||||||
let delta = point - center;
|
let delta = point - center;
|
||||||
var normalized: vec2<f32>;
|
var normalized: vec2<f32>;
|
||||||
if is_horizontal {
|
if is_horizontal {
|
||||||
@@ -92,45 +96,76 @@ fn ellipse_distance(point: vec2<f32>, center: vec2<f32>, radius_along: f32, radi
|
|||||||
} else {
|
} else {
|
||||||
normalized = vec2<f32>(delta.x / radius_perp, delta.y / radius_along);
|
normalized = vec2<f32>(delta.x / radius_perp, delta.y / radius_along);
|
||||||
}
|
}
|
||||||
return length(normalized) * min(radius_along, radius_perp);
|
return length(normalized);
|
||||||
}
|
}
|
||||||
|
|
||||||
@fragment
|
// Natural light intensity falloff
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
// Creates a bright core with soft extended halo
|
||||||
// Early out if not enabled
|
fn light_intensity(dist: f32) -> f32 {
|
||||||
if params.enabled == 0u {
|
// Multi-layer falloff for natural light appearance:
|
||||||
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
// 1. Bright core (inverse square-ish, clamped)
|
||||||
|
// 2. Soft halo that extends further
|
||||||
|
|
||||||
|
if dist < 0.001 {
|
||||||
|
return 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let progress = params.progress;
|
// Core intensity - bright center that falls off quickly
|
||||||
|
// Using smoothed inverse for the hot center
|
||||||
|
let core = 1.0 / (1.0 + dist * dist * 4.0);
|
||||||
|
|
||||||
// Convert UV to pixel coordinates
|
// Soft halo - gaussian-like falloff that extends further
|
||||||
let pixel = vec2<f32>(
|
let halo = exp(-dist * dist * 1.5);
|
||||||
in.uv.x * params.screen_width,
|
|
||||||
in.uv.y * params.screen_height
|
|
||||||
);
|
|
||||||
|
|
||||||
let terminal_height = params.screen_height - params.terminal_y_offset;
|
// Combine: core dominates near center, halo extends the glow
|
||||||
let is_horizontal = params.direction == 0u || params.direction == 1u;
|
return core * 0.7 + halo * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the "hotness" - how white/bright the center should be
|
||||||
|
// Returns 0-1 where 1 = pure white (hottest), 0 = base color
|
||||||
|
fn light_hotness(dist: f32) -> f32 {
|
||||||
|
// Very bright white core that quickly transitions to color
|
||||||
|
let hot = 1.0 / (1.0 + dist * dist * 12.0);
|
||||||
|
return hot * hot; // Square it for sharper transition
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate contribution from a single glow at the given pixel
|
||||||
|
// Returns (intensity, hotness, 1.0) packed in vec3
|
||||||
|
fn calculate_glow(pixel: vec2<f32>, glow: GlowInstance) -> vec3<f32> {
|
||||||
|
// Get pane bounds from the glow instance
|
||||||
|
let pane_x = glow.pane_x;
|
||||||
|
let pane_y = glow.pane_y;
|
||||||
|
let pane_width = glow.pane_width;
|
||||||
|
let pane_height = glow.pane_height;
|
||||||
|
|
||||||
|
// Mask: if pixel is outside pane bounds, return zero contribution
|
||||||
|
if pixel.x < pane_x || pixel.x > pane_x + pane_width ||
|
||||||
|
pixel.y < pane_y || pixel.y > pane_y + pane_height {
|
||||||
|
return vec3<f32>(0.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let progress = glow.progress;
|
||||||
|
let is_horizontal = glow.direction == 0u || glow.direction == 1u;
|
||||||
|
|
||||||
// Calculate glow parameters based on animation phase
|
// Calculate glow parameters based on animation phase
|
||||||
var alpha: f32;
|
var intensity_mult: f32;
|
||||||
var size_factor: f32;
|
var size_factor: f32;
|
||||||
var split: f32;
|
var split: f32;
|
||||||
|
|
||||||
if progress < PHASE1_END {
|
if progress < PHASE1_END {
|
||||||
// Phase 1: Fade in, grow
|
// Phase 1: Appear and grow
|
||||||
let t = progress / PHASE1_END;
|
let t = progress / PHASE1_END;
|
||||||
let ease = ease_out_cubic(t);
|
let ease = ease_out_cubic(t);
|
||||||
alpha = ease * 0.8;
|
intensity_mult = ease;
|
||||||
size_factor = 0.3 + 0.7 * ease;
|
size_factor = 0.4 + 0.6 * ease;
|
||||||
split = 0.0;
|
split = 0.0;
|
||||||
} else {
|
} else {
|
||||||
// Phase 2: Split and fade out
|
// Phase 2: Split and fade out
|
||||||
let t = (progress - PHASE1_END) / (1.0 - PHASE1_END);
|
let t = (progress - PHASE1_END) / (1.0 - PHASE1_END);
|
||||||
let fade = 1.0 - t;
|
let fade = 1.0 - t;
|
||||||
alpha = fade * fade * 0.8;
|
// Slower fade for more visible effect
|
||||||
size_factor = 1.0 - 0.3 * t;
|
intensity_mult = fade * fade * fade;
|
||||||
|
size_factor = 1.0 - 0.2 * t;
|
||||||
split = ease_out_cubic(t);
|
split = ease_out_cubic(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,29 +174,30 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
|||||||
let radius_perp = base_radius;
|
let radius_perp = base_radius;
|
||||||
|
|
||||||
// Calculate edge center and travel distance based on direction
|
// Calculate edge center and travel distance based on direction
|
||||||
|
// Now using pane bounds instead of screen bounds
|
||||||
var edge_center: vec2<f32>;
|
var edge_center: vec2<f32>;
|
||||||
var travel: vec2<f32>;
|
var travel: vec2<f32>;
|
||||||
|
|
||||||
switch params.direction {
|
switch glow.direction {
|
||||||
// Up - top edge
|
// Up - top edge of pane
|
||||||
case 0u: {
|
case 0u: {
|
||||||
edge_center = vec2<f32>(params.screen_width / 2.0, params.terminal_y_offset);
|
edge_center = vec2<f32>(pane_x + pane_width / 2.0, pane_y);
|
||||||
travel = vec2<f32>(params.screen_width / 2.0, 0.0);
|
travel = vec2<f32>(pane_width / 2.0, 0.0);
|
||||||
}
|
}
|
||||||
// Down - bottom edge
|
// Down - bottom edge of pane
|
||||||
case 1u: {
|
case 1u: {
|
||||||
edge_center = vec2<f32>(params.screen_width / 2.0, params.screen_height);
|
edge_center = vec2<f32>(pane_x + pane_width / 2.0, pane_y + pane_height);
|
||||||
travel = vec2<f32>(params.screen_width / 2.0, 0.0);
|
travel = vec2<f32>(pane_width / 2.0, 0.0);
|
||||||
}
|
}
|
||||||
// Left - left edge
|
// Left - left edge of pane
|
||||||
case 2u: {
|
case 2u: {
|
||||||
edge_center = vec2<f32>(0.0, params.terminal_y_offset + terminal_height / 2.0);
|
edge_center = vec2<f32>(pane_x, pane_y + pane_height / 2.0);
|
||||||
travel = vec2<f32>(0.0, terminal_height / 2.0);
|
travel = vec2<f32>(0.0, pane_height / 2.0);
|
||||||
}
|
}
|
||||||
// Right - right edge
|
// Right - right edge of pane
|
||||||
case 3u: {
|
case 3u: {
|
||||||
edge_center = vec2<f32>(params.screen_width, params.terminal_y_offset + terminal_height / 2.0);
|
edge_center = vec2<f32>(pane_x + pane_width, pane_y + pane_height / 2.0);
|
||||||
travel = vec2<f32>(0.0, terminal_height / 2.0);
|
travel = vec2<f32>(0.0, pane_height / 2.0);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
edge_center = vec2<f32>(0.0, 0.0);
|
edge_center = vec2<f32>(0.0, 0.0);
|
||||||
@@ -169,34 +205,97 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var glow_intensity: f32 = 0.0;
|
// Accumulate light from one or two sources
|
||||||
|
var total_intensity: f32 = 0.0;
|
||||||
|
var total_hotness: f32 = 0.0;
|
||||||
|
|
||||||
if split < 0.01 {
|
if split < 0.01 {
|
||||||
// Single glow at center
|
// Single light at center
|
||||||
let dist = ellipse_distance(pixel, edge_center, radius_along, radius_perp, is_horizontal);
|
let dist = ellipse_dist_normalized(pixel, edge_center, radius_along, radius_perp, is_horizontal);
|
||||||
glow_intensity = glow_falloff(dist, base_radius);
|
total_intensity = light_intensity(dist);
|
||||||
|
total_hotness = light_hotness(dist);
|
||||||
} else {
|
} else {
|
||||||
// Two glows splitting apart
|
// Two lights splitting apart
|
||||||
let split_radius = base_radius * (1.0 - 0.2 * split);
|
let split_factor = 1.0 - 0.15 * split;
|
||||||
let split_radius_along = radius_along * (1.0 - 0.2 * split);
|
let r_along = radius_along * split_factor;
|
||||||
let split_radius_perp = radius_perp * (1.0 - 0.2 * split);
|
let r_perp = radius_perp * split_factor;
|
||||||
|
|
||||||
let center1 = edge_center - travel * split;
|
let center1 = edge_center - travel * split;
|
||||||
let center2 = edge_center + travel * split;
|
let center2 = edge_center + travel * split;
|
||||||
|
|
||||||
let dist1 = ellipse_distance(pixel, center1, split_radius_along, split_radius_perp, is_horizontal);
|
let dist1 = ellipse_dist_normalized(pixel, center1, r_along, r_perp, is_horizontal);
|
||||||
let dist2 = ellipse_distance(pixel, center2, split_radius_along, split_radius_perp, is_horizontal);
|
let dist2 = ellipse_dist_normalized(pixel, center2, r_along, r_perp, is_horizontal);
|
||||||
|
|
||||||
// Combine both glows (additive but capped)
|
let intensity1 = light_intensity(dist1);
|
||||||
let glow1 = glow_falloff(dist1, split_radius);
|
let intensity2 = light_intensity(dist2);
|
||||||
let glow2 = glow_falloff(dist2, split_radius);
|
let hotness1 = light_hotness(dist1);
|
||||||
glow_intensity = min(glow1 + glow2, 1.0);
|
let hotness2 = light_hotness(dist2);
|
||||||
|
|
||||||
|
// Additive blending for overlapping lights (capped)
|
||||||
|
total_intensity = min(intensity1 + intensity2, 1.5);
|
||||||
|
total_hotness = max(hotness1, hotness2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply alpha
|
// Apply animation intensity multiplier
|
||||||
let final_alpha = glow_intensity * alpha;
|
total_intensity *= intensity_mult;
|
||||||
|
total_hotness *= intensity_mult;
|
||||||
|
|
||||||
|
// Return intensity, hotness, and a flag that this glow contributed
|
||||||
|
return vec3<f32>(total_intensity, total_hotness, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
// Early out if no glows
|
||||||
|
if params.glow_count == 0u {
|
||||||
|
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert UV to pixel coordinates
|
||||||
|
let pixel = vec2<f32>(
|
||||||
|
in.uv.x * params.screen_width,
|
||||||
|
in.uv.y * params.screen_height
|
||||||
|
);
|
||||||
|
|
||||||
|
// Accumulate contributions from all active glows
|
||||||
|
var total_intensity: f32 = 0.0;
|
||||||
|
var total_hotness: f32 = 0.0;
|
||||||
|
var accum_color = vec3<f32>(0.0, 0.0, 0.0);
|
||||||
|
var color_weight: f32 = 0.0;
|
||||||
|
|
||||||
|
for (var i: u32 = 0u; i < params.glow_count && i < MAX_GLOWS; i++) {
|
||||||
|
let glow = params.glows[i];
|
||||||
|
let result = calculate_glow(pixel, glow);
|
||||||
|
let intensity = result.x;
|
||||||
|
let hotness = result.y;
|
||||||
|
|
||||||
|
// Accumulate intensity and hotness additively
|
||||||
|
total_intensity += intensity;
|
||||||
|
total_hotness = max(total_hotness, hotness);
|
||||||
|
|
||||||
|
// Weight color contribution by intensity
|
||||||
|
let base_color = vec3<f32>(glow.color_r, glow.color_g, glow.color_b);
|
||||||
|
accum_color += base_color * intensity;
|
||||||
|
color_weight += intensity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap intensity for overlapping glows
|
||||||
|
total_intensity = min(total_intensity, 1.5);
|
||||||
|
|
||||||
|
// Calculate final base color (weighted average)
|
||||||
|
var base_color = vec3<f32>(0.0, 0.0, 0.0);
|
||||||
|
if color_weight > 0.001 {
|
||||||
|
base_color = accum_color / color_weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mix between base color and white based on hotness
|
||||||
|
// Hot center = white, outer regions = base color
|
||||||
|
let white = vec3<f32>(1.0, 1.0, 1.0);
|
||||||
|
let final_color = mix(base_color, white, total_hotness * 0.8);
|
||||||
|
|
||||||
|
// Final alpha based on intensity, scaled by global glow_intensity setting
|
||||||
|
let final_alpha = clamp(total_intensity * 0.9 * params.glow_intensity, 0.0, 1.0);
|
||||||
|
|
||||||
// Output with premultiplied alpha for proper blending
|
// Output with premultiplied alpha for proper blending
|
||||||
let color = vec3<f32>(params.color_r, params.color_g, params.color_b);
|
return vec4<f32>(final_color * final_alpha, final_alpha);
|
||||||
return vec4<f32>(color * final_alpha, final_alpha);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+126
-16
@@ -1,5 +1,6 @@
|
|||||||
//! Terminal state management and escape sequence handling.
|
//! Terminal state management and escape sequence handling.
|
||||||
|
|
||||||
|
use crate::graphics::{GraphicsCommand, ImageStorage};
|
||||||
use crate::keyboard::{query_response, KeyboardState};
|
use crate::keyboard::{query_response, KeyboardState};
|
||||||
use crate::vt_parser::{CsiParams, Handler, Parser};
|
use crate::vt_parser::{CsiParams, Handler, Parser};
|
||||||
|
|
||||||
@@ -320,15 +321,12 @@ pub struct ScrollbackBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ScrollbackBuffer {
|
impl ScrollbackBuffer {
|
||||||
/// Creates a new scrollback buffer with the given capacity and column width.
|
/// Creates a new scrollback buffer with the given capacity.
|
||||||
/// All lines are pre-allocated to avoid any allocation during scrolling.
|
/// Lines are allocated lazily as needed to avoid slow startup.
|
||||||
pub fn new(capacity: usize, cols: usize) -> Self {
|
pub fn new(capacity: usize) -> Self {
|
||||||
// Pre-allocate all lines upfront
|
// Don't pre-allocate lines - allocate them lazily as content is added
|
||||||
let lines = if capacity > 0 {
|
// This avoids allocating and zeroing potentially 20MB+ of memory at startup
|
||||||
(0..capacity).map(|_| vec![Cell::default(); cols]).collect()
|
let lines = Vec::with_capacity(capacity.min(1024)); // Start with reasonable capacity
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
lines,
|
lines,
|
||||||
@@ -361,9 +359,9 @@ impl ScrollbackBuffer {
|
|||||||
/// If the buffer is full, the oldest line is overwritten and its slot is returned
|
/// If the buffer is full, the oldest line is overwritten and its slot is returned
|
||||||
/// for reuse (the caller can swap content into it).
|
/// for reuse (the caller can swap content into it).
|
||||||
///
|
///
|
||||||
/// This is the key operation - it's O(1) with just modulo arithmetic, no allocation.
|
/// Lines are allocated lazily on first use to avoid slow startup.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn push(&mut self) -> &mut Vec<Cell> {
|
pub fn push(&mut self, cols: usize) -> &mut Vec<Cell> {
|
||||||
if self.capacity == 0 {
|
if self.capacity == 0 {
|
||||||
// Shouldn't happen in normal use, but handle gracefully
|
// Shouldn't happen in normal use, but handle gracefully
|
||||||
panic!("Cannot push to zero-capacity scrollback buffer");
|
panic!("Cannot push to zero-capacity scrollback buffer");
|
||||||
@@ -379,7 +377,11 @@ impl ScrollbackBuffer {
|
|||||||
self.start = (self.start + 1) % self.capacity;
|
self.start = (self.start + 1) % self.capacity;
|
||||||
// count stays the same
|
// count stays the same
|
||||||
} else {
|
} else {
|
||||||
// Buffer not full yet - just increment count
|
// Buffer not full yet - allocate new line if needed
|
||||||
|
if idx >= self.lines.len() {
|
||||||
|
// Grow the lines vector and allocate the new line
|
||||||
|
self.lines.push(vec![Cell::default(); cols]);
|
||||||
|
}
|
||||||
self.count += 1;
|
self.count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,6 +490,12 @@ pub struct Terminal {
|
|||||||
/// Command queue for terminal-to-application communication.
|
/// Command queue for terminal-to-application communication.
|
||||||
/// Commands are added by OSC handlers and consumed by the application.
|
/// Commands are added by OSC handlers and consumed by the application.
|
||||||
command_queue: Vec<TerminalCommand>,
|
command_queue: Vec<TerminalCommand>,
|
||||||
|
/// Image storage for Kitty graphics protocol.
|
||||||
|
pub image_storage: ImageStorage,
|
||||||
|
/// Cell width in pixels (for image sizing).
|
||||||
|
pub cell_width: f32,
|
||||||
|
/// Cell height in pixels (for image sizing).
|
||||||
|
pub cell_height: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Terminal {
|
impl Terminal {
|
||||||
@@ -529,7 +537,7 @@ impl Terminal {
|
|||||||
keyboard: KeyboardState::new(),
|
keyboard: KeyboardState::new(),
|
||||||
response_queue: Vec::new(),
|
response_queue: Vec::new(),
|
||||||
palette: ColorPalette::default(),
|
palette: ColorPalette::default(),
|
||||||
scrollback: ScrollbackBuffer::new(scrollback_limit, cols),
|
scrollback: ScrollbackBuffer::new(scrollback_limit),
|
||||||
scroll_offset: 0,
|
scroll_offset: 0,
|
||||||
mouse_tracking: MouseTrackingMode::default(),
|
mouse_tracking: MouseTrackingMode::default(),
|
||||||
mouse_encoding: MouseEncoding::default(),
|
mouse_encoding: MouseEncoding::default(),
|
||||||
@@ -545,6 +553,9 @@ impl Terminal {
|
|||||||
parser: Some(Parser::new()),
|
parser: Some(Parser::new()),
|
||||||
stats: ProcessingStats::default(),
|
stats: ProcessingStats::default(),
|
||||||
command_queue: Vec::new(),
|
command_queue: Vec::new(),
|
||||||
|
image_storage: ImageStorage::new(),
|
||||||
|
cell_width: 10.0, // Default, will be set by renderer
|
||||||
|
cell_height: 20.0, // Default, will be set by renderer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -808,7 +819,8 @@ impl Terminal {
|
|||||||
if self.scroll_top == 0 && !self.using_alternate_screen && self.scrollback.capacity > 0 {
|
if self.scroll_top == 0 && !self.using_alternate_screen && self.scrollback.capacity > 0 {
|
||||||
// Get a slot in the ring buffer - this is O(1) with just modulo arithmetic
|
// Get a slot in the ring buffer - this is O(1) with just modulo arithmetic
|
||||||
// If buffer is full, this overwrites the oldest line (perfect for our swap)
|
// If buffer is full, this overwrites the oldest line (perfect for our swap)
|
||||||
let dest = self.scrollback.push();
|
let cols = self.cols;
|
||||||
|
let dest = self.scrollback.push(cols);
|
||||||
// Swap grid row content into scrollback slot
|
// Swap grid row content into scrollback slot
|
||||||
// The scrollback slot's old content (if any) moves to the grid row
|
// The scrollback slot's old content (if any) moves to the grid row
|
||||||
std::mem::swap(&mut self.grid[recycled_grid_row], dest);
|
std::mem::swap(&mut self.grid[recycled_grid_row], dest);
|
||||||
@@ -1184,7 +1196,8 @@ impl Terminal {
|
|||||||
for visual_row in 0..self.rows {
|
for visual_row in 0..self.rows {
|
||||||
let grid_row = self.line_map[visual_row];
|
let grid_row = self.line_map[visual_row];
|
||||||
// Get a slot in the ring buffer and swap content into it
|
// Get a slot in the ring buffer and swap content into it
|
||||||
let dest = self.scrollback.push();
|
let cols = self.cols;
|
||||||
|
let dest = self.scrollback.push(cols);
|
||||||
std::mem::swap(&mut self.grid[grid_row], dest);
|
std::mem::swap(&mut self.grid[grid_row], dest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1403,6 +1416,12 @@ impl Handler for Terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle an APC (Application Program Command) sequence.
|
||||||
|
/// Used for Kitty graphics protocol.
|
||||||
|
fn apc(&mut self, data: &[u8]) {
|
||||||
|
self.handle_apc(data);
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle a complete CSI sequence.
|
/// Handle a complete CSI sequence.
|
||||||
fn csi(&mut self, params: &CsiParams) {
|
fn csi(&mut self, params: &CsiParams) {
|
||||||
let action = params.final_char as char;
|
let action = params.final_char as char;
|
||||||
@@ -1619,10 +1638,32 @@ impl Handler for Terminal {
|
|||||||
self.cursor_row = 0;
|
self.cursor_row = 0;
|
||||||
self.cursor_col = 0;
|
self.cursor_col = 0;
|
||||||
}
|
}
|
||||||
// Window manipulation (CSI Ps t)
|
// Window manipulation (CSI Ps t) - XTWINOPS
|
||||||
't' => {
|
't' => {
|
||||||
let ps = params.get(0, 0);
|
let ps = params.get(0, 0);
|
||||||
match ps {
|
match ps {
|
||||||
|
14 => {
|
||||||
|
// Report text area size in pixels: CSI 4 ; height ; width t
|
||||||
|
let pixel_height = (self.rows as f32 * self.cell_height) as u32;
|
||||||
|
let pixel_width = (self.cols as f32 * self.cell_width) as u32;
|
||||||
|
let response = format!("\x1b[4;{};{}t", pixel_height, pixel_width);
|
||||||
|
self.response_queue.extend_from_slice(response.as_bytes());
|
||||||
|
log::debug!("XTWINOPS 14: Reported text area size {}x{} pixels", pixel_width, pixel_height);
|
||||||
|
}
|
||||||
|
16 => {
|
||||||
|
// Report cell size in pixels: CSI 6 ; height ; width t
|
||||||
|
let cell_h = self.cell_height as u32;
|
||||||
|
let cell_w = self.cell_width as u32;
|
||||||
|
let response = format!("\x1b[6;{};{}t", cell_h, cell_w);
|
||||||
|
self.response_queue.extend_from_slice(response.as_bytes());
|
||||||
|
log::debug!("XTWINOPS 16: Reported cell size {}x{} pixels", cell_w, cell_h);
|
||||||
|
}
|
||||||
|
18 => {
|
||||||
|
// Report text area size in characters: CSI 8 ; rows ; cols t
|
||||||
|
let response = format!("\x1b[8;{};{}t", self.rows, self.cols);
|
||||||
|
self.response_queue.extend_from_slice(response.as_bytes());
|
||||||
|
log::debug!("XTWINOPS 18: Reported text area size {}x{} chars", self.cols, self.rows);
|
||||||
|
}
|
||||||
22 | 23 => {
|
22 | 23 => {
|
||||||
// Save/restore window title - ignore
|
// Save/restore window title - ignore
|
||||||
}
|
}
|
||||||
@@ -2079,4 +2120,73 @@ impl Terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set cell dimensions (called by renderer after font metrics are calculated).
|
||||||
|
pub fn set_cell_size(&mut self, width: f32, height: f32) {
|
||||||
|
self.cell_width = width;
|
||||||
|
self.cell_height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle an APC (Application Program Command) sequence.
|
||||||
|
/// This is used for the Kitty graphics protocol.
|
||||||
|
fn handle_apc(&mut self, data: &[u8]) {
|
||||||
|
// Kitty graphics protocol: APC starts with 'G'
|
||||||
|
if let Some(cmd) = GraphicsCommand::parse(data) {
|
||||||
|
log::debug!(
|
||||||
|
"Graphics command: action={:?} format={:?} id={:?} size={}x{:?} C={} U={}",
|
||||||
|
cmd.action,
|
||||||
|
cmd.format,
|
||||||
|
cmd.image_id,
|
||||||
|
cmd.width.unwrap_or(0),
|
||||||
|
cmd.height,
|
||||||
|
cmd.cursor_movement,
|
||||||
|
cmd.unicode_placeholder
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert cursor_row to absolute row (accounting for scrollback)
|
||||||
|
// This allows images to scroll with terminal content
|
||||||
|
let absolute_row = self.scrollback.len() + self.cursor_row;
|
||||||
|
|
||||||
|
// Process the command
|
||||||
|
let (response, placement_result) = self.image_storage.process_command(
|
||||||
|
cmd,
|
||||||
|
self.cursor_col,
|
||||||
|
absolute_row,
|
||||||
|
self.cell_width,
|
||||||
|
self.cell_height,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Queue the response to send back to the application
|
||||||
|
if let Some(resp) = response {
|
||||||
|
self.response_queue.extend_from_slice(resp.as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move cursor after image placement per Kitty protocol spec:
|
||||||
|
// "After placing an image on the screen the cursor must be moved to the
|
||||||
|
// right by the number of cols in the image placement rectangle and down
|
||||||
|
// by the number of rows in the image placement rectangle."
|
||||||
|
// However, if C=1 was specified, don't move the cursor.
|
||||||
|
if let Some(placement) = placement_result {
|
||||||
|
if !placement.suppress_cursor_move && !placement.virtual_placement {
|
||||||
|
// Move cursor down by (rows - 1) since we're already on the first row
|
||||||
|
// Then set cursor to the column after the image
|
||||||
|
let new_row = self.cursor_row + placement.rows.saturating_sub(1);
|
||||||
|
if new_row >= self.rows {
|
||||||
|
// Need to scroll
|
||||||
|
let scroll_amount = new_row - self.rows + 1;
|
||||||
|
self.scroll_up(scroll_amount);
|
||||||
|
self.cursor_row = self.rows - 1;
|
||||||
|
} else {
|
||||||
|
self.cursor_row = new_row;
|
||||||
|
}
|
||||||
|
// Move cursor to after the image (or stay at column 0 of next line)
|
||||||
|
// Per protocol, cursor ends at the last row of the image
|
||||||
|
log::debug!(
|
||||||
|
"Cursor moved after image placement: row={} (moved {} rows)",
|
||||||
|
self.cursor_row, placement.rows.saturating_sub(1)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -558,7 +558,7 @@ impl Parser {
|
|||||||
self.csi.primary = ch;
|
self.csi.primary = ch;
|
||||||
self.csi.state = CsiState::Body;
|
self.csi.state = CsiState::Body;
|
||||||
}
|
}
|
||||||
b' ' | b'\'' | b'"' | b'!' | b'$' => {
|
b' ' | b'\'' | b'"' | b'!' | b'$' | b'#' | b'*' => {
|
||||||
self.csi.secondary = ch;
|
self.csi.secondary = ch;
|
||||||
self.csi.state = CsiState::PostSecondary;
|
self.csi.state = CsiState::PostSecondary;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,3 +68,7 @@ zterm|ZTerm - GPU-accelerated terminal emulator,
|
|||||||
XM=\E[?1006;1000%?%p1%{1}%=%th%el%;,
|
XM=\E[?1006;1000%?%p1%{1}%=%th%el%;,
|
||||||
Ss=\E[%p1%d q,
|
Ss=\E[%p1%d q,
|
||||||
Se=\E[2 q,
|
Se=\E[2 q,
|
||||||
|
Sync=\E[?2026%?%p1%{1}%=%th%el%;,
|
||||||
|
Smulx=\E[4:%p1%dm,
|
||||||
|
setrgbf=\E[38:2:%p1%d:%p2%d:%p3%dm,
|
||||||
|
setrgbb=\E[48:2:%p1%d:%p2%d:%p3%dm,
|
||||||
|
|||||||
Reference in New Issue
Block a user