tick system, AVX2 UTF-8 decoder, uh faster in general

This commit is contained in:
Zacharias-Brohn
2025-12-22 00:22:55 +01:00
parent f6a5e23f3d
commit 73b52ab341
30 changed files with 10231 additions and 5210 deletions
+359
View File
@@ -0,0 +1,359 @@
# Atlas Texture Refactor: Array of Textures
## Problem
When adding a new layer to the glyph atlas, the current implementation creates a new texture array with N+1 layers and copies all N existing layers to it. This causes performance issues that scale with the number of layers:
- Layer 1→2: Copy 256MB (8192×8192×4 bytes)
- Layer 2→3: Copy 512MB
- Layer 3→4: Copy 768MB
- etc.
Observed frame times when adding layers:
- Layer 1 added: 14.4ms
- Layer 2 added: 21.9ms
- Layer 3 added: 34.2ms
## Solution
Instead of using a single `texture_2d_array` that must be reallocated and copied when growing, use a **`Vec` of separate 2D textures**. When a new layer is needed, simply create a new texture and add it to the vector. No copying of existing texture data is required.
The bind group must be recreated to include the new texture, but this is a cheap CPU-side operation (just creating metadata/pointers).
## Current Implementation
### Rust (renderer.rs)
**Struct fields:**
```rust
atlas_texture: wgpu::Texture, // Single texture array
atlas_view: wgpu::TextureView, // Single view
atlas_num_layers: u32, // Number of layers in the array
atlas_current_layer: u32, // Current layer being written to
```
**Bind group layout (binding 0):**
```rust
ty: wgpu::BindingType::Texture {
view_dimension: wgpu::TextureViewDimension::D2Array,
// ...
},
count: None, // Single texture
```
**Bind group entry:**
```rust
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(&atlas_view),
}
```
### WGSL (glyph_shader.wgsl)
**Texture declaration:**
```wgsl
@group(0) @binding(0)
var atlas_texture: texture_2d_array<f32>;
```
**Sampling:**
```wgsl
let sample = textureSample(atlas_texture, atlas_sampler, uv, layer_index);
```
## New Implementation
### Rust (renderer.rs)
**Struct fields:**
```rust
atlas_textures: Vec<wgpu::Texture>, // Vector of separate textures
atlas_views: Vec<wgpu::TextureView>, // Vector of views (one per texture)
atlas_current_layer: u32, // Current layer being written to
// atlas_num_layers removed - use atlas_textures.len() instead
```
**Bind group layout (binding 0):**
```rust
ty: wgpu::BindingType::Texture {
view_dimension: wgpu::TextureViewDimension::D2, // Changed from D2Array
// ...
},
count: Some(NonZeroU32::new(MAX_ATLAS_LAYERS).unwrap()), // Array of textures
```
**Bind group entry:**
```rust
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureViewArray(&atlas_view_refs),
}
```
Where `atlas_view_refs` is a `Vec<&wgpu::TextureView>` containing references to all views.
**Note:** wgpu requires the bind group to have exactly `count` textures. We'll need to either:
1. Pre-create dummy textures to fill unused slots, OR
2. Recreate the bind group layout when adding textures (more complex)
Option 1 is simpler: create small 1x1 dummy textures for unused slots up to MAX_ATLAS_LAYERS.
### WGSL (glyph_shader.wgsl)
**Texture declaration:**
```wgsl
@group(0) @binding(0)
var atlas_textures: binding_array<texture_2d<f32>>;
```
**Sampling:**
```wgsl
let sample = textureSample(atlas_textures[layer_index], atlas_sampler, uv);
```
**Note:** `binding_array` requires the `binding_array` feature in wgpu, which should be enabled by default on most backends.
## Implementation Steps
### Step 1: Update Struct Fields
In `renderer.rs`, change:
```rust
// Old
atlas_texture: wgpu::Texture,
atlas_view: wgpu::TextureView,
atlas_num_layers: u32,
// New
atlas_textures: Vec<wgpu::Texture>,
atlas_views: Vec<wgpu::TextureView>,
```
### Step 2: Create Helper for New Atlas Layer
```rust
fn create_atlas_layer(device: &wgpu::Device) -> (wgpu::Texture, wgpu::TextureView) {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Glyph Atlas Layer"),
size: wgpu::Extent3d {
width: ATLAS_SIZE,
height: ATLAS_SIZE,
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: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
(texture, view)
}
```
### Step 3: Update Bind Group Layout
```rust
use std::num::NonZeroU32;
let glyph_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Glyph Bind Group Layout"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: false },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: Some(NonZeroU32::new(MAX_ATLAS_LAYERS).unwrap()),
},
// ... sampler entry unchanged
],
});
```
### Step 4: Initialize with Dummy Textures
At initialization, create one real texture and fill the rest with 1x1 dummy textures:
```rust
fn create_dummy_texture(device: &wgpu::Device) -> (wgpu::Texture, wgpu::TextureView) {
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Dummy Atlas Texture"),
size: wgpu::Extent3d {
width: 1,
height: 1,
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,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
(texture, view)
}
// In new():
let mut atlas_textures = Vec::with_capacity(MAX_ATLAS_LAYERS as usize);
let mut atlas_views = Vec::with_capacity(MAX_ATLAS_LAYERS as usize);
// First texture is real
let (tex, view) = create_atlas_layer(&device);
atlas_textures.push(tex);
atlas_views.push(view);
// Fill rest with dummies
for _ in 1..MAX_ATLAS_LAYERS {
let (tex, view) = create_dummy_texture(&device);
atlas_textures.push(tex);
atlas_views.push(view);
}
```
### Step 5: Update Bind Group Creation
```rust
fn create_atlas_bind_group(&self) -> wgpu::BindGroup {
let view_refs: Vec<&wgpu::TextureView> = self.atlas_views.iter().collect();
self.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Glyph Bind Group"),
layout: &self.glyph_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureViewArray(&view_refs),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(&self.atlas_sampler),
},
],
})
}
```
### Step 6: Update add_atlas_layer / ensure_atlas_layer_capacity
```rust
fn ensure_atlas_layer_capacity(&mut self, target_layer: u32) {
// Count real layers (non-dummy)
let current_real_layers = self.atlas_current_layer + 1;
if target_layer < current_real_layers {
return; // Already have this layer
}
if target_layer >= MAX_ATLAS_LAYERS {
log::error!("Atlas layer limit reached");
return;
}
log::info!("Adding atlas layer {}", target_layer);
// Create new real texture
let (texture, view) = Self::create_atlas_layer(&self.device);
// Replace dummy at this index with real texture
self.atlas_textures[target_layer as usize] = texture;
self.atlas_views[target_layer as usize] = view;
// Recreate bind group with updated view
self.glyph_bind_group = self.create_atlas_bind_group();
}
```
### Step 7: Update upload_cell_canvas_to_atlas
Change texture reference from `&self.atlas_texture` to `&self.atlas_textures[layer as usize]`:
```rust
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.atlas_textures[layer as usize], // Changed
mip_level: 0,
origin: wgpu::Origin3d {
x: self.atlas_cursor_x,
y: self.atlas_cursor_y,
z: 0, // Always 0 now, layer is selected by texture index
},
aspect: wgpu::TextureAspect::All,
},
// ... rest unchanged
);
```
### Step 8: Update Shader
In `glyph_shader.wgsl`:
```wgsl
// Old
@group(0) @binding(0)
var atlas_texture: texture_2d_array<f32>;
// New
@group(0) @binding(0)
var atlas_textures: binding_array<texture_2d<f32>>;
```
Update all `textureSample` calls:
```wgsl
// Old
let sample = textureSample(atlas_texture, atlas_sampler, uv, layer_index);
// New
let sample = textureSample(atlas_textures[layer_index], atlas_sampler, uv);
```
**Locations to update in glyph_shader.wgsl:**
- Line 91: Declaration
- Line 106: `fs_main` sampling
- Line 700: Cursor sprite sampling in `fs_cell`
- Line 723: Glyph sampling in `fs_cell`
- Line 747: Underline sampling in `fs_cell`
- Line 761: Strikethrough sampling in `fs_cell`
### Step 9: Update statusline_shader.wgsl
The statusline shader also uses the atlas. Check and update similarly.
### Step 10: Update Other References
Search for all uses of:
- `atlas_texture`
- `atlas_view`
- `atlas_num_layers`
- `D2Array`
And update accordingly.
## Testing
1. Build and run: `cargo build && cargo run`
2. Verify glyphs render correctly
3. Use terminal heavily to trigger layer additions
4. Check logs for "Adding atlas layer" messages
5. Verify no slow frame warnings during layer addition
6. Test with emoji (color glyphs) to ensure they still work
## Rollback Plan
If issues arise, the changes can be reverted by:
1. Restoring `texture_2d_array` in shaders
2. Restoring single `atlas_texture`/`atlas_view` in Rust
3. Restoring the copy-based layer addition
## References
- wgpu binding arrays: https://docs.rs/wgpu/latest/wgpu/enum.BindingResource.html#variant.TextureViewArray
- WGSL binding_array: https://www.w3.org/TR/WGSL/#binding-array
- Kitty's approach: `/tmp/kitty/kitty/shaders.c` (uses `GL_TEXTURE_2D_ARRAY` with `glCopyImageSubData`)
+248
View File
@@ -0,0 +1,248 @@
# Config.rs Reorganization Analysis
This document analyzes `/src/config.rs` (438 lines) and identifies sections that could be extracted into separate files to improve code organization.
## Current Structure Overview
The file contains:
- `TabBarPosition` enum (lines 10-21)
- `Keybind` struct + parsing logic (lines 23-166)
- `Action` enum (lines 168-206)
- `Keybindings` struct + default impl (lines 208-321)
- `Config` struct + load/save logic (lines 323-437)
---
## Recommended Extractions
### 1. Keybind Parsing Module
**Proposed file:** `src/keybind.rs`
**Lines:** 23-166 (144 lines)
**Types involved:**
- `struct Keybind`
- `impl Keybind` (including `parse()` and `normalize_key_name()`)
**Current code:**
```rust
pub struct Keybind(pub String);
impl Keybind {
pub fn parse(&self) -> Option<(bool, bool, bool, bool, String)> { ... }
fn normalize_key_name(name: &str) -> Option<&'static str> { ... }
}
```
**Dependencies needed:**
- `serde::{Deserialize, Serialize}` (for derive macros)
**Why extract:**
- Self-contained parsing logic with no dependencies on other config types
- The `normalize_key_name` function is a substantial lookup table (70+ lines)
- Could be tested independently with unit tests for key parsing edge cases
- Reusable if keybinding logic is needed elsewhere (e.g., a config editor UI)
**Challenges:**
- None significant. This is a clean extraction.
**After extraction, config.rs would:**
```rust
pub use keybind::Keybind;
// or
mod keybind;
pub use keybind::Keybind;
```
---
### 2. Actions Module
**Proposed file:** `src/action.rs`
**Lines:** 168-206 (39 lines)
**Types involved:**
- `enum Action`
**Current code:**
```rust
pub enum Action {
NewTab,
NextTab,
PrevTab,
Tab1, Tab2, ... Tab9,
SplitHorizontal,
SplitVertical,
ClosePane,
FocusPaneUp, FocusPaneDown, FocusPaneLeft, FocusPaneRight,
Copy,
Paste,
}
```
**Dependencies needed:**
- `serde::{Deserialize, Serialize}`
**Why extract:**
- `Action` is a distinct concept used throughout the codebase (keyboard.rs likely references it)
- Decouples the "what can be done" from "how it's configured"
- Makes it easy to add new actions without touching config logic
- Could be extended with action metadata (description, default keybind, etc.)
**Challenges:**
- Small file on its own (39 lines). Could be bundled with `keybind.rs` as a combined `src/keybindings.rs` module.
**Alternative:** Combine with Keybindings into a single module (see option 3).
---
### 3. Combined Keybindings Module (Recommended)
**Proposed file:** `src/keybindings.rs`
**Lines:** 23-321 (299 lines)
**Types involved:**
- `struct Keybind` + impl
- `enum Action`
- `struct Keybindings` + impl (including `Default` and `build_action_map`)
**Dependencies needed:**
- `serde::{Deserialize, Serialize}`
- `std::collections::HashMap`
**Why extract:**
- These three types form a cohesive "keybindings subsystem"
- `Keybindings::build_action_map()` ties together `Keybind`, `Action`, and `Keybindings`
- Reduces config.rs from 438 lines to ~140 lines
- Clear separation: config.rs handles general settings, keybindings.rs handles input mapping
**What stays in config.rs:**
- `TabBarPosition` enum
- `Config` struct with `keybindings: Keybindings` field
- `Config::load()`, `Config::save()`, `Config::config_path()`
**After extraction, config.rs would:**
```rust
mod keybindings;
pub use keybindings::{Action, Keybind, Keybindings};
```
**Challenges:**
- Need to ensure `Keybindings` is re-exported for external use
- The `Keybindings` struct is embedded in `Config`, so it must remain `pub`
---
### 4. TabBarPosition Enum
**Proposed file:** Could stay in `config.rs` or move to `src/ui.rs` / `src/types.rs`
**Lines:** 10-21 (12 lines)
**Types involved:**
- `enum TabBarPosition`
**Why extract:**
- Very small (12 lines) - extraction may be overkill
- Could be grouped with other UI-related enums if more are added in the future
**Recommendation:** Keep in `config.rs` for now. Only extract if you add more UI-related configuration enums (e.g., `CursorStyle`, `ScrollbarPosition`, etc.).
---
## Recommended Module Structure
```
src/
config.rs # Config struct, load/save, TabBarPosition (~140 lines)
keybindings.rs # Keybind, Action, Keybindings (~300 lines)
# or alternatively:
keybindings/
mod.rs # Re-exports
keybind.rs # Keybind struct + parsing
action.rs # Action enum
bindings.rs # Keybindings struct
```
For a codebase of this size, the single `keybindings.rs` file is recommended over a subdirectory.
---
## Implementation Steps
### Step 1: Create `src/keybindings.rs`
1. Create new file `src/keybindings.rs`
2. Move lines 23-321 from config.rs
3. Add module header:
```rust
//! Keybinding types and parsing for ZTerm.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
```
4. Ensure all types are `pub`
### Step 2: Update `src/config.rs`
1. Remove lines 23-321
2. Add at the top (after the module doc comment):
```rust
mod keybindings;
pub use keybindings::{Action, Keybind, Keybindings};
```
3. Keep existing imports that `Config` needs
### Step 3: Update `src/lib.rs` or `src/main.rs`
If `keybindings` types are used directly elsewhere, update the module declarations:
```rust
// In lib.rs, if keybindings needs to be public:
pub mod keybindings;
// Or re-export from config:
pub use config::{Action, Keybind, Keybindings};
```
### Step 4: Verify compilation
```bash
cargo check
cargo test
```
---
## Summary Table
| Section | Lines | New File | Priority | Effort |
|---------|-------|----------|----------|--------|
| Keybind + Action + Keybindings | 23-321 (299 lines) | `keybindings.rs` | **High** | Low |
| TabBarPosition | 10-21 (12 lines) | Keep in config.rs | Low | N/A |
| Config struct + impls | 323-437 (115 lines) | Keep in config.rs | N/A | N/A |
**Recommended action:** Extract the keybindings module as a single file. This provides the best balance of organization improvement vs. complexity.
---
## Benefits After Reorganization
1. **config.rs** becomes focused on:
- Core configuration values (font, opacity, scrollback, etc.)
- File I/O (load/save)
- ~140 lines instead of 438
2. **keybindings.rs** becomes a focused module for:
- Input parsing and normalization
- Action definitions
- Keybinding-to-action mapping
- ~300 lines, highly cohesive
3. **Testing:** Keybinding parsing can be unit tested in isolation
4. **Future extensibility:**
- Adding new actions: edit `keybindings.rs`
- Adding new config options: edit `config.rs`
- Clear separation of concerns
+12 -1
View File
@@ -14,7 +14,7 @@ path = "src/main.rs"
[dependencies] [dependencies]
# Window and rendering # Window and rendering
winit = { version = "0.30", features = ["wayland", "x11"] } winit = { version = "0.30", features = ["wayland", "x11"] }
wgpu = "23" wgpu = "28"
pollster = "0.4" pollster = "0.4"
# PTY handling # PTY handling
@@ -27,6 +27,10 @@ polling = "3"
thiserror = "2" thiserror = "2"
# Logging # Logging
# No release_max_level set by default - use features to control:
# - Default: all logs enabled (for development)
# - --features production: disables all logs in release builds
# - --features render_timing: enables timing instrumentation with info-level logging
log = "0.4" log = "0.4"
env_logger = "0.11" env_logger = "0.11"
@@ -65,6 +69,9 @@ memmap2 = "0.9"
# Fast byte searching # Fast byte searching
memchr = "2" memchr = "2"
# Fast HashMap (FxHash - what rustc uses)
rustc-hash = "2"
# Base64 decoding for OSC statusline # Base64 decoding for OSC statusline
base64 = "0.22" base64 = "0.22"
@@ -79,3 +86,7 @@ ffmpeg-next = { version = "8.0", optional = true }
[features] [features]
default = ["webm"] default = ["webm"]
webm = ["ffmpeg-next"] webm = ["ffmpeg-next"]
# Enable timing instrumentation for performance debugging
render_timing = []
# Production build: disable all logging for zero overhead
production = ["log/release_max_level_off"]
+346
View File
@@ -0,0 +1,346 @@
# Main.rs Reorganization Plan
This document identifies sections of `src/main.rs` (2793 lines) that could be extracted into separate modules to improve code organization, maintainability, and testability.
## Summary of Proposed Extractions
| Module Name | Lines | Primary Contents |
|-------------|-------|------------------|
| `pty_buffer.rs` | ~180 | `SharedPtyBuffer`, `BufferState` |
| `pane.rs` | ~400 | `PaneId`, `Pane`, `PaneGeometry`, `SplitNode` |
| `tab.rs` | ~150 | `TabId`, `Tab` |
| `selection.rs` | ~45 | `CellPosition`, `Selection` |
| `statusline.rs` | ~220 | `GitStatus`, `build_cwd_section()`, `build_git_section()`, `get_git_status()` |
| `instance.rs` | ~45 | PID file management, `signal_existing_instance()` |
---
## 1. PTY Buffer Module (`pty_buffer.rs`)
**Lines: 30-227 (~197 lines)**
### Contents
- `PTY_BUF_SIZE` constant
- `SharedPtyBuffer` struct and impl
- `BufferState` struct
- `unsafe impl Sync/Send for SharedPtyBuffer`
### Description
This is a self-contained, zero-copy PTY I/O buffer implementation inspired by Kitty. It has no dependencies on other application-specific types and is purely concerned with efficient I/O buffering between an I/O thread and the main thread.
### Types/Functions to Extract
```rust
const PTY_BUF_SIZE: usize
struct BufferState
struct SharedPtyBuffer
impl SharedPtyBuffer
impl Drop for SharedPtyBuffer
unsafe impl Sync for SharedPtyBuffer
unsafe impl Send for SharedPtyBuffer
```
### Dependencies
- `std::cell::UnsafeCell`
- `std::sync::Mutex`
- `libc` (for `eventfd`, `read`, `write`, `close`)
### Challenges
- **Unsafe code**: Contains `UnsafeCell` and raw pointer manipulation. Must carefully preserve safety invariants.
- **libc dependency**: Uses Linux-specific `eventfd` syscalls.
### Recommendation
**High priority extraction.** This is a well-documented, self-contained component with clear boundaries. Would benefit from its own unit tests for buffer operations.
---
## 2. Pane Module (`pane.rs`)
**Lines: 229-691 (~462 lines)**
### Contents
- `PaneId` - unique identifier with atomic generation
- `Pane` - terminal + PTY + selection state
- `PaneGeometry` - pixel layout information
- `SplitNode` - tree structure for split pane layouts
### Types/Functions to Extract
```rust
struct PaneId
impl PaneId
struct Pane
impl Pane
- new()
- resize()
- write_to_pty()
- child_exited()
- foreground_matches()
- calculate_dim_factor()
struct PaneGeometry
enum SplitNode
impl SplitNode
- leaf()
- split()
- layout()
- find_geometry()
- collect_geometries()
- find_neighbor()
- overlaps_horizontally()
- overlaps_vertically()
- remove_pane()
- contains_pane()
- split_pane()
```
### Dependencies
- `zterm::terminal::{Direction, Terminal, TerminalCommand, MouseTrackingMode}`
- `zterm::pty::Pty`
- `std::sync::Arc`
- `std::os::fd::AsRawFd`
- `SharedPtyBuffer` (from pty_buffer module)
- `Selection` (from selection module)
### Challenges
- **Cross-module dependencies**: `Pane` references `SharedPtyBuffer` and `Selection`, so those would need to be extracted first or extracted together.
- **`SplitNode` complexity**: The recursive tree structure has complex layout logic. Consider keeping `SplitNode` with `Pane` since they're tightly coupled.
### Recommendation
**High priority extraction.** The pane management is a distinct concern from the main application loop. This would make the split tree logic easier to test in isolation.
---
## 3. Tab Module (`tab.rs`)
**Lines: 693-872 (~180 lines)**
### Contents
- `TabId` - unique identifier with atomic generation
- `Tab` - collection of panes with a split tree
### Types/Functions to Extract
```rust
struct TabId
impl TabId
struct Tab
impl Tab
- new()
- active_pane() / active_pane_mut()
- resize()
- write_to_pty()
- check_exited_panes()
- split()
- remove_pane()
- close_active_pane()
- focus_neighbor()
- get_pane() / get_pane_mut()
- collect_pane_geometries()
- child_exited()
```
### Dependencies
- `std::collections::HashMap`
- `PaneId`, `Pane`, `PaneGeometry`, `SplitNode` (from pane module)
- `zterm::terminal::Direction`
### Challenges
- **Tight coupling with Pane module**: Tab is essentially a container for panes. Consider combining into a single `pane.rs` module or using `pane/mod.rs` with submodules.
### Recommendation
**Medium priority.** Could be combined with the pane module under `pane/mod.rs` with `pane/tab.rs` as a submodule, or kept as a separate `tab.rs`.
---
## 4. Selection Module (`selection.rs`)
**Lines: 1218-1259 (~42 lines)**
### Contents
- `CellPosition` - column/row position
- `Selection` - start/end positions for text selection
### Types/Functions to Extract
```rust
struct CellPosition
struct Selection
impl Selection
- normalized()
- to_screen_coords()
```
### Dependencies
- None (completely self-contained)
### Challenges
- **Very small**: Only 42 lines. May be too small to justify its own file.
### Recommendation
**Low priority as standalone.** Consider bundling with the pane module since selection is per-pane state, or creating a `types.rs` for small shared types.
---
## 5. Statusline Module (`statusline.rs`)
**Lines: 917-1216 (~300 lines)**
### Contents
- `build_cwd_section()` - creates CWD statusline section
- `GitStatus` struct - git repository state
- `get_git_status()` - queries git for repo status
- `build_git_section()` - creates git statusline section
### Types/Functions to Extract
```rust
fn build_cwd_section(cwd: &str) -> StatuslineSection
struct GitStatus
fn get_git_status(cwd: &str) -> Option<GitStatus>
fn build_git_section(cwd: &str) -> Option<StatuslineSection>
```
### Dependencies
- `zterm::renderer::{StatuslineComponent, StatuslineSection}`
- `std::process::Command` (for git commands)
- `std::env` (for HOME variable)
### Challenges
- **External process calls**: Uses `git` commands. Consider whether this should be async or cached.
- **Powerline icons**: Uses hardcoded Unicode codepoints (Nerd Font icons).
### Recommendation
**High priority extraction.** This is completely independent of the main application state. Would benefit from:
- Caching git status (it's currently queried every frame)
- Unit tests for path transformation logic
- Potential async git queries
---
## 6. Instance Management Module (`instance.rs`)
**Lines: 874-915, 2780-2792 (~55 lines total)**
### Contents
- PID file path management
- Single-instance detection and signaling
- Signal handler for SIGUSR1
### Types/Functions to Extract
```rust
fn pid_file_path() -> PathBuf
fn signal_existing_instance() -> bool
fn write_pid_file() -> std::io::Result<()>
fn remove_pid_file()
// From end of file:
static mut EVENT_PROXY: Option<EventLoopProxy<UserEvent>>
extern "C" fn handle_sigusr1(_: i32)
```
### Dependencies
- `std::fs`
- `std::path::PathBuf`
- `libc` (for `kill` syscall)
- `winit::event_loop::EventLoopProxy`
- `UserEvent` enum
### Challenges
- **Global static**: The `EVENT_PROXY` static is `unsafe` and tightly coupled to the signal handler.
- **Split location**: The signal handler is at the end of the file, separate from PID functions.
### Recommendation
**Medium priority.** Small but distinct concern. The global static handling could be cleaner in a dedicated module.
---
## 7. Additional Observations
### UserEvent Enum (Line 1262-1270)
```rust
enum UserEvent {
ShowWindow,
PtyReadable(PaneId),
ConfigReloaded,
}
```
This is a small enum but is referenced throughout. Consider placing in a `types.rs` or `events.rs` module.
### Config Watcher (Lines 2674-2735)
The `setup_config_watcher()` function is self-contained and could go in the instance module or a dedicated `config_watcher.rs`.
### App Struct (Lines 1272-1334)
The `App` struct and its impl are the core of the application and should remain in `main.rs`. However, some of its methods could potentially be split:
- I/O thread management (lines 1418-1553)
- Keyboard/keybinding handling (lines 1773-2221)
- Mouse handling (scattered through `window_event`)
### Keyboard Handling
Lines 1773-2221 contain significant keyboard handling logic. This could potentially be extracted, but it's tightly integrated with the `App` state.
---
## Suggested Module Structure
```
src/
main.rs (~1200 lines - App, ApplicationHandler, main())
lib.rs (existing)
pty_buffer.rs (new - ~200 lines)
pane.rs (new - ~500 lines, includes SplitNode)
tab.rs (new - ~180 lines)
selection.rs (new - ~45 lines, or merge with pane.rs)
statusline.rs (new - ~300 lines)
instance.rs (new - ~60 lines)
config.rs (existing)
...
```
Alternative with submodules:
```
src/
main.rs
lib.rs
pane/
mod.rs (re-exports)
pane.rs (Pane, PaneId)
split.rs (SplitNode, PaneGeometry)
selection.rs (Selection, CellPosition)
tab.rs
pty_buffer.rs
statusline.rs
instance.rs
...
```
---
## Implementation Order
1. **`pty_buffer.rs`** - No internal dependencies, completely self-contained
2. **`selection.rs`** - No dependencies, simple extraction
3. **`statusline.rs`** - No internal dependencies, high value for testability
4. **`instance.rs`** - Small, isolated concern
5. **`pane.rs`** - Depends on pty_buffer and selection
6. **`tab.rs`** - Depends on pane
---
## Testing Opportunities
After extraction, these modules would benefit from unit tests:
| Module | Testable Functionality |
|--------|----------------------|
| `pty_buffer` | Buffer overflow handling, space checking, wakeup signaling |
| `selection` | `normalized()` ordering, `to_screen_coords()` boundary conditions |
| `statusline` | Path normalization (~/ replacement), git status parsing |
| `pane` / `SplitNode` | Layout calculations, neighbor finding, tree operations |
| `instance` | PID file creation/cleanup (integration test) |
---
## Notes on Maintaining Backward Compatibility
All extracted types should be re-exported from `lib.rs` or a prelude if they're used externally. The current architecture appears to be internal to the binary, so this is likely not a concern.
The `Pane` and `Tab` types are not part of the public API (defined in `main.rs`), so extraction won't affect external consumers.
+220
View File
@@ -0,0 +1,220 @@
# Code Reorganization Analysis
This document identifies sections of code that could be moved into separate files to improve code organization.
## Summary
| File | Lines | Assessment | Recommended Extractions |
|------|-------|------------|------------------------|
| `pty.rs` | 260 | Well-organized | None needed |
| `keyboard.rs` | 558 | Could benefit from split | 1 extraction recommended |
| `graphics.rs` | 1846 | Needs refactoring | 3-4 extractions recommended |
---
## pty.rs (260 lines)
### Assessment: Well-Organized - No Changes Needed
The file is focused and cohesive:
- `PtyError` enum (lines 10-28) - Error types for PTY operations
- `Pty` struct and impl (lines 31-250) - Core PTY functionality
- All code directly relates to PTY management
**Rationale for keeping as-is:**
- Single responsibility: pseudo-terminal handling
- Manageable size (260 lines)
- All components are tightly coupled
- No reusable utilities that other modules would need
---
## keyboard.rs (558 lines)
### Assessment: Could Benefit from Minor Refactoring
The file contains the Kitty keyboard protocol implementation with several logical sections.
### Recommended Extraction: None (Optional Minor Refactoring)
While the file has distinct sections, they are all tightly coupled around the keyboard protocol:
1. **Flags/Types** (lines 8-95): `KeyboardFlags`, `KeyEventType`, `Modifiers`
2. **FunctionalKey enum** (lines 96-195): Key code definitions
3. **KeyboardState** (lines 197-292): Protocol state management
4. **KeyEncoder** (lines 294-548): Key encoding logic
**Rationale for keeping as-is:**
- All components implement a single protocol specification
- `KeyEncoder` depends on `KeyboardState`, `Modifiers`, `FunctionalKey`
- The file is under 600 lines, which is manageable
- Splitting would require importing everything back together in practice
**Optional Consideration:**
If the codebase grows to support multiple keyboard protocols, consider:
- `keyboard/mod.rs` - Public API
- `keyboard/kitty.rs` - Kitty protocol implementation
- `keyboard/legacy.rs` - Legacy encoding (currently in `encode_legacy_*` methods)
---
## graphics.rs (1846 lines)
### Assessment: Needs Refactoring - Multiple Extractions Recommended
This file is too large and contains several distinct logical modules that could be separated.
### Extraction 1: Animation Module
**Location:** Lines 391-746 (animation-related code)
**Components to extract:**
- `AnimationState` enum (lines 707-717)
- `AnimationData` struct (lines 719-736)
- `AnimationFrame` struct (lines 738-745)
- `decode_gif()` function (lines 393-459)
- `decode_webm()` function (lines 461-646, when feature enabled)
**Suggested file:** `src/graphics/animation.rs`
**Dependencies:**
```rust
use std::io::{Cursor, Read};
use std::time::Instant;
use image::{codecs::gif::GifDecoder, AnimationDecoder};
use super::GraphicsError;
```
**Challenges:**
- `decode_webm` is feature-gated (`#[cfg(feature = "webm")]`)
- Need to re-export types from `graphics/mod.rs`
---
### Extraction 2: Graphics Protocol Types
**Location:** Lines 16-162, 648-789
**Components to extract:**
- `Action` enum (lines 17-34)
- `Format` enum (lines 36-48)
- `Transmission` enum (lines 50-62)
- `Compression` enum (lines 64-69)
- `DeleteTarget` enum (lines 71-92)
- `GraphicsCommand` struct (lines 94-162)
- `GraphicsError` enum (lines 648-673)
- `ImageData` struct (lines 675-705)
- `PlacementResult` struct (lines 747-758)
- `ImagePlacement` struct (lines 760-789)
**Suggested file:** `src/graphics/types.rs`
**Dependencies:**
```rust
use std::time::Instant;
use super::animation::{AnimationData, AnimationState};
```
**Challenges:**
- `GraphicsCommand` has methods that depend on decoding logic
- Consider keeping `GraphicsCommand::parse()` and `decode_*` methods in types.rs or a separate `parsing.rs`
---
### Extraction 3: Image Storage
**Location:** Lines 791-1807
**Components to extract:**
- `ImageStorage` struct and impl (lines 791-1807)
- `ChunkBuffer` struct (lines 808-813)
**Suggested file:** `src/graphics/storage.rs`
**Dependencies:**
```rust
use std::collections::HashMap;
use std::time::Instant;
use super::types::*;
use super::animation::*;
```
**Challenges:**
- This is the largest section (~1000 lines)
- Contains many handler methods (`handle_transmit`, `handle_put`, etc.)
- Could be further split into:
- `storage.rs` - Core storage and simple operations
- `handlers.rs` - Command handlers
- `animation_handlers.rs` - Animation-specific handlers (lines 1026-1399)
---
### Extraction 4: Base64 Utility
**Location:** Lines 1809-1817
**Components to extract:**
- `base64_decode()` function
**Suggested file:** Could go in a general `src/utils.rs` or stay in graphics
**Dependencies:**
```rust
use base64::Engine;
use super::GraphicsError;
```
**Challenges:** Minimal - this is a simple utility function
---
### Recommended Graphics Module Structure
```
src/
graphics/
mod.rs # Re-exports, module declarations
types.rs # Enums, structs, GraphicsCommand parsing
animation.rs # AnimationData, AnimationFrame, GIF/WebM decoding
storage.rs # ImageStorage, placement logic
handlers.rs # Command handlers (optional further split)
```
**mod.rs example:**
```rust
mod animation;
mod handlers;
mod storage;
mod types;
pub use animation::{AnimationData, AnimationFrame, AnimationState, decode_gif};
pub use storage::ImageStorage;
pub use types::*;
#[cfg(feature = "webm")]
pub use animation::decode_webm;
```
---
## Implementation Priority
1. **High Priority:** Split `graphics.rs` - it's nearly 1900 lines and hard to navigate
2. **Low Priority:** `keyboard.rs` is fine as-is but could be modularized if protocol support expands
3. **No Action:** `pty.rs` is well-organized
---
## Migration Notes
When splitting `graphics.rs`:
1. Start by creating `src/graphics/` directory
2. Move `graphics.rs` to `src/graphics/mod.rs` temporarily
3. Extract types first (fewest dependencies)
4. Extract animation module
5. Extract storage module
6. Update imports in `renderer.rs`, `terminal.rs`, and any other consumers
7. Run tests after each extraction to catch breakages
The tests at the bottom of `graphics.rs` (lines 1819-1845) should remain in `mod.rs` or be split into module-specific test files.
+1 -1
View File
@@ -18,7 +18,7 @@ source=()
build() { build() {
cd "$startdir" cd "$startdir"
cargo build --release --locked cargo build --release --features production
} }
package() { package() {
+705
View File
@@ -0,0 +1,705 @@
# ZTerm Redundancy Audit
Generated: 2025-12-20
## Summary
| File | Current Lines | Est. Savings | % Reduction |
|------|---------------|--------------|-------------|
| renderer.rs | 8426 | ~1000-1400 | 12-17% |
| terminal.rs | 2366 | ~150-180 | ~7% |
| main.rs | 2787 | ~150-180 | ~6% |
| vt_parser.rs | 1044 | ~60-90 | 6-9% |
| Other files | ~3000 | ~100 | ~3% |
| **Total** | **~17600** | **~1500-2000** | **~10%** |
---
## High Priority
### 1. renderer.rs: Pipeline Creation Boilerplate
**Lines:** 2200-2960 (~760 lines)
**Estimated Savings:** 400-500 lines
**Problem:** Nearly identical pipeline creation code with slight variations in entry points, blend modes, and bind group layouts.
**Suggested Fix:** Create a pipeline builder:
```rust
struct PipelineBuilder<'a> {
device: &'a wgpu::Device,
shader: &'a wgpu::ShaderModule,
layout: &'a wgpu::PipelineLayout,
format: wgpu::TextureFormat,
}
impl PipelineBuilder<'_> {
fn build(self, vs_entry: &str, fs_entry: &str, blend: Option<wgpu::BlendState>) -> wgpu::RenderPipeline
}
```
---
### 2. renderer.rs: Box Drawing Character Rendering
**Lines:** 4800-5943 (~1150 lines)
**Estimated Savings:** 400-600 lines
**Problem:** Massive match statement with repeated `hline()`/`vline()`/`fill_rect()` patterns. Many similar light/heavy/double line variants.
**Suggested Fix:**
- Use lookup tables for line positions/thicknesses
- Create a `BoxDrawingSpec` struct with line positions
- Consolidate repeated drawing patterns into parameterized helpers
---
### 3. main.rs: Duplicate NamedKey-to-String Matching
**Lines:** 1778-1812, 2146-2181
**Estimated Savings:** ~70 lines
**Problem:** `check_keybinding()` and `handle_keyboard_input()` both have nearly identical `NamedKey` to string/FunctionalKey matching logic for F1-F12, arrow keys, Home/End/PageUp/PageDown, etc.
**Suggested Fix:**
```rust
fn named_key_to_str(named: &NamedKey) -> Option<&'static str> { ... }
fn named_key_to_functional(named: &NamedKey) -> Option<FunctionalKey> { ... }
```
---
### 4. terminal.rs: Duplicated SGR Extended Color Parsing
**Lines:** 2046-2106
**Estimated Savings:** ~30 lines
**Problem:** SGR 38 (foreground) and SGR 48 (background) extended color parsing logic is nearly identical - duplicated twice each (once for sub-params, once for regular params).
**Suggested Fix:**
```rust
fn parse_extended_color(&self, params: &CsiParams, i: &mut usize) -> Option<Color> {
let mode = params.get(*i + 1, 0);
if mode == 5 && *i + 2 < params.num_params {
*i += 2;
Some(Color::Indexed(params.params[*i] as u8))
} else if mode == 2 && *i + 4 < params.num_params {
let color = Color::Rgb(
params.params[*i + 2] as u8,
params.params[*i + 3] as u8,
params.params[*i + 4] as u8,
);
*i += 4;
Some(color)
} else {
None
}
}
```
---
### 5. terminal.rs: Cursor-Row-With-Scroll Pattern
**Lines:** 1239-1246, 1262-1270, 1319-1324, 1836-1842, 1844-1851, 1916-1922, 1934-1940
**Estimated Savings:** ~25 lines
**Problem:** The pattern "increment cursor_row, check against scroll_bottom, scroll_up(1) if needed" is repeated 7 times.
**Suggested Fix:**
```rust
#[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;
}
}
```
---
### 6. terminal.rs: Cell Construction with Current Attributes
**Lines:** 1278-1287, 1963-1972, 1985-1994
**Estimated Savings:** ~20 lines
**Problem:** `Cell` construction using current attributes is repeated 3 times with nearly identical code.
**Suggested Fix:**
```rust
#[inline]
fn current_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,
}
}
```
---
### 7. config.rs: normalize_key_name Allocates String for Static Values
**Lines:** 89-160
**Estimated Savings:** Eliminates 50+ string allocations
**Problem:** Every match arm allocates a new String even though most are static:
```rust
"left" | "arrowleft" | "arrow_left" => "left".to_string(),
```
**Suggested Fix:**
```rust
fn normalize_key_name(name: &str) -> Cow<'static, str> {
match name {
"left" | "arrowleft" | "arrow_left" => Cow::Borrowed("left"),
// ...
_ => Cow::Owned(name.to_string()),
}
}
```
---
### 8. config.rs: Repeated Parse-and-Insert Blocks in build_action_map
**Lines:** 281-349
**Estimated Savings:** ~40 lines
**Problem:** 20+ repeated blocks:
```rust
if let Some(parsed) = self.new_tab.parse() {
map.insert(parsed, Action::NewTab);
}
```
**Suggested Fix:**
```rust
let bindings: &[(&Keybind, Action)] = &[
(&self.new_tab, Action::NewTab),
(&self.next_tab, Action::NextTab),
// ...
];
for (keybind, action) in bindings {
if let Some(parsed) = keybind.parse() {
map.insert(parsed, *action);
}
}
```
---
### 9. vt_parser.rs: Duplicate OSC/String Command Terminator Handling
**Lines:** 683-755, 773-843
**Estimated Savings:** ~30 lines
**Problem:** `consume_osc` and `consume_string_command` have nearly identical structure for finding and handling terminators (ESC, BEL, C1 ST).
**Suggested Fix:** Extract a common helper:
```rust
fn consume_st_terminated<F>(
&mut self,
bytes: &[u8],
pos: usize,
buffer: &mut Vec<u8>,
include_bel: bool,
on_complete: F,
) -> usize
where
F: FnOnce(&mut Self, &[u8])
```
---
### 10. vt_parser.rs: Duplicate Control Char Handling in CSI States
**Lines:** 547-551, 593-596, 650-653
**Estimated Savings:** ~15 lines
**Problem:** Identical control character handling appears in all three `CsiState` match arms:
```rust
0x00..=0x1F => {
if ch != 0x1B {
handler.control(ch);
}
}
```
**Suggested Fix:** Move control char handling before the `match self.csi.state` block:
```rust
if ch <= 0x1F && ch != 0x1B {
handler.control(ch);
consumed += 1;
continue;
}
```
---
### 11. main.rs: Repeated active_tab().and_then(active_pane()) Pattern
**Lines:** Various (10+ occurrences)
**Estimated Savings:** ~30 lines
**Problem:** This nested Option chain appears throughout the code.
**Suggested Fix:**
```rust
fn active_pane(&self) -> Option<&Pane> {
self.active_tab().and_then(|t| t.active_pane())
}
fn active_pane_mut(&mut self) -> Option<&mut Pane> {
self.active_tab_mut().and_then(|t| t.active_pane_mut())
}
```
---
## Medium Priority
### 12. renderer.rs: set_scale_factor and set_font_size Duplicate Cell Metric Recalc
**Lines:** 3306-3413
**Estimated Savings:** ~50 lines
**Problem:** ~100 lines of nearly identical cell metric recalculation logic.
**Suggested Fix:** Extract to a shared `recalculate_cell_metrics(&mut self)` method.
---
### 13. renderer.rs: find_font_for_char and find_color_font_for_char Similar
**Lines:** 939-1081
**Estimated Savings:** ~60-80 lines
**Problem:** ~140 lines of similar fontconfig query patterns.
**Suggested Fix:** Extract common fontconfig query helper, parameterize the charset/color requirements.
---
### 14. renderer.rs: place_glyph_in_cell_canvas vs Color Variant
**Lines:** 6468-6554
**Estimated Savings:** ~40-50 lines
**Problem:** ~90 lines of nearly identical logic, differing only in bytes-per-pixel (1 vs 4).
**Suggested Fix:**
```rust
fn place_glyph_in_cell_canvas_impl(&self, bitmap: &[u8], ..., bytes_per_pixel: usize) -> Vec<u8>
```
---
### 15. renderer.rs: render_rect vs render_overlay_rect Near-Identical
**Lines:** 7192-7215
**Estimated Savings:** ~10 lines
**Problem:** Near-identical functions pushing to different Vec.
**Suggested Fix:**
```rust
fn render_quad(&mut self, x: f32, y: f32, w: f32, h: f32, color: [f32; 4], overlay: bool)
```
---
### 16. renderer.rs: Pane Border Adjacency Checks Repeated
**Lines:** 7471-7587
**Estimated Savings:** ~60-80 lines
**Problem:** ~120 lines of repetitive adjacency detection for 4 directions.
**Suggested Fix:**
```rust
fn check_pane_adjacency(&self, a: &PaneInfo, b: &PaneInfo) -> Vec<Border>
```
---
### 17. terminal.rs: to_rgba and to_rgba_bg Nearly Identical
**Lines:** 212-239
**Estimated Savings:** ~15 lines
**Problem:** These two methods differ only in the `Color::Default` case.
**Suggested Fix:**
```rust
pub fn to_rgba(&self, color: &Color, is_bg: bool) -> [f32; 4] {
let [r, g, b] = match color {
Color::Default => if is_bg { self.default_bg } else { self.default_fg },
Color::Rgb(r, g, b) => [*r, *g, *b],
Color::Indexed(idx) => self.colors[*idx as usize],
};
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0]
}
```
---
### 18. terminal.rs: insert_lines/delete_lines Share Dirty Marking
**Lines:** 1080-1134
**Estimated Savings:** ~6 lines
**Problem:** Both use identical dirty marking loop that could use existing `mark_region_dirty`.
**Suggested Fix:** Replace:
```rust
for line in self.cursor_row..=self.scroll_bottom {
self.mark_line_dirty(line);
}
```
with:
```rust
self.mark_region_dirty(self.cursor_row, self.scroll_bottom);
```
---
### 19. terminal.rs: handle_dec_private_mode_set/reset Are Mirror Images
**Lines:** 2148-2295
**Estimated Savings:** ~50 lines
**Problem:** ~70 lines each, almost mirror images with `true` vs `false`.
**Suggested Fix:**
```rust
fn handle_dec_private_mode(&mut self, params: &CsiParams, set: bool) {
for i in 0..params.num_params {
match params.params[i] {
1 => self.application_cursor_keys = set,
7 => self.auto_wrap = set,
25 => self.cursor_visible = set,
// ...
}
}
}
```
---
### 20. main.rs: Duplicate Git Status String Building
**Lines:** 1095-1118
**Estimated Savings:** ~15 lines
**Problem:** Identical logic for building `working_string` and `staging_string` with `~N`, `+N`, `-N` format.
**Suggested Fix:**
```rust
fn format_git_changes(modified: usize, added: usize, deleted: usize) -> String {
let mut parts = Vec::new();
if modified > 0 { parts.push(format!("~{}", modified)); }
if added > 0 { parts.push(format!("+{}", added)); }
if deleted > 0 { parts.push(format!("-{}", deleted)); }
parts.join(" ")
}
```
---
### 21. main.rs: Repeated StatuslineComponent RGB Color Application
**Lines:** 1169-1212
**Estimated Savings:** ~10 lines
**Problem:** Multiple `StatuslineComponent::new(...).rgb_fg(fg_color.0, fg_color.1, fg_color.2)` calls with exact same color.
**Suggested Fix:**
```rust
let with_fg = |text: &str| StatuslineComponent::new(text).rgb_fg(fg_color.0, fg_color.1, fg_color.2);
components.push(with_fg(" "));
components.push(with_fg(&head_text));
```
---
### 22. main.rs: Tab1-Tab9 as Separate Match Arms
**Lines:** 1862-1870
**Estimated Savings:** ~8 lines
**Problem:** Nine separate match arms each calling `self.switch_to_tab(N)`.
**Suggested Fix:**
```rust
impl Action {
fn tab_index(&self) -> Option<usize> {
match self {
Action::Tab1 => Some(0),
Action::Tab2 => Some(1),
// ...
}
}
}
// Then in match:
action if action.tab_index().is_some() => {
self.switch_to_tab(action.tab_index().unwrap());
}
```
---
### 23. keyboard.rs: encode_arrow and encode_f1_f4 Are Identical
**Lines:** 462-485
**Estimated Savings:** ~15 lines
**Problem:** These two methods have identical implementations.
**Suggested Fix:** Remove `encode_f1_f4` and use `encode_arrow` for both, or rename to `encode_ss3_key`.
---
### 24. keyboard.rs: Repeated to_string().as_bytes() Allocations
**Lines:** 356, 365, 372, 378, 466, 479, 490, 493, 554
**Estimated Savings:** Reduced allocations
**Problem:** Multiple places call `.to_string().as_bytes()` on integers.
**Suggested Fix:**
```rust
fn write_u32_to_vec(n: u32, buf: &mut Vec<u8>) {
use std::io::Write;
write!(buf, "{}", n).unwrap();
}
```
Or use `itoa` crate for zero-allocation integer formatting.
---
### 25. graphics.rs: Duplicate AnimationData Construction
**Lines:** 444-456, 623-635
**Estimated Savings:** ~10 lines
**Problem:** Both `decode_gif` and `decode_webm` create identical `AnimationData` structs.
**Suggested Fix:**
```rust
impl AnimationData {
pub fn new(frames: Vec<AnimationFrame>, total_duration_ms: u64) -> Self {
Self {
frames,
current_frame: 0,
frame_start: None,
looping: true,
total_duration_ms,
state: AnimationState::Running,
loops_remaining: None,
}
}
}
```
---
### 26. graphics.rs: Duplicate RGBA Stride Handling
**Lines:** 554-566, 596-607
**Estimated Savings:** ~15 lines
**Problem:** Code for handling RGBA stride appears twice (nearly identical).
**Suggested Fix:**
```rust
fn copy_with_stride(data: &[u8], width: u32, height: u32, stride: usize) -> Vec<u8> {
let row_bytes = (width * 4) as usize;
if stride == row_bytes {
data[..(width * height * 4) as usize].to_vec()
} else {
let mut result = Vec::with_capacity((width * height * 4) as usize);
for row in 0..height as usize {
let start = row * stride;
result.extend_from_slice(&data[start..start + row_bytes]);
}
result
}
}
```
---
### 27. graphics.rs: Duplicate File/TempFile/SharedMemory Reading Logic
**Lines:** 1051-1098, 1410-1489
**Estimated Savings:** ~30 lines
**Problem:** Both `handle_animation_frame` and `store_image` have similar file reading logic.
**Suggested Fix:**
```rust
fn load_transmission_data(&mut self, cmd: &mut GraphicsCommand) -> Result<Vec<u8>, GraphicsError>
```
---
### 28. pty.rs: Duplicate Winsize/ioctl Pattern
**Lines:** 57-67, 170-185
**Estimated Savings:** ~8 lines
**Problem:** Same `libc::winsize` struct creation and `TIOCSWINSZ` ioctl pattern duplicated.
**Suggested Fix:**
```rust
fn set_winsize(fd: RawFd, cols: u16, rows: u16, xpixel: u16, ypixel: u16) -> Result<(), PtyError> {
let winsize = libc::winsize {
ws_row: rows,
ws_col: cols,
ws_xpixel: xpixel,
ws_ypixel: ypixel,
};
let result = unsafe { libc::ioctl(fd, libc::TIOCSWINSZ, &winsize) };
if result == -1 {
Err(PtyError::Io(std::io::Error::last_os_error()))
} else {
Ok(())
}
}
```
---
### 29. vt_parser.rs: Repeated Max Length Check Pattern
**Lines:** 537-541, 693-697, 745-749, 785-789, 831-835
**Estimated Savings:** ~15 lines
**Problem:** This pattern appears 5 times:
```rust
if self.escape_len + X > MAX_ESCAPE_LEN {
log::debug!("... sequence too long, aborting");
self.state = State::Normal;
return consumed;
}
```
**Suggested Fix:**
```rust
#[inline]
fn check_max_len(&mut self, additional: usize) -> bool {
if self.escape_len + additional > MAX_ESCAPE_LEN {
log::debug!("Escape sequence too long, aborting");
self.state = State::Normal;
true
} else {
false
}
}
```
---
## Low Priority
### 30. main.rs: PaneId/TabId Have Identical Implementations
**Lines:** 230-239, 694-703
**Estimated Savings:** ~15 lines
**Problem:** Both use identical `new()` implementations with static atomics.
**Suggested Fix:**
```rust
macro_rules! define_id {
($name:ident) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct $name(u64);
impl $name {
pub fn new() -> Self {
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
Self(NEXT_ID.fetch_add(1, Ordering::Relaxed))
}
}
};
}
define_id!(PaneId);
define_id!(TabId);
```
---
### 31. main.rs: Duplicate "All Tabs Closed" Exit Checks
**Lines:** 2622-2653
**Estimated Savings:** ~5 lines
**Problem:** Two separate checks with same log message in `about_to_wait()`.
**Suggested Fix:** Restructure to have a single exit point after cleanup logic.
---
### 32. terminal.rs: scroll_viewport_up/down Similarity
**Lines:** 885-915
**Estimated Savings:** ~8 lines
**Problem:** Similar structure - both check for alternate screen, calculate offset, set dirty.
**Suggested Fix:** Merge into single method with signed delta parameter.
---
### 33. terminal.rs: screen_alignment Cell Construction Verbose
**Lines:** 1881-1890
**Estimated Savings:** ~6 lines
**Problem:** Constructs Cell with all default values explicitly.
**Suggested Fix:**
```rust
*cell = Cell { character: 'E', ..Cell::default() };
```
---
### 34. keyboard.rs: Repeated UTF-8 Char Encoding Pattern
**Lines:** 503-505, 529-532, 539-541
**Estimated Savings:** ~8 lines
**Problem:** Pattern appears 3 times:
```rust
let mut buf = [0u8; 4];
let s = c.encode_utf8(&mut buf);
return s.as_bytes().to_vec();
```
**Suggested Fix:**
```rust
fn char_to_vec(c: char) -> Vec<u8> {
let mut buf = [0u8; 4];
c.encode_utf8(&mut buf).as_bytes().to_vec()
}
```
---
### 35. config.rs: Tab1-Tab9 as Separate Enum Variants/Fields
**Lines:** 174-182, 214-230
**Estimated Savings:** Structural improvement
**Problem:** Having separate `Tab1`, `Tab2`, ... `Tab9` variants and corresponding struct fields is verbose.
**Suggested Fix:** Use `Tab(u8)` variant and `tab_keys: [Keybind; 9]` array. Note: This changes the JSON config format.
---
### 36. pty.rs: Inconsistent AsRawFd Usage
**Lines:** 63, 177
**Estimated Savings:** Cleanup only
**Problem:** Uses fully-qualified `std::os::fd::AsRawFd::as_raw_fd` despite importing the trait.
**Suggested Fix:**
```rust
// Change from:
let fd = std::os::fd::AsRawFd::as_raw_fd(&master);
// To:
let fd = master.as_raw_fd();
```
---
### 37. pty.rs: Repeated /proc Path Pattern
**Lines:** 222-243
**Estimated Savings:** ~4 lines
**Problem:** Both `foreground_process_name` and `foreground_cwd` build `/proc/{pgid}/...` paths similarly.
**Suggested Fix:**
```rust
fn proc_path(&self, file: &str) -> Option<std::path::PathBuf> {
let pgid = self.foreground_pgid()?;
Some(std::path::PathBuf::from(format!("/proc/{}/{}", pgid, file)))
}
```
+314
View File
@@ -0,0 +1,314 @@
# Renderer.rs Reorganization Analysis
This document analyzes `/src/renderer.rs` (8189 lines) and identifies sections that could be extracted into separate modules for better code organization and maintainability.
## Overview
The `renderer.rs` file is the largest file in the codebase and handles:
- GPU pipeline setup and rendering
- Font loading and glyph caching
- Color/emoji font rendering via FreeType/Cairo
- Box drawing character generation
- Statusline rendering
- Image/Kitty graphics protocol support
- Edge glow effects
---
## Extraction Opportunities
### 1. Color Utilities / Linear Palette
**Lines:** 23-77
**Suggested file:** `src/color.rs` or `src/palette.rs`
**Contents:**
- `LinearPalette` struct
- `srgb_to_linear()` function
- `make_linear_palette()` function
**Dependencies:**
- `crate::config::ColorScheme`
**Complexity:** Low - self-contained utility code with minimal dependencies.
```rust
pub struct LinearPalette {
pub foreground: [f32; 4],
pub background: [f32; 4],
pub cursor: [f32; 4],
pub colors: [[f32; 4]; 256],
}
```
---
### 2. Statusline Types
**Lines:** 123-258
**Suggested file:** `src/statusline.rs`
**Contents:**
- `StatuslineColor` enum
- `StatuslineComponent` struct
- `StatuslineSection` struct
- `StatuslineContent` struct and its `impl` block
**Dependencies:**
- Standard library only
**Complexity:** Low - pure data structures with simple logic.
**Note:** The `parse_ansi_statusline` method (lines 4029-4144) should also move here.
---
### 3. Edge Glow Animation
**Lines:** 260-307
**Suggested file:** `src/effects.rs` or `src/edge_glow.rs`
**Contents:**
- `EdgeGlowSide` enum
- `EdgeGlow` struct
- Animation logic (`update`, `is_active`, `intensity` methods)
**Dependencies:**
- `std::time::Instant`
**Complexity:** Low - isolated animation state machine.
---
### 4. GPU Data Structures
**Lines:** 399-710
**Suggested file:** `src/gpu_types.rs`
**Contents:**
- `GlyphVertex` struct
- `GlowInstance` struct
- `EdgeGlowUniforms` struct
- `ImageUniforms` struct
- `GPUCell` struct
- `SpriteInfo` struct
- `GridParams` struct
- `Quad` struct
- `QuadParams` struct
- `StatuslineParams` struct
- Various constants (`GLYPH_ATLAS_SIZE`, `CELL_INSTANCE_SIZE`, etc.)
**Dependencies:**
- `bytemuck::{Pod, Zeroable}`
**Complexity:** Low - pure data structures with `#[repr(C)]` layouts for GPU compatibility.
---
### 5. Font Loading Helpers
**Lines:** 929-996, 1002-1081, 1382-1565
**Suggested file:** `src/font_loader.rs`
**Contents:**
- `find_font_for_char()` - finds a font file that can render a given character
- `find_color_font_for_char()` - finds color emoji fonts
- `load_font_variant()` - loads a specific font variant (bold, italic, etc.)
- `find_font_family_variants()` - discovers all variants of a font family
- `load_font_family()` - loads an entire font family
**Dependencies:**
- `fontconfig` crate
- `fontdue` crate
- `std::fs`
- `std::collections::HashMap`
**Complexity:** Medium - these are standalone functions but have some interdependencies. Would need to pass fontconfig patterns and settings as parameters.
---
### 6. Color Font Renderer (Emoji)
**Lines:** 1083-1380
**Suggested file:** `src/color_font.rs` or `src/emoji_renderer.rs`
**Contents:**
- `ColorFontRenderer` struct
- FreeType library/face management
- Cairo surface rendering
- Emoji glyph rasterization
**Dependencies:**
- `freetype` crate
- `cairo` crate
- `std::collections::HashMap`
- `std::ptr`
**Complexity:** Medium - self-contained but uses unsafe code and external C libraries. The struct is instantiated inside `Renderer::new()`.
```rust
struct ColorFontRenderer {
library: freetype::Library,
faces: HashMap<String, FontFaceEntry>,
}
```
---
### 7. Box Drawing / Supersampled Canvas
**Lines:** 1567-1943, 4500-5706
**Suggested file:** `src/box_drawing.rs`
**Contents:**
- `Corner` enum
- `SupersampledCanvas` struct
- Canvas rendering methods (lines, arcs, shading, etc.)
- `render_box_char()` method (1200+ lines of box drawing logic)
**Dependencies:**
- Standard library only
**Complexity:** High - the `render_box_char` method is massive (1200+ lines) and handles all Unicode box drawing, block elements, and legacy graphics characters. However, it's functionally isolated.
**Recommendation:** This is the highest-value extraction target. The box drawing code is:
1. Completely self-contained
2. Very large (adds ~2400 lines)
3. Rarely needs modification
4. Easy to test in isolation
---
### 8. Pipeline Builder
**Lines:** 1947-2019
**Suggested file:** `src/pipeline.rs`
**Contents:**
- `PipelineBuilder` struct
- Builder pattern for wgpu render pipelines
**Dependencies:**
- `wgpu` crate
**Complexity:** Low - clean builder pattern, easily extractable.
```rust
struct PipelineBuilder<'a> {
device: &'a wgpu::Device,
label: &'a str,
// ... other fields
}
```
---
### 9. Pane GPU Resources
**Lines:** 105-121, 3710-3785
**Suggested file:** `src/pane_resources.rs`
**Contents:**
- `PaneGpuResources` struct
- Buffer management for per-pane GPU state
- Methods: `new()`, `ensure_grid_capacity()`, `ensure_glyph_capacity()`
**Dependencies:**
- `wgpu` crate
**Complexity:** Medium - the struct is simple but tightly coupled to the `Renderer` for buffer creation. Would need to pass `device` as parameter.
---
### 10. Image Rendering / Kitty Graphics
**Lines:** 7940-8186 (+ `GpuImage` at 483-491)
**Suggested file:** `src/image_renderer.rs`
**Contents:**
- `GpuImage` struct
- `upload_image()` method
- `remove_image()` method
- `sync_images()` method
- `prepare_image_renders()` method
**Dependencies:**
- `wgpu` crate
- `crate::terminal::ImageData`
**Complexity:** Medium - these methods operate on `Renderer` state but could be extracted into a helper struct that holds image-specific GPU resources.
---
## Recommended Extraction Order
Based on complexity and value, here's a suggested order:
| Priority | Module | Lines Saved | Complexity | Value |
|----------|--------|-------------|------------|-------|
| 1 | `box_drawing.rs` | ~2400 | High | Very High |
| 2 | `gpu_types.rs` | ~310 | Low | High |
| 3 | `color_font.rs` | ~300 | Medium | High |
| 4 | `font_loader.rs` | ~330 | Medium | Medium |
| 5 | `statusline.rs` | ~250 | Low | Medium |
| 6 | `pipeline.rs` | ~75 | Low | Medium |
| 7 | `color.rs` | ~55 | Low | Low |
| 8 | `edge_glow.rs` | ~50 | Low | Low |
| 9 | `pane_resources.rs` | ~80 | Medium | Medium |
| 10 | `image_renderer.rs` | ~250 | Medium | Medium |
---
## Implementation Notes
### The Renderer Struct (Lines 712-927)
The main `Renderer` struct ties everything together and would remain in `renderer.rs`. After extraction, it would:
1. Import types from the new modules
2. Potentially hold instances of extracted helper structs (e.g., `ColorFontRenderer`, `ImageRenderer`)
3. Still contain the core rendering logic (`render()`, `prepare_pane_data()`, etc.)
### Module Structure
After refactoring, the structure might look like:
```
src/
├── renderer/
│ ├── mod.rs # Main Renderer struct and render logic
│ ├── box_drawing.rs # SupersampledCanvas + render_box_char
│ ├── color_font.rs # ColorFontRenderer for emoji
│ ├── font_loader.rs # Font discovery and loading
│ ├── gpu_types.rs # GPU data structures
│ ├── image.rs # Kitty graphics support
│ ├── pipeline.rs # PipelineBuilder
│ ├── statusline.rs # Statusline types and parsing
│ └── effects.rs # EdgeGlow and other effects
├── color.rs # LinearPalette (or keep in renderer/)
└── ...
```
### Challenges
1. **Circular dependencies:** The `Renderer` struct is used throughout. Extracted modules should receive what they need via parameters, not by importing `Renderer`.
2. **GPU resources:** Many extracted components need `&wgpu::Device` and `&wgpu::Queue`. These should be passed as parameters rather than stored.
3. **Method extraction:** Some methods like `render_box_char` are currently `impl Renderer` methods. They'd need to become standalone functions or methods on the extracted structs.
4. **Testing:** Extracted modules will be easier to unit test, which is a significant benefit.
---
## Quick Wins
These can be extracted with minimal refactoring:
1. **`gpu_types.rs`** - Just move the structs and constants, add `pub use` in renderer
2. **`color.rs`** - Move `LinearPalette` and helper functions
3. **`pipeline.rs`** - Move `PipelineBuilder` as-is
4. **`edge_glow.rs`** - Move `EdgeGlow` and related types
---
## Conclusion
The `renderer.rs` file is doing too much. Extracting the identified modules would:
- Reduce `renderer.rs` from ~8200 lines to ~4000-4500 lines
- Improve code organization and discoverability
- Enable better unit testing of isolated components
- Make the codebase more approachable for new contributors
The highest-impact extraction is `box_drawing.rs`, which alone would remove ~2400 lines of self-contained code.
+356
View File
@@ -0,0 +1,356 @@
# Terminal.rs Reorganization Plan
This document identifies sections of `src/terminal.rs` (2336 lines) that could be extracted into separate files to improve code organization, maintainability, and testability.
---
## Summary of Proposed Extractions
| New File | Lines | Primary Contents |
|----------|-------|------------------|
| `src/cell.rs` | ~90 | `Cell`, `Color`, `CursorShape` |
| `src/color.rs` | ~115 | `ColorPalette` and color parsing |
| `src/mouse.rs` | ~110 | `MouseTrackingMode`, `MouseEncoding`, `encode_mouse()` |
| `src/scrollback.rs` | ~100 | `ScrollbackBuffer` ring buffer |
| `src/terminal_commands.rs` | ~30 | `TerminalCommand`, `Direction` |
| `src/stats.rs` | ~45 | `ProcessingStats` |
**Total extractable: ~490 lines** (reducing terminal.rs by ~21%)
---
## 1. Cell Types and Color Enum
**Lines:** 30-87 (Cell, Color, CursorShape)
**Contents:**
- `Cell` struct (lines 31-45) - terminal grid cell with character and attributes
- `impl Default for Cell` (lines 47-60)
- `Color` enum (lines 63-69) - `Default`, `Rgb(u8, u8, u8)`, `Indexed(u8)`
- `CursorShape` enum (lines 72-87) - cursor style variants
**Proposed file:** `src/cell.rs`
**Dependencies:**
```rust
// No external dependencies - these are self-contained types
```
**Exports needed by terminal.rs:**
```rust
pub use cell::{Cell, Color, CursorShape};
```
**Challenges:**
- None. These are simple data types with no logic dependencies.
**Benefits:**
- `Cell` and `Color` are referenced by renderer and could be imported directly
- Makes the core data structures discoverable
- Easy to test color conversion independently
---
## 2. Color Palette
**Lines:** 119-240
**Contents:**
- `ColorPalette` struct (lines 120-128) - 256-color palette storage
- `impl Default for ColorPalette` (lines 130-177) - ANSI + 216 color cube + grayscale
- `ColorPalette::parse_color_spec()` (lines 181-209) - parse `#RRGGBB` and `rgb:RR/GG/BB`
- `ColorPalette::to_rgba()` (lines 212-224) - foreground color conversion
- `ColorPalette::to_rgba_bg()` (lines 227-239) - background color conversion
**Proposed file:** `src/color.rs`
**Dependencies:**
```rust
use crate::cell::Color; // For Color enum
```
**Challenges:**
- Depends on `Color` enum (extract cell.rs first)
- `to_rgba()` and `to_rgba_bg()` are called by the renderer
**Benefits:**
- Color parsing logic is self-contained and testable
- Palette initialization is complex (color cube math) and benefits from isolation
- Could add color scheme loading from config files in the future
---
## 3. Mouse Tracking and Encoding
**Lines:** 89-117, 962-1059
**Contents:**
- `MouseTrackingMode` enum (lines 90-103)
- `MouseEncoding` enum (lines 106-117)
- `Terminal::encode_mouse()` method (lines 964-1059) - encode mouse events for PTY
**Proposed file:** `src/mouse.rs`
**Dependencies:**
```rust
// MouseTrackingMode and MouseEncoding are self-contained enums
// encode_mouse() would need to be a standalone function or trait
```
**Refactoring approach:**
```rust
// In mouse.rs
pub fn encode_mouse_event(
tracking: MouseTrackingMode,
encoding: MouseEncoding,
button: u8,
col: u16,
row: u16,
pressed: bool,
is_motion: bool,
modifiers: u8,
) -> Vec<u8>
```
**Challenges:**
- `encode_mouse()` is currently a method on `Terminal` but only reads `mouse_tracking` and `mouse_encoding`
- Need to change call sites to pass mode/encoding explicitly, OR keep as Terminal method but move enums
**Benefits:**
- Mouse protocol logic is self-contained and well-documented
- Could add unit tests for X10, SGR, URXVT encoding without instantiating Terminal
---
## 4. Scrollback Buffer
**Lines:** 314-425
**Contents:**
- `ScrollbackBuffer` struct (lines 326-335) - Kitty-style ring buffer
- `ScrollbackBuffer::new()` (lines 340-351) - lazy allocation
- `ScrollbackBuffer::len()`, `is_empty()`, `is_full()` (lines 354-369)
- `ScrollbackBuffer::push()` (lines 377-403) - O(1) ring buffer insertion
- `ScrollbackBuffer::get()` (lines 407-415) - logical index access
- `ScrollbackBuffer::clear()` (lines 419-424)
**Proposed file:** `src/scrollback.rs`
**Dependencies:**
```rust
use crate::cell::Cell; // For Vec<Cell> line storage
```
**Challenges:**
- Depends on `Cell` type (extract cell.rs first)
- Otherwise completely self-contained with great documentation
**Benefits:**
- Ring buffer is a reusable data structure
- Excellent candidate for unit testing (push, wrap-around, get by index)
- Performance-critical code that benefits from isolation for profiling
---
## 5. Terminal Commands
**Lines:** 8-28
**Contents:**
- `TerminalCommand` enum (lines 10-19) - commands sent from terminal to application
- `Direction` enum (lines 22-28) - navigation direction
**Proposed file:** `src/terminal_commands.rs`
**Dependencies:**
```rust
// No dependencies - self-contained enums
```
**Challenges:**
- Very small extraction, but cleanly separates protocol from implementation
**Benefits:**
- Defines the terminal-to-application interface
- Could grow as more custom OSC commands are added
- Clear documentation of what commands exist
---
## 6. Processing Stats
**Lines:** 268-312
**Contents:**
- `ProcessingStats` struct (lines 269-288) - timing/debugging statistics
- `ProcessingStats::reset()` (lines 291-293)
- `ProcessingStats::log_if_slow()` (lines 295-311) - conditional performance logging
**Proposed file:** `src/stats.rs`
**Dependencies:**
```rust
// No dependencies - uses only log crate
```
**Challenges:**
- Only used for debugging/profiling
- Could be feature-gated behind a `profiling` feature flag
**Benefits:**
- Separates debug instrumentation from core logic
- Could be conditionally compiled out for release builds
---
## 7. Saved Cursor and Alternate Screen (Keep in terminal.rs)
**Lines:** 243-265
**Contents:**
- `SavedCursor` struct (lines 243-253)
- `AlternateScreen` struct (lines 256-265)
**Recommendation:** Keep in terminal.rs
**Rationale:**
- These are private implementation details of cursor save/restore and alternate screen
- Tightly coupled to Terminal's internal state
- No benefit from extraction
---
## 8. Handler Implementation (Keep in terminal.rs)
**Lines:** 1236-1904, 1906-2335
**Contents:**
- `impl Handler for Terminal` - VT parser callback implementations
- CSI handling, SGR parsing, DEC private modes
- Keyboard protocol, OSC handling, graphics protocol
**Recommendation:** Keep in terminal.rs (or consider splitting if file grows further)
**Rationale:**
- These are the core terminal emulation callbacks
- Heavily intertwined with Terminal's internal state
- Could potentially split into `terminal_csi.rs`, `terminal_sgr.rs`, etc. but adds complexity
---
## Recommended Extraction Order
1. **`src/cell.rs`** - No dependencies, foundational types
2. **`src/terminal_commands.rs`** - No dependencies, simple enums
3. **`src/stats.rs`** - No dependencies, debugging utility
4. **`src/color.rs`** - Depends on `cell.rs`, self-contained logic
5. **`src/scrollback.rs`** - Depends on `cell.rs`, self-contained data structure
6. **`src/mouse.rs`** - Self-contained enums, may need refactoring for encode function
---
## Example: cell.rs Implementation
```rust
//! Terminal cell and color types.
/// A single cell in the terminal grid.
#[derive(Clone, Copy, Debug)]
pub struct Cell {
pub character: char,
pub fg_color: Color,
pub bg_color: Color,
pub bold: bool,
pub italic: bool,
/// Underline style: 0=none, 1=single, 2=double, 3=curly, 4=dotted, 5=dashed
pub underline_style: u8,
/// Strikethrough decoration
pub strikethrough: bool,
/// If true, this cell is the continuation of a wide (double-width) character.
pub wide_continuation: bool,
}
impl Default for Cell {
fn default() -> Self {
Self {
character: ' ',
fg_color: Color::Default,
bg_color: Color::Default,
bold: false,
italic: false,
underline_style: 0,
strikethrough: false,
wide_continuation: false,
}
}
}
/// Terminal colors.
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum Color {
#[default]
Default,
Rgb(u8, u8, u8),
Indexed(u8),
}
/// Cursor shape styles (DECSCUSR).
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum CursorShape {
#[default]
BlinkingBlock,
SteadyBlock,
BlinkingUnderline,
SteadyUnderline,
BlinkingBar,
SteadyBar,
}
```
---
## Impact on lib.rs
After extraction, `lib.rs` would need:
```rust
pub mod cell;
pub mod color;
pub mod mouse;
pub mod scrollback;
pub mod stats;
pub mod terminal;
pub mod terminal_commands;
// ... existing modules ...
```
And `terminal.rs` would add:
```rust
use crate::cell::{Cell, Color, CursorShape};
use crate::color::ColorPalette;
use crate::mouse::{MouseEncoding, MouseTrackingMode};
use crate::scrollback::ScrollbackBuffer;
use crate::stats::ProcessingStats;
use crate::terminal_commands::{Direction, TerminalCommand};
```
---
## Testing Opportunities
Extracting these modules enables focused unit tests:
- **cell.rs**: Default cell values, wide_continuation handling
- **color.rs**: `parse_color_spec()` for various formats, palette indexing, RGBA conversion
- **mouse.rs**: Encoding tests for X10, SGR, URXVT formats, tracking mode filtering
- **scrollback.rs**: Ring buffer push/get/wrap, capacity limits, clear behavior
---
## Notes
- The `Terminal` struct itself (lines 428-512) should remain in `terminal.rs` as the central state container
- Private helper structs like `SavedCursor` and `AlternateScreen` should stay in `terminal.rs`
- The `Handler` trait implementation spans ~600 lines but is core terminal logic
- Consider feature-gating `ProcessingStats` behind `#[cfg(feature = "profiling")]`
+295
View File
@@ -0,0 +1,295 @@
# VT Parser Reorganization Recommendations
This document analyzes `src/vt_parser.rs` (1033 lines) and identifies sections that could be extracted into separate files to improve code organization, testability, and maintainability.
## Current File Structure Overview
| Lines | Section | Description |
|-------|---------|-------------|
| 1-49 | Constants & UTF-8 Tables | Parser limits, UTF-8 DFA decode table |
| 51-133 | UTF-8 Decoder | `Utf8Decoder` struct and implementation |
| 135-265 | State & CSI Types | `State` enum, `CsiState` enum, `CsiParams` struct |
| 267-832 | Parser Core | Main `Parser` struct with all parsing logic |
| 835-906 | Handler Trait | `Handler` trait definition |
| 908-1032 | Tests | Unit tests |
---
## Recommended Extractions
### 1. UTF-8 Decoder Module
**File:** `src/utf8_decoder.rs`
**Lines:** 27-133
**Components:**
- `UTF8_ACCEPT`, `UTF8_REJECT` constants (lines 28-29)
- `UTF8_DECODE_TABLE` static (lines 33-49)
- `decode_utf8()` function (lines 52-62)
- `Utf8Decoder` struct and impl (lines 66-133)
- `REPLACEMENT_CHAR` constant (line 25)
**Dependencies:**
- None (completely self-contained)
**Rationale:**
- This is a completely standalone UTF-8 DFA decoder based on Bjoern Hoehrmann's design
- Zero dependencies on the rest of the parser
- Could be reused in other parts of the codebase (keyboard input, file parsing)
- Independently testable
- ~100 lines, a good size for a focused module
**Extraction Difficulty:** Easy
**Example structure:**
```rust
// src/utf8_decoder.rs
pub const REPLACEMENT_CHAR: char = '\u{FFFD}';
const UTF8_ACCEPT: u8 = 0;
const UTF8_REJECT: u8 = 12;
static UTF8_DECODE_TABLE: [u8; 364] = [ /* ... */ ];
#[inline]
fn decode_utf8(state: &mut u8, codep: &mut u32, byte: u8) -> u8 { /* ... */ }
#[derive(Debug, Default)]
pub struct Utf8Decoder { /* ... */ }
impl Utf8Decoder {
pub fn new() -> Self { /* ... */ }
pub fn reset(&mut self) { /* ... */ }
pub fn decode_to_esc(&mut self, src: &[u8], output: &mut Vec<char>) -> (usize, bool) { /* ... */ }
}
```
---
### 2. CSI Parameters Module
**File:** `src/csi_params.rs`
**Lines:** 14-265 (constants and CSI-related types)
**Components:**
- `MAX_CSI_PARAMS` constant (line 15)
- `CsiState` enum (lines 165-171)
- `CsiParams` struct and impl (lines 174-265)
**Dependencies:**
- None (self-contained data structure)
**Rationale:**
- `CsiParams` is a self-contained data structure for CSI parameter parsing
- Has its own sub-state machine (`CsiState`)
- The struct is 2KB+ in size due to the arrays - isolating it makes the size impact clearer
- Could be tested independently for parameter parsing edge cases
- The `get()`, `add_digit()`, `commit_param()` methods form a cohesive unit
**Extraction Difficulty:** Easy
**Note:** `CsiState` is currently private and only used within CSI parsing. It should remain private to the module.
---
### 3. Handler Trait Module
**File:** `src/vt_handler.rs`
**Lines:** 835-906
**Components:**
- `Handler` trait (lines 840-906)
- `CsiParams` would need to be re-exported or the trait would depend on `csi_params` module
**Dependencies:**
- `CsiParams` type (for `csi()` method signature)
**Rationale:**
- Clear separation between the parser implementation and the callback interface
- Makes it easier for consumers to implement handlers without pulling in parser internals
- Trait documentation is substantial and benefits from its own file
- Allows different modules to implement handlers without circular dependencies
**Extraction Difficulty:** Easy (after `CsiParams` is extracted)
---
### 4. Parser Constants Module
**File:** `src/vt_constants.rs` (or inline in a `mod.rs` approach)
**Lines:** 14-25
**Components:**
- `MAX_CSI_PARAMS` (already mentioned above)
- `MAX_OSC_LEN` (line 19)
- `MAX_ESCAPE_LEN` (line 22)
- `REPLACEMENT_CHAR` (line 25, if not moved to utf8_decoder)
**Dependencies:**
- None
**Rationale:**
- Centralizes magic numbers
- Easy to find and adjust limits
- However, these are only 4 constants, so this extraction is optional
**Extraction Difficulty:** Trivial
**Recommendation:** Keep these in the main parser file or move to a `mod.rs` if using a directory structure.
---
### 5. Parser State Enum
**File:** Could remain in `vt_parser.rs` or move to `vt_handler.rs`
**Lines:** 136-162
**Components:**
- `State` enum (lines 136-156)
- `Default` impl (lines 158-162)
**Dependencies:**
- None
**Rationale:**
- The `State` enum is public and part of the `Parser` struct
- It's tightly coupled with the parser's operation
- Small enough (~25 lines) to not warrant its own file
**Recommendation:** Keep in main parser file or combine with handler trait.
---
## Proposed Directory Structure
### Option A: Flat Module Structure (Recommended)
```
src/
vt_parser.rs # Main Parser struct, State enum, parsing logic (~700 lines)
utf8_decoder.rs # UTF-8 DFA decoder (~110 lines)
csi_params.rs # CsiParams struct and CsiState (~100 lines)
vt_handler.rs # Handler trait (~75 lines)
```
**lib.rs changes:**
```rust
mod utf8_decoder;
mod csi_params;
mod vt_handler;
mod vt_parser;
pub use vt_parser::{Parser, State};
pub use csi_params::{CsiParams, MAX_CSI_PARAMS};
pub use vt_handler::Handler;
```
### Option B: Directory Module Structure
```
src/
vt_parser/
mod.rs # Re-exports and constants
parser.rs # Main Parser struct
utf8.rs # UTF-8 decoder
csi.rs # CSI params
handler.rs # Handler trait
tests.rs # Tests (optional, can stay inline)
```
---
## Extraction Priority
| Priority | Module | Lines Saved | Benefit |
|----------|--------|-------------|---------|
| 1 | `utf8_decoder.rs` | ~110 | Completely independent, reusable |
| 2 | `csi_params.rs` | ~100 | Clear data structure boundary |
| 3 | `vt_handler.rs` | ~75 | Cleaner API surface |
| 4 | Constants | ~10 | Optional, low impact |
---
## Challenges and Considerations
### 1. Test Organization
- Lines 908-1032 contain tests that use private test helpers (`TestHandler`)
- If the `Handler` trait is extracted, `TestHandler` could move to a test module
- Consider using `#[cfg(test)]` modules in each file
### 2. Circular Dependencies
- `Handler` trait references `CsiParams` - extract `CsiParams` first
- `Parser` uses both `Utf8Decoder` and `CsiParams` - both should be extracted before any handler extraction
### 3. Public API Surface
- Currently public: `MAX_CSI_PARAMS`, `State`, `CsiParams`, `Parser`, `Handler`, `Utf8Decoder`
- After extraction, ensure re-exports maintain the same public API
### 4. Performance Considerations
- The UTF-8 decoder uses `#[inline]` extensively - ensure this is preserved
- `CsiParams::reset()` is hot and optimized to avoid memset - document this
---
## Migration Steps
1. **Extract `utf8_decoder.rs`**
- Move lines 25-133 to new file
- Add `mod utf8_decoder;` to lib.rs
- Update `vt_parser.rs` to `use crate::utf8_decoder::Utf8Decoder;`
2. **Extract `csi_params.rs`**
- Move lines 14-15 (MAX_CSI_PARAMS) and 164-265 to new file
- Make `CsiState` private to the module (`pub(crate)` at most)
- Add `mod csi_params;` to lib.rs
3. **Extract `vt_handler.rs`**
- Move lines 835-906 to new file
- Add `use crate::csi_params::CsiParams;`
- Add `mod vt_handler;` to lib.rs
4. **Update imports in `vt_parser.rs`**
```rust
use crate::utf8_decoder::Utf8Decoder;
use crate::csi_params::{CsiParams, CsiState, MAX_CSI_PARAMS};
use crate::vt_handler::Handler;
```
5. **Verify public API unchanged**
- Ensure lib.rs re-exports all previously public items
- Run tests to verify nothing broke
---
## Code That Should Stay in `vt_parser.rs`
The following should remain in the main parser file:
- `State` enum (lines 136-162) - tightly coupled to parser
- `Parser` struct (lines 268-299) - core type
- All `Parser` methods (lines 301-832) - core parsing logic
- Constants `MAX_OSC_LEN`, `MAX_ESCAPE_LEN` (lines 19, 22) - parser-specific limits
After extraction, `vt_parser.rs` would be ~700 lines focused purely on the state machine and escape sequence parsing logic.
---
## Summary
The `vt_parser.rs` file has clear natural boundaries:
1. **UTF-8 decoding** - completely standalone, based on external algorithm
2. **CSI parameter handling** - self-contained data structure with its own state
3. **Handler trait** - defines the callback interface
4. **Core parser** - the state machine and escape sequence processing
Extracting the first three would reduce `vt_parser.rs` from 1033 lines to ~700 lines while improving:
- Code navigation
- Testability of individual components
- Reusability of the UTF-8 decoder
- API clarity (handler trait in its own file)
+134 -27
View File
@@ -1,36 +1,143 @@
use zterm::terminal::Terminal; use zterm::terminal::Terminal;
use zterm::vt_parser::Parser;
use std::time::Instant; use std::time::Instant;
use std::io::Write; use std::io::Write;
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()]);
}
}
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() { fn main() {
// Generate seq 1 100000 output println!("=== ZTerm VT Parser Benchmark ===");
let mut data = Vec::new(); println!("Matching Kitty's kitten __benchmark__ methodology\n");
for i in 1..=100000 {
writeln!(&mut data, "{}", i).unwrap();
}
println!("Data size: {} bytes", data.len());
// Test with different terminal sizes to see scroll impact // Benchmark 1: Only ASCII chars (matches Kitty's simple_ascii)
for rows in [24, 100, 1000] { println!("--- Only ASCII chars ---");
let mut terminal = Terminal::new(80, rows, 10000); let target_sz = 1024 * 2048 + 13;
let start = Instant::now(); let mut rng: u64 = 12345;
terminal.process(&data); let mut ascii_data = Vec::with_capacity(target_sz);
let elapsed = start.elapsed(); let alphabet = [ASCII_PRINTABLE, CONTROL_CHARS].concat();
println!("Terminal {}x{}: {:?} ({:.2} MB/s)", for _ in 0..target_sz {
80, rows, rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1);
elapsed, let idx = ((rng >> 33) % alphabet.len() as u64) as usize;
(data.len() as f64 / 1024.0 / 1024.0) / elapsed.as_secs_f64() ascii_data.push(alphabet[idx]);
);
} }
// Test with scrollback disabled run_benchmark("Only ASCII chars", &ascii_data, REPETITIONS, || {
println!("\nWith scrollback disabled:"); (Terminal::new(80, 25, 20000), Parser::new())
let mut terminal = Terminal::new(80, 24, 0); });
let start = Instant::now();
terminal.process(&data); // Benchmark 2: CSI codes with few chars (matches Kitty's ascii_with_csi)
let elapsed = start.elapsed(); println!("\n--- CSI codes with few chars ---");
println!("Terminal 80x24, no scrollback: {:?} ({:.2} MB/s)", let target_sz = 1024 * 1024 + 17;
elapsed, let mut csi_data = Vec::with_capacity(target_sz + 100);
(data.len() as f64 / 1024.0 / 1024.0) / elapsed.as_secs_f64() 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
View File
File diff suppressed because it is too large Load Diff
+57
View File
@@ -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())
}
}
+398
View File
@@ -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))
}
}
+98 -127
View File
@@ -52,11 +52,15 @@ impl Keybind {
let key_part = &lowercase[last_plus + 1..]; let key_part = &lowercase[last_plus + 1..];
let mod_part = &lowercase[..last_plus]; let mod_part = &lowercase[..last_plus];
// Normalize symbol names to actual characters // 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) (mod_part, key)
} else { } else {
// No modifiers, just a key // 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) ("", key)
}; };
@@ -86,77 +90,78 @@ impl Keybind {
/// Normalizes key names to their canonical form. /// Normalizes key names to their canonical form.
/// Supports both symbol names ("plus", "minus") and literal symbols ("+", "-"). /// Supports both symbol names ("plus", "minus") and literal symbols ("+", "-").
fn normalize_key_name(name: &str) -> String { /// Returns a static str for known keys, None for unknown (caller uses input).
match name { fn normalize_key_name(name: &str) -> Option<&'static str> {
Some(match name {
// Arrow keys // Arrow keys
"left" | "arrowleft" | "arrow_left" => "left".to_string(), "left" | "arrowleft" | "arrow_left" => "left",
"right" | "arrowright" | "arrow_right" => "right".to_string(), "right" | "arrowright" | "arrow_right" => "right",
"up" | "arrowup" | "arrow_up" => "up".to_string(), "up" | "arrowup" | "arrow_up" => "up",
"down" | "arrowdown" | "arrow_down" => "down".to_string(), "down" | "arrowdown" | "arrow_down" => "down",
// Other special keys // Other special keys
"enter" | "return" => "enter".to_string(), "enter" | "return" => "enter",
"tab" => "tab".to_string(), "tab" => "tab",
"escape" | "esc" => "escape".to_string(), "escape" | "esc" => "escape",
"backspace" | "back" => "backspace".to_string(), "backspace" | "back" => "backspace",
"delete" | "del" => "delete".to_string(), "delete" | "del" => "delete",
"insert" | "ins" => "insert".to_string(), "insert" | "ins" => "insert",
"home" => "home".to_string(), "home" => "home",
"end" => "end".to_string(), "end" => "end",
"pageup" | "page_up" | "pgup" => "pageup".to_string(), "pageup" | "page_up" | "pgup" => "pageup",
"pagedown" | "page_down" | "pgdn" => "pagedown".to_string(), "pagedown" | "page_down" | "pgdn" => "pagedown",
// Function keys // Function keys
"f1" => "f1".to_string(), "f1" => "f1",
"f2" => "f2".to_string(), "f2" => "f2",
"f3" => "f3".to_string(), "f3" => "f3",
"f4" => "f4".to_string(), "f4" => "f4",
"f5" => "f5".to_string(), "f5" => "f5",
"f6" => "f6".to_string(), "f6" => "f6",
"f7" => "f7".to_string(), "f7" => "f7",
"f8" => "f8".to_string(), "f8" => "f8",
"f9" => "f9".to_string(), "f9" => "f9",
"f10" => "f10".to_string(), "f10" => "f10",
"f11" => "f11".to_string(), "f11" => "f11",
"f12" => "f12".to_string(), "f12" => "f12",
// Symbol name aliases // Symbol name aliases
"plus" => "+".to_string(), "plus" => "+",
"minus" => "-".to_string(), "minus" => "-",
"equal" | "equals" => "=".to_string(), "equal" | "equals" => "=",
"bracket_left" | "bracketleft" | "lbracket" => "[".to_string(), "bracket_left" | "bracketleft" | "lbracket" => "[",
"bracket_right" | "bracketright" | "rbracket" => "]".to_string(), "bracket_right" | "bracketright" | "rbracket" => "]",
"brace_left" | "braceleft" | "lbrace" => "{".to_string(), "brace_left" | "braceleft" | "lbrace" => "{",
"brace_right" | "braceright" | "rbrace" => "}".to_string(), "brace_right" | "braceright" | "rbrace" => "}",
"semicolon" => ";".to_string(), "semicolon" => ";",
"colon" => ":".to_string(), "colon" => ":",
"apostrophe" | "quote" => "'".to_string(), "apostrophe" | "quote" => "'",
"quotedbl" | "doublequote" => "\"".to_string(), "quotedbl" | "doublequote" => "\"",
"comma" => ",".to_string(), "comma" => ",",
"period" | "dot" => ".".to_string(), "period" | "dot" => ".",
"slash" => "/".to_string(), "slash" => "/",
"backslash" => "\\".to_string(), "backslash" => "\\",
"grave" | "backtick" => "`".to_string(), "grave" | "backtick" => "`",
"tilde" => "~".to_string(), "tilde" => "~",
"at" => "@".to_string(), "at" => "@",
"hash" | "pound" => "#".to_string(), "hash" | "pound" => "#",
"dollar" => "$".to_string(), "dollar" => "$",
"percent" => "%".to_string(), "percent" => "%",
"caret" => "^".to_string(), "caret" => "^",
"ampersand" => "&".to_string(), "ampersand" => "&",
"asterisk" | "star" => "*".to_string(), "asterisk" | "star" => "*",
"paren_left" | "parenleft" | "lparen" => "(".to_string(), "paren_left" | "parenleft" | "lparen" => "(",
"paren_right" | "parenright" | "rparen" => ")".to_string(), "paren_right" | "parenright" | "rparen" => ")",
"underscore" => "_".to_string(), "underscore" => "_",
"pipe" | "bar" => "|".to_string(), "pipe" | "bar" => "|",
"question" => "?".to_string(), "question" => "?",
"exclam" | "exclamation" | "bang" => "!".to_string(), "exclam" | "exclamation" | "bang" => "!",
"less" | "lessthan" => "<".to_string(), "less" | "lessthan" => "<",
"greater" | "greaterthan" => ">".to_string(), "greater" | "greaterthan" => ">",
"space" => " ".to_string(), "space" => " ",
// Pass through everything else as-is // Unknown - caller handles passthrough
_ => name.to_string(), _ => return None,
} })
} }
} }
@@ -281,68 +286,34 @@ impl Keybindings {
pub fn build_action_map(&self) -> HashMap<(bool, bool, bool, bool, String), Action> { pub fn build_action_map(&self) -> HashMap<(bool, bool, bool, bool, String), Action> {
let mut map = HashMap::new(); let mut map = HashMap::new();
if let Some(parsed) = self.new_tab.parse() { let bindings: &[(&Keybind, Action)] = &[
map.insert(parsed, Action::NewTab); (&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);
} }
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);
} }
map map
+55
View File
@@ -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
}
}
+301
View File
@@ -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");
}
+6 -6
View File
@@ -88,7 +88,7 @@ fn vs_main(in: VertexInput) -> VertexOutput {
} }
@group(0) @binding(0) @group(0) @binding(0)
var atlas_texture: texture_2d_array<f32>; var atlas_textures: binding_array<texture_2d<f32>>;
@group(0) @binding(1) @group(0) @binding(1)
var atlas_sampler: sampler; 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) // 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 // 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. // 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 { } else {
// Non-block cursors (bar, underline) - sample from pre-rendered cursor sprite // Non-block cursors (bar, underline) - sample from pre-rendered cursor sprite
// The cursor_uv was calculated in the vertex shader // 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; let cursor_alpha = cursor_sample.a;
if cursor_alpha > 0.0 { 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; let has_glyph = in.uv.x != 0.0 || in.uv.y != 0.0;
if has_glyph { 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 { if in.is_colored_glyph == 1u {
// Colored glyph (emoji) - use atlas color directly // 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 // Sample and blend underline decoration if present
if in.has_underline > 0u { 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; let underline_alpha = underline_sample.a;
if underline_alpha > 0.0 { 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 // Sample and blend strikethrough decoration if present
if in.has_strikethrough > 0u { 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; let strike_alpha = strike_sample.a;
if strike_alpha > 0.0 { if strike_alpha > 0.0 {
+291
View File
@@ -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],
}
+347
View File
@@ -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
View File
@@ -2,10 +2,21 @@
//! //!
//! Single-process architecture: one process owns PTY, terminal state, and rendering. //! 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 config;
pub mod font_loader;
pub mod edge_glow;
pub mod gpu_types;
pub mod graphics; pub mod graphics;
pub mod image_renderer;
pub mod keyboard; pub mod keyboard;
pub mod pane_resources;
pub mod pipeline;
pub mod pty; pub mod pty;
pub mod renderer; pub mod renderer;
pub mod statusline;
pub mod terminal; pub mod terminal;
pub mod simd_utf8;
pub mod vt_parser; pub mod vt_parser;
+556 -526
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -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,
}
+79
View File
@@ -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,
})
}
}
+457 -3917
View File
File diff suppressed because it is too large Load Diff
+1476
View File
File diff suppressed because it is too large Load Diff
+137
View File
@@ -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())
}
}
+3 -3
View File
@@ -103,7 +103,7 @@ struct ColorTable {
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
@group(0) @binding(0) @group(0) @binding(0)
var atlas_texture: texture_2d_array<f32>; var atlas_textures: binding_array<texture_2d<f32>>;
@group(0) @binding(1) @group(0) @binding(1)
var atlas_sampler: sampler; var atlas_sampler: sampler;
@@ -312,8 +312,8 @@ fn fs_statusline(in: VertexOutput) -> @location(0) vec4<f32> {
return in.bg_color; return in.bg_color;
} }
// Sample glyph from atlas (using layer for texture array) // Sample glyph from atlas (using layer to index texture array)
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 { if in.is_colored_glyph == 1u {
// Colored glyph (emoji) - use atlas color directly // Colored glyph (emoji) - use atlas color directly
+314 -307
View File
@@ -2,7 +2,7 @@
use crate::graphics::{GraphicsCommand, ImageStorage}; 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};
use unicode_width::UnicodeWidthChar; use unicode_width::UnicodeWidthChar;
/// Commands that the terminal can send to the application. /// Commands that the terminal can send to the application.
@@ -265,50 +265,80 @@ struct AlternateScreen {
} }
/// Timing stats for performance debugging. /// Timing stats for performance debugging.
/// Only populated when the `render_timing` feature is enabled.
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct ProcessingStats { pub struct ProcessingStats {
#[cfg(feature = "render_timing")]
/// Total time spent in scroll_up operations (nanoseconds). /// Total time spent in scroll_up operations (nanoseconds).
pub scroll_up_ns: u64, pub scroll_up_ns: u64,
#[cfg(feature = "render_timing")]
/// Number of scroll_up calls. /// Number of scroll_up calls.
pub scroll_up_count: u32, pub scroll_up_count: u32,
#[cfg(feature = "render_timing")]
/// Total time spent in scrollback operations (nanoseconds). /// Total time spent in scrollback operations (nanoseconds).
pub scrollback_ns: u64, pub scrollback_ns: u64,
#[cfg(feature = "render_timing")]
/// Time in VecDeque pop_front. /// Time in VecDeque pop_front.
pub pop_front_ns: u64, pub pop_front_ns: u64,
#[cfg(feature = "render_timing")]
/// Time in VecDeque push_back. /// Time in VecDeque push_back.
pub push_back_ns: u64, pub push_back_ns: u64,
#[cfg(feature = "render_timing")]
/// Time in mem::swap. /// Time in mem::swap.
pub swap_ns: u64, pub swap_ns: u64,
#[cfg(feature = "render_timing")]
/// Total time spent in line clearing (nanoseconds). /// Total time spent in line clearing (nanoseconds).
pub clear_line_ns: u64, pub clear_line_ns: u64,
#[cfg(feature = "render_timing")]
/// Total time spent in text handler (nanoseconds). /// Total time spent in text handler (nanoseconds).
pub text_handler_ns: u64, 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. /// Number of characters processed.
pub chars_processed: u32, 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 { impl ProcessingStats {
#[cfg(feature = "render_timing")]
pub fn reset(&mut self) { pub fn reset(&mut self) {
*self = Self::default(); *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) { 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 { 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!( 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={}", "[PARSE_DETAIL] text={:.2}ms ({}chars) csi={:.2}ms ({}x) vt_only={:.2}ms ({}calls) scroll={:.2}ms ({}x)",
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,
self.text_handler_ns as f64 / 1_000_000.0, self.text_handler_ns as f64 / 1_000_000.0,
self.chars_processed, self.chars_processed,
self.csi_handler_ns as f64 / 1_000_000.0,
self.csi_count,
vt_only_ns as f64 / 1_000_000.0,
self.consume_input_count,
self.scroll_up_ns as f64 / 1_000_000.0,
self.scroll_up_count,
); );
} }
} }
#[cfg(not(feature = "render_timing"))]
pub fn log_if_slow(&self, _threshold_ms: u64) {}
} }
/// Kitty-style ring buffer for scrollback history. /// Kitty-style ring buffer for scrollback history.
@@ -496,11 +526,6 @@ pub struct Terminal {
pub focus_reporting: bool, pub focus_reporting: bool,
/// Synchronized output mode (for reducing flicker). /// Synchronized output mode (for reducing flicker).
synchronized_output: bool, 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). /// Performance timing stats (for debugging).
pub stats: ProcessingStats, pub stats: ProcessingStats,
/// Command queue for terminal-to-application communication. /// Command queue for terminal-to-application communication.
@@ -518,21 +543,12 @@ impl Terminal {
/// Default scrollback limit (10,000 lines for better cache performance). /// Default scrollback limit (10,000 lines for better cache performance).
pub const DEFAULT_SCROLLBACK_LIMIT: usize = 10_000; 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. /// Creates a new terminal with the given dimensions and scrollback limit.
pub fn new(cols: usize, rows: usize, scrollback_limit: usize) -> Self { pub fn new(cols: usize, rows: usize, scrollback_limit: usize) -> Self {
log::info!("Terminal::new: cols={}, rows={}, scroll_bottom={}", cols, rows, rows.saturating_sub(1)); log::info!("Terminal::new: cols={}, rows={}, scroll_bottom={}", cols, rows, rows.saturating_sub(1));
let grid = vec![vec![Cell::default(); cols]; rows]; let grid = vec![vec![Cell::default(); cols]; rows];
let line_map: Vec<usize> = (0..rows).collect(); let line_map: Vec<usize> = (0..rows).collect();
// 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 { Self {
grid, grid,
line_map, line_map,
@@ -567,8 +583,6 @@ impl Terminal {
bracketed_paste: false, bracketed_paste: false,
focus_reporting: false, focus_reporting: false,
synchronized_output: false, synchronized_output: false,
line_pool,
parser: Some(Parser::new()),
stats: ProcessingStats::default(), stats: ProcessingStats::default(),
command_queue: Vec::new(), command_queue: Vec::new(),
image_storage: ImageStorage::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). /// Mark a specific line as dirty (needs redrawing).
#[inline] #[inline]
pub fn mark_line_dirty(&mut self, line: usize) { pub fn mark_line_dirty(&mut self, line: usize) {
@@ -642,6 +646,32 @@ impl Terminal {
self.synchronized_output 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. /// Get the actual grid row index for a visual row.
#[inline] #[inline]
pub fn grid_row(&self, visual_row: usize) -> usize { pub fn grid_row(&self, visual_row: usize) -> usize {
@@ -667,7 +697,7 @@ impl Terminal {
let blank = self.blank_cell(); let blank = self.blank_cell();
let row = &mut self.grid[grid_row]; let row = &mut self.grid[grid_row];
// Ensure row has correct width (may differ after swap with scrollback post-resize) // 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); row.fill(blank);
} }
@@ -695,16 +725,8 @@ impl Terminal {
} }
} }
/// Processes raw bytes from the PTY using the internal VT parser. /// Mark terminal as dirty (needs redraw). Called after parsing.
/// Uses Kitty-style architecture: UTF-8 decode until ESC, then parse escape sequences. pub fn mark_dirty(&mut self) {
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);
}
self.dirty = true; self.dirty = true;
} }
@@ -836,7 +858,8 @@ impl Terminal {
let region_size = self.scroll_bottom - self.scroll_top + 1; let region_size = self.scroll_bottom - self.scroll_top + 1;
let n = n.min(region_size); 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 { for _ in 0..n {
// Save the top line's grid index before rotation // 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); 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] #[inline]
fn mark_region_dirty(&mut self, start: usize, end: usize) { fn mark_region_dirty(&mut self, start: usize, end: usize) {
// For small regions (< 64 lines), this is faster than individual calls let end = end.min(255);
for line in start..=end.min(255) { if start > end {
let word = line / 64; return;
let bit = line % 64; }
self.dirty_lines[word] |= 1u64 << bit;
// 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;
} }
} }
@@ -1098,6 +1143,39 @@ impl Terminal {
rows 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. /// Inserts n blank lines at the cursor position, scrolling lines below down.
/// Uses line_map rotation for efficiency. /// Uses line_map rotation for efficiency.
fn insert_lines(&mut self, n: usize) { fn insert_lines(&mut self, n: usize) {
@@ -1119,13 +1197,13 @@ impl Terminal {
// Clear the recycled line (now at cursor position) // Clear the recycled line (now at cursor position)
self.clear_grid_row(recycled_grid_row); self.clear_grid_row(recycled_grid_row);
}
// Mark affected lines dirty // Mark affected lines dirty once after all rotations
for line in self.cursor_row..=self.scroll_bottom { for line in self.cursor_row..=self.scroll_bottom {
self.mark_line_dirty(line); self.mark_line_dirty(line);
} }
} }
}
/// Deletes n lines at the cursor position, scrolling lines below up. /// Deletes n lines at the cursor position, scrolling lines below up.
/// Uses line_map rotation for efficiency. /// Uses line_map rotation for efficiency.
@@ -1148,13 +1226,13 @@ impl Terminal {
// Clear the recycled line (now at bottom of scroll region) // Clear the recycled line (now at bottom of scroll region)
self.clear_grid_row(recycled_grid_row); self.clear_grid_row(recycled_grid_row);
}
// Mark affected lines dirty // Mark affected lines dirty once after all rotations
for line in self.cursor_row..=self.scroll_bottom { for line in self.cursor_row..=self.scroll_bottom {
self.mark_line_dirty(line); self.mark_line_dirty(line);
} }
} }
}
/// Inserts n blank characters at the cursor, shifting existing chars right. /// Inserts n blank characters at the cursor, shifting existing chars right.
fn insert_characters(&mut self, n: usize) { fn insert_characters(&mut self, n: usize) {
@@ -1162,14 +1240,10 @@ impl Terminal {
let blank = self.blank_cell(); let blank = self.blank_cell();
let row = &mut self.grid[grid_row]; let row = &mut self.grid[grid_row];
let n = n.min(self.cols - self.cursor_col); let n = n.min(self.cols - self.cursor_col);
// Remove n characters from the end // Truncate n characters from the end
for _ in 0..n { row.truncate(self.cols - n);
row.pop(); // 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));
// Insert n blank characters at cursor position
for _ in 0..n {
row.insert(self.cursor_col, blank);
}
self.mark_line_dirty(self.cursor_row); self.mark_line_dirty(self.cursor_row);
} }
@@ -1179,16 +1253,11 @@ impl Terminal {
let blank = self.blank_cell(); let blank = self.blank_cell();
let row = &mut self.grid[grid_row]; let row = &mut self.grid[grid_row];
let n = n.min(self.cols - self.cursor_col); let n = n.min(self.cols - self.cursor_col);
// Remove n characters at cursor position let end = (self.cursor_col + n).min(row.len());
for _ in 0..n { // Remove n characters at cursor position (single O(cols) operation)
if self.cursor_col < row.len() { row.drain(self.cursor_col..end);
row.remove(self.cursor_col);
}
}
// Pad with blank characters at the end // Pad with blank characters at the end
while row.len() < self.cols { row.resize(self.cols, blank);
row.push(blank);
}
self.mark_line_dirty(self.cursor_row); self.mark_line_dirty(self.cursor_row);
} }
@@ -1197,21 +1266,18 @@ impl Terminal {
let grid_row = self.line_map[self.cursor_row]; let grid_row = self.line_map[self.cursor_row];
let n = n.min(self.cols - self.cursor_col); let n = n.min(self.cols - self.cursor_col);
let blank = self.blank_cell(); let blank = self.blank_cell();
for i in 0..n { // Fill range with blanks (bounds already guaranteed by min above)
if self.cursor_col + i < self.cols { self.grid[grid_row][self.cursor_col..self.cursor_col + n].fill(blank);
self.grid[grid_row][self.cursor_col + i] = blank;
}
}
self.mark_line_dirty(self.cursor_row); self.mark_line_dirty(self.cursor_row);
} }
/// Clears the current line from cursor to end. /// Clears the current line from cursor to end.
#[inline]
fn clear_line_from_cursor(&mut self) { fn clear_line_from_cursor(&mut self) {
let grid_row = self.line_map[self.cursor_row]; let grid_row = self.line_map[self.cursor_row];
let blank = self.blank_cell(); let blank = self.blank_cell();
for col in self.cursor_col..self.cols { // Use slice fill for efficiency
self.grid[grid_row][col] = blank; self.grid[grid_row][self.cursor_col..].fill(blank);
}
self.mark_line_dirty(self.cursor_row); self.mark_line_dirty(self.cursor_row);
} }
@@ -1243,9 +1309,12 @@ impl Terminal {
} }
impl Handler for 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). /// 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 // Cache the current line to avoid repeated line_map lookups
let mut cached_row = self.cursor_row; let mut cached_row = self.cursor_row;
let mut grid_row = self.line_map[cached_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) // Mark the initial line as dirty (like Kitty's init_text_loop_line)
self.mark_line_dirty(cached_row); self.mark_line_dirty(cached_row);
for &c in chars { for &cp in codepoints {
match c { // 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 // Bell
'\x07' => { 0x07 => {
// BEL - ignore for now (could trigger visual bell) // BEL - ignore for now (could trigger visual bell)
} }
// Backspace // Backspace
'\x08' => { 0x08 => {
if self.cursor_col > 0 { if self.cursor_col > 0 {
self.cursor_col -= 1; self.cursor_col -= 1;
} }
} }
// Tab // Tab
'\x09' => { 0x09 => {
let next_tab = (self.cursor_col / 8 + 1) * 8; let next_tab = (self.cursor_col / 8 + 1) * 8;
self.cursor_col = next_tab.min(self.cols - 1); self.cursor_col = next_tab.min(self.cols - 1);
} }
// Line feed, Vertical tab, Form feed // Line feed, Vertical tab, Form feed
'\x0A' | '\x0B' | '\x0C' => { 0x0A | 0x0B | 0x0C => {
let old_row = self.cursor_row; let old_row = self.cursor_row;
self.cursor_row += 1; self.cursor_row += 1;
if self.cursor_row > self.scroll_bottom { if self.cursor_row > self.scroll_bottom {
@@ -1286,21 +1357,17 @@ impl Handler for Terminal {
self.mark_line_dirty(cached_row); self.mark_line_dirty(cached_row);
} }
// Carriage return // Carriage return
'\x0D' => { 0x0D => {
self.cursor_col = 0; self.cursor_col = 0;
} }
// Fast path for printable ASCII (0x20-0x7E) - like Kitty // Fast path for printable ASCII (0x20-0x7E) - like Kitty
// ASCII is always width 1, never zero-width, never wide // ASCII is always width 1, never zero-width, never wide
c if c >= ' ' && c <= '~' => { cp if cp >= 0x20 && cp <= 0x7E => {
// Handle wrap // Handle wrap
if self.cursor_col >= self.cols { if self.cursor_col >= self.cols {
if self.auto_wrap { if self.auto_wrap {
self.cursor_col = 0; self.cursor_col = 0;
self.cursor_row += 1; self.advance_row();
if self.cursor_row > self.scroll_bottom {
self.scroll_up(1);
self.cursor_row = self.scroll_bottom;
}
cached_row = self.cursor_row; cached_row = self.cursor_row;
grid_row = self.line_map[cached_row]; grid_row = self.line_map[cached_row];
self.mark_line_dirty(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 // Write character directly - no wide char handling needed for ASCII
self.grid[grid_row][self.cursor_col] = Cell { // SAFETY: cp is in 0x20..=0x7E which are valid ASCII chars
character: c, let c = unsafe { char::from_u32_unchecked(cp) };
fg_color: self.current_fg, self.grid[grid_row][self.cursor_col] = self.make_cell(c, false);
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; self.cursor_col += 1;
} }
// Slow path for non-ASCII printable characters (including all Unicode) // Slow path for non-ASCII printable characters (including all Unicode)
c if c > '~' => { // Delegates to print_char() which handles wide characters, wrapping, etc.
// Determine character width using Unicode Standard Annex #11 cp if cp > 0x7E => {
let char_width = c.width().unwrap_or(1); // Convert to char, using replacement character for invalid codepoints
let c = char::from_u32(cp).unwrap_or('\u{FFFD}');
// Skip zero-width characters (combining marks, etc.) self.print_char(c);
if char_width == 0 { // Update cached values since print_char may have scrolled or wrapped
// TODO: Handle combining characters if cached_row != self.cursor_row {
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; cached_row = self.cursor_row;
grid_row = self.line_map[cached_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;
} }
} }
// Other control chars - ignore // 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() // 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. /// Handle control characters embedded in escape sequences.
@@ -1437,11 +1420,7 @@ impl Handler for Terminal {
self.cursor_col = next_tab.min(self.cols - 1); self.cursor_col = next_tab.min(self.cols - 1);
} }
0x0A | 0x0B | 0x0C => { 0x0A | 0x0B | 0x0C => {
self.cursor_row += 1; self.advance_row();
if self.cursor_row > self.scroll_bottom {
self.scroll_up(1);
self.cursor_row = self.scroll_bottom;
}
} }
0x0D => { 0x0D => {
self.cursor_col = 0; self.cursor_col = 0;
@@ -1617,7 +1596,11 @@ impl Handler for Terminal {
} }
/// Handle a complete CSI sequence. /// Handle a complete CSI sequence.
#[inline]
fn csi(&mut self, params: &CsiParams) { fn csi(&mut self, params: &CsiParams) {
#[cfg(feature = "render_timing")]
let start = std::time::Instant::now();
let action = params.final_char as char; let action = params.final_char as char;
let primary = params.primary; let primary = params.primary;
let secondary = params.secondary; let secondary = params.secondary;
@@ -1766,16 +1749,43 @@ impl Handler for Terminal {
self.insert_characters(n); self.insert_characters(n);
} }
// Repeat preceding character (REP) // Repeat preceding character (REP)
// Optimized like Kitty: batch writes for ASCII, avoid per-char overhead
'b' => { 'b' => {
let n = params.get(0, 1).max(1) as usize; let n = (params.get(0, 1).max(1) as usize).min(65535); // Like Kitty's CSI_REP_MAX_REPETITIONS
if self.cursor_col > 0 { if self.cursor_col > 0 && n > 0 {
let grid_row = self.line_map[self.cursor_row]; let grid_row = self.line_map[self.cursor_row];
let last_char = self.grid[grid_row][self.cursor_col - 1].character; let last_char = self.grid[grid_row][self.cursor_col - 1].character;
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 { for _ in 0..n {
self.print_char(last_char); self.print_char(last_char);
} }
} }
} }
}
// Device Attributes (DA) // Device Attributes (DA)
'c' => { 'c' => {
if primary == 0 || primary == b'?' { if primary == 0 || primary == b'?' {
@@ -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) { fn save_cursor(&mut self) {
@@ -2012,6 +2028,15 @@ impl Handler for Terminal {
self.mark_line_dirty(visual_row); 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 { impl Terminal {
@@ -2035,11 +2060,7 @@ impl Terminal {
if self.cursor_col >= self.cols { if self.cursor_col >= self.cols {
if self.auto_wrap { if self.auto_wrap {
self.cursor_col = 0; self.cursor_col = 0;
self.cursor_row += 1; self.advance_row();
if self.cursor_row > self.scroll_bottom {
self.scroll_up(1);
self.cursor_row = self.scroll_bottom;
}
} else { } else {
self.cursor_col = self.cols - 1; self.cursor_col = self.cols - 1;
} }
@@ -2053,11 +2074,7 @@ impl Terminal {
let grid_row = self.line_map[self.cursor_row]; let grid_row = self.line_map[self.cursor_row];
self.grid[grid_row][self.cursor_col] = Cell::default(); self.grid[grid_row][self.cursor_col] = Cell::default();
self.cursor_col = 0; self.cursor_col = 0;
self.cursor_row += 1; self.advance_row();
if self.cursor_row > self.scroll_bottom {
self.scroll_up(1);
self.cursor_row = self.scroll_bottom;
}
} else { } else {
// Can't fit, don't print // Can't fit, don't print
return; return;
@@ -2080,16 +2097,7 @@ impl Terminal {
} }
// Write the character to the first cell // Write the character to the first cell
self.grid[grid_row][self.cursor_col] = Cell { self.grid[grid_row][self.cursor_col] = self.make_cell(c, false);
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.mark_line_dirty(self.cursor_row); self.mark_line_dirty(self.cursor_row);
self.cursor_col += 1; 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 + 1] = Cell::default();
} }
self.grid[grid_row][self.cursor_col] = Cell { self.grid[grid_row][self.cursor_col] = self.make_cell(' ', true);
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.cursor_col += 1; 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 = &params.params;
let is_sub = &params.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. /// 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) { fn handle_sgr(&mut self, params: &CsiParams) {
if params.num_params == 0 { let num = params.num_params;
self.current_fg = Color::Default;
self.current_bg = Color::Default; // Fast path: SGR 0 (reset) with no params or explicit 0
self.current_bold = false; if num == 0 {
self.current_italic = false; self.reset_sgr_attributes();
self.current_underline_style = 0;
self.current_strikethrough = false;
return; return;
} }
let p = &params.params;
let is_sub = &params.is_sub_param;
let mut i = 0; 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 { match code {
0 => { 0 => self.reset_sgr_attributes(),
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;
}
1 => self.current_bold = true, 1 => self.current_bold = true,
// 2 => dim (not currently rendered)
3 => self.current_italic = true, 3 => self.current_italic = true,
4 => { 4 => {
// Check for sub-parameter (4:x format for underline style) // Check for sub-parameter (4:x format for underline style)
if i + 1 < params.num_params && params.is_sub_param[i + 1] { if i + 1 < num && is_sub[i + 1] {
let style = params.params[i + 1];
// 0=none, 1=single, 2=double, 3=curly, 4=dotted, 5=dashed // 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; i += 1;
} else { } else {
// Plain SGR 4 = single underline // Plain SGR 4 = single underline
@@ -2157,76 +2191,35 @@ impl Terminal {
} }
7 => std::mem::swap(&mut self.current_fg, &mut self.current_bg), 7 => std::mem::swap(&mut self.current_fg, &mut self.current_bg),
9 => self.current_strikethrough = true, 9 => self.current_strikethrough = true,
21 => self.current_underline_style = 2, // Double underline
22 => self.current_bold = false, 22 => self.current_bold = false,
23 => self.current_italic = false, 23 => self.current_italic = false,
24 => self.current_underline_style = 0, 24 => self.current_underline_style = 0,
27 => std::mem::swap(&mut self.current_fg, &mut self.current_bg), 27 => std::mem::swap(&mut self.current_fg, &mut self.current_bg),
29 => self.current_strikethrough = false, 29 => self.current_strikethrough = false,
// Standard foreground colors (30-37)
30..=37 => self.current_fg = Color::Indexed((code - 30) as u8), 30..=37 => self.current_fg = Color::Indexed((code - 30) as u8),
38 => { 38 => {
// Extended foreground color // Extended foreground color
if i + 1 < params.num_params && params.is_sub_param[i + 1] { if let Some((color, consumed)) = Self::parse_extended_color(params, i) {
let mode = params.params[i + 1]; self.current_fg = color;
if mode == 5 && i + 2 < params.num_params { i += consumed;
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;
}
} }
} }
39 => self.current_fg = Color::Default, 39 => self.current_fg = Color::Default,
// Standard background colors (40-47)
40..=47 => self.current_bg = Color::Indexed((code - 40) as u8), 40..=47 => self.current_bg = Color::Indexed((code - 40) as u8),
48 => { 48 => {
// Extended background color // Extended background color
if i + 1 < params.num_params && params.is_sub_param[i + 1] { if let Some((color, consumed)) = Self::parse_extended_color(params, i) {
let mode = params.params[i + 1]; self.current_bg = color;
if mode == 5 && i + 2 < params.num_params { i += consumed;
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;
}
} }
} }
49 => self.current_bg = Color::Default, 49 => self.current_bg = Color::Default,
// Bright foreground colors (90-97)
90..=97 => self.current_fg = Color::Indexed((code - 90 + 8) as u8), 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), 100..=107 => self.current_bg = Color::Indexed((code - 100 + 8) as u8),
_ => {} _ => {}
} }
@@ -2234,7 +2227,19 @@ impl Terminal {
} }
} }
/// 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. /// Handle Kitty keyboard protocol CSI sequences.
#[inline]
fn handle_keyboard_protocol_csi(&mut self, params: &CsiParams) { fn handle_keyboard_protocol_csi(&mut self, params: &CsiParams) {
match params.primary { match params.primary {
b'?' => { b'?' => {
@@ -2266,6 +2271,7 @@ impl Terminal {
} }
/// Handle DEC private mode set (CSI ? Ps h). /// Handle DEC private mode set (CSI ? Ps h).
#[inline]
fn handle_dec_private_mode_set(&mut self, params: &CsiParams) { fn handle_dec_private_mode_set(&mut self, params: &CsiParams) {
for i in 0..params.num_params { for i in 0..params.num_params {
match params.params[i] { match params.params[i] {
@@ -2334,6 +2340,7 @@ impl Terminal {
} }
/// Handle DEC private mode reset (CSI ? Ps l). /// Handle DEC private mode reset (CSI ? Ps l).
#[inline]
fn handle_dec_private_mode_reset(&mut self, params: &CsiParams) { fn handle_dec_private_mode_reset(&mut self, params: &CsiParams) {
for i in 0..params.num_params { for i in 0..params.num_params {
match params.params[i] { match params.params[i] {
+910 -183
View File
File diff suppressed because it is too large Load Diff