tick system, AVX2 UTF-8 decoder, uh faster in general
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||||
@@ -18,7 +18,7 @@ source=()
|
|||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "$startdir"
|
cd "$startdir"
|
||||||
cargo build --release --locked
|
cargo build --release --features production
|
||||||
}
|
}
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -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.
|
||||||
@@ -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")]`
|
||||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
|||||||
|
//! Linear color palette for GPU rendering.
|
||||||
|
//!
|
||||||
|
//! Provides pre-computed sRGB to linear RGB conversion for efficient GPU color handling.
|
||||||
|
|
||||||
|
use crate::terminal::ColorPalette;
|
||||||
|
|
||||||
|
/// Pre-computed linear RGB color palette.
|
||||||
|
/// Avoids repeated sRGB→linear conversions during rendering.
|
||||||
|
/// The color_table contains [258][4] floats: 256 indexed colors + default fg (256) + default bg (257).
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct LinearPalette {
|
||||||
|
/// Pre-computed linear RGBA colors ready for GPU upload.
|
||||||
|
/// Index 0-255: palette colors, 256: default_fg, 257: default_bg
|
||||||
|
pub color_table: [[f32; 4]; 258],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LinearPalette {
|
||||||
|
/// Convert sRGB component (0.0-1.0) to linear RGB.
|
||||||
|
#[inline]
|
||||||
|
pub fn srgb_to_linear(c: f32) -> f32 {
|
||||||
|
if c <= 0.04045 {
|
||||||
|
c / 12.92
|
||||||
|
} else {
|
||||||
|
((c + 0.055) / 1.055).powf(2.4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert an sRGB [u8; 3] color to linear [f32; 4] with alpha=1.0.
|
||||||
|
#[inline]
|
||||||
|
pub fn rgb_to_linear(rgb: [u8; 3]) -> [f32; 4] {
|
||||||
|
[
|
||||||
|
Self::srgb_to_linear(rgb[0] as f32 / 255.0),
|
||||||
|
Self::srgb_to_linear(rgb[1] as f32 / 255.0),
|
||||||
|
Self::srgb_to_linear(rgb[2] as f32 / 255.0),
|
||||||
|
1.0,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a LinearPalette from a ColorPalette.
|
||||||
|
pub fn from_palette(palette: &ColorPalette) -> Self {
|
||||||
|
let mut color_table = [[0.0f32; 4]; 258];
|
||||||
|
|
||||||
|
for i in 0..256 {
|
||||||
|
color_table[i] = Self::rgb_to_linear(palette.colors[i]);
|
||||||
|
}
|
||||||
|
color_table[256] = Self::rgb_to_linear(palette.default_fg);
|
||||||
|
color_table[257] = Self::rgb_to_linear(palette.default_bg);
|
||||||
|
|
||||||
|
Self { color_table }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LinearPalette {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::from_palette(&ColorPalette::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,398 @@
|
|||||||
|
//! Color font (emoji) rendering using FreeType + Cairo.
|
||||||
|
//!
|
||||||
|
//! This module provides color emoji rendering support by using FreeType to load
|
||||||
|
//! color fonts (COLR, CBDT, sbix formats) and Cairo to render them.
|
||||||
|
|
||||||
|
use cairo::{Format, ImageSurface};
|
||||||
|
use freetype::Library as FtLibrary;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// COLOR FONT LOOKUP
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Find a color font (emoji font) that contains the given character using fontconfig.
|
||||||
|
/// Returns the path to the font file if found.
|
||||||
|
pub fn find_color_font_for_char(c: char) -> Option<PathBuf> {
|
||||||
|
use fontconfig_sys as fcsys;
|
||||||
|
use fcsys::*;
|
||||||
|
use fcsys::constants::{FC_CHARSET, FC_COLOR, FC_FILE};
|
||||||
|
|
||||||
|
log::debug!("find_color_font_for_char: looking for color font for U+{:04X} '{}'", c as u32, c);
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// Create a pattern
|
||||||
|
let pat = FcPatternCreate();
|
||||||
|
if pat.is_null() {
|
||||||
|
log::debug!("find_color_font_for_char: FcPatternCreate failed");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a charset with the target character
|
||||||
|
let charset = FcCharSetCreate();
|
||||||
|
if charset.is_null() {
|
||||||
|
FcPatternDestroy(pat);
|
||||||
|
log::debug!("find_color_font_for_char: FcCharSetCreate failed");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the character to the charset
|
||||||
|
FcCharSetAddChar(charset, c as u32);
|
||||||
|
|
||||||
|
// Add the charset to the pattern
|
||||||
|
FcPatternAddCharSet(pat, FC_CHARSET.as_ptr() as *const i8, charset);
|
||||||
|
|
||||||
|
// Request a color font
|
||||||
|
FcPatternAddBool(pat, FC_COLOR.as_ptr() as *const i8, 1); // FcTrue = 1
|
||||||
|
|
||||||
|
// Run substitutions
|
||||||
|
FcConfigSubstitute(std::ptr::null_mut(), pat, FcMatchPattern);
|
||||||
|
FcDefaultSubstitute(pat);
|
||||||
|
|
||||||
|
// Find matching font
|
||||||
|
let mut result = FcResultNoMatch;
|
||||||
|
let matched = FcFontMatch(std::ptr::null_mut(), pat, &mut result);
|
||||||
|
|
||||||
|
let font_path = if !matched.is_null() && result == FcResultMatch {
|
||||||
|
// Check if the matched font is actually a color font
|
||||||
|
let mut is_color: i32 = 0;
|
||||||
|
let has_color = FcPatternGetBool(matched, FC_COLOR.as_ptr() as *const i8, 0, &mut is_color) == FcResultMatch && is_color != 0;
|
||||||
|
|
||||||
|
log::debug!("find_color_font_for_char: matched font, is_color={}", has_color);
|
||||||
|
|
||||||
|
if has_color {
|
||||||
|
// Get the file path from the matched pattern
|
||||||
|
let mut file_ptr: *mut u8 = std::ptr::null_mut();
|
||||||
|
if FcPatternGetString(matched, FC_FILE.as_ptr() as *const i8, 0, &mut file_ptr) == FcResultMatch {
|
||||||
|
let path_cstr = CStr::from_ptr(file_ptr as *const i8);
|
||||||
|
let path = PathBuf::from(path_cstr.to_string_lossy().into_owned());
|
||||||
|
log::debug!("find_color_font_for_char: found color font {:?}", path);
|
||||||
|
Some(path)
|
||||||
|
} else {
|
||||||
|
log::debug!("find_color_font_for_char: couldn't get file path");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::debug!("find_color_font_for_char: matched font is not a color font");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::debug!("find_color_font_for_char: no match found (result={:?})", result);
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
if !matched.is_null() {
|
||||||
|
FcPatternDestroy(matched);
|
||||||
|
}
|
||||||
|
FcCharSetDestroy(charset);
|
||||||
|
FcPatternDestroy(pat);
|
||||||
|
|
||||||
|
font_path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// COLOR FONT RENDERER
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Lazy-initialized color font renderer using FreeType + Cairo.
|
||||||
|
/// Only created when a color emoji is first encountered.
|
||||||
|
/// Cairo is required for proper color font rendering (COLR, CBDT, sbix formats).
|
||||||
|
pub struct ColorFontRenderer {
|
||||||
|
/// FreeType library instance
|
||||||
|
ft_library: FtLibrary,
|
||||||
|
/// Loaded FreeType faces and their Cairo font faces, keyed by font path
|
||||||
|
faces: HashMap<PathBuf, (freetype::Face, cairo::FontFace)>,
|
||||||
|
/// Reusable Cairo surface for rendering
|
||||||
|
surface: Option<ImageSurface>,
|
||||||
|
/// Current surface dimensions
|
||||||
|
surface_size: (i32, i32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColorFontRenderer {
|
||||||
|
pub fn new() -> Result<Self, freetype::Error> {
|
||||||
|
let ft_library = FtLibrary::init()?;
|
||||||
|
Ok(Self {
|
||||||
|
ft_library,
|
||||||
|
faces: HashMap::new(),
|
||||||
|
surface: None,
|
||||||
|
surface_size: (0, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure faces are loaded and return font size to set
|
||||||
|
fn ensure_faces_loaded(&mut self, path: &PathBuf) -> bool {
|
||||||
|
if !self.faces.contains_key(path) {
|
||||||
|
match self.ft_library.new_face(path, 0) {
|
||||||
|
Ok(ft_face) => {
|
||||||
|
// Create Cairo font face from FreeType face
|
||||||
|
match cairo::FontFace::create_from_ft(&ft_face) {
|
||||||
|
Ok(cairo_face) => {
|
||||||
|
self.faces.insert(path.clone(), (ft_face, cairo_face));
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to create Cairo font face for {:?}: {:?}", path, e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to load color font {:?}: {:?}", path, e);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a color glyph using FreeType + Cairo.
|
||||||
|
/// Returns (width, height, RGBA bitmap, offset_x, offset_y) or None if rendering fails.
|
||||||
|
pub fn render_color_glyph(
|
||||||
|
&mut self,
|
||||||
|
font_path: &PathBuf,
|
||||||
|
c: char,
|
||||||
|
font_size_px: f32,
|
||||||
|
cell_width: u32,
|
||||||
|
cell_height: u32,
|
||||||
|
) -> Option<(u32, u32, Vec<u8>, f32, f32)> {
|
||||||
|
log::debug!("render_color_glyph: U+{:04X} '{}' font={:?}", c as u32, c, font_path);
|
||||||
|
|
||||||
|
// Ensure faces are loaded
|
||||||
|
if !self.ensure_faces_loaded(font_path) {
|
||||||
|
log::debug!("render_color_glyph: failed to load faces");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
log::debug!("render_color_glyph: faces loaded successfully, faces count={}", self.faces.len());
|
||||||
|
|
||||||
|
// Get glyph index from FreeType face
|
||||||
|
// Note: We do NOT call set_pixel_sizes here because CBDT (bitmap) fonts have fixed sizes
|
||||||
|
// and will fail. Cairo handles font sizing internally.
|
||||||
|
let glyph_index = {
|
||||||
|
let face_entry = self.faces.get(font_path);
|
||||||
|
if face_entry.is_none() {
|
||||||
|
log::debug!("render_color_glyph: face not found in hashmap after ensure_faces_loaded!");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (ft_face, _) = face_entry?;
|
||||||
|
log::debug!("render_color_glyph: got ft_face, getting char index for U+{:04X}", c as u32);
|
||||||
|
let idx = ft_face.get_char_index(c as usize);
|
||||||
|
log::debug!("render_color_glyph: FreeType glyph index for U+{:04X} = {:?}", c as u32, idx);
|
||||||
|
if idx.is_none() {
|
||||||
|
log::debug!("render_color_glyph: glyph index is None - char not in font!");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
idx?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clone the Cairo font face (it's reference-counted)
|
||||||
|
let cairo_face = {
|
||||||
|
let (_, cairo_face) = self.faces.get(font_path)?;
|
||||||
|
cairo_face.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
// For emoji, we typically render at 2x cell width (double-width character)
|
||||||
|
let render_width = (cell_width * 2).max(cell_height) as i32;
|
||||||
|
let render_height = cell_height as i32;
|
||||||
|
|
||||||
|
log::debug!("render_color_glyph: render size {}x{}", render_width, render_height);
|
||||||
|
|
||||||
|
// Ensure we have a large enough surface
|
||||||
|
let surface_width = render_width.max(256);
|
||||||
|
let surface_height = render_height.max(256);
|
||||||
|
|
||||||
|
if self.surface.is_none() || self.surface_size.0 < surface_width || self.surface_size.1 < surface_height {
|
||||||
|
let new_width = surface_width.max(self.surface_size.0);
|
||||||
|
let new_height = surface_height.max(self.surface_size.1);
|
||||||
|
match ImageSurface::create(Format::ARgb32, new_width, new_height) {
|
||||||
|
Ok(surface) => {
|
||||||
|
log::debug!("render_color_glyph: created Cairo surface {}x{}", new_width, new_height);
|
||||||
|
self.surface = Some(surface);
|
||||||
|
self.surface_size = (new_width, new_height);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to create Cairo surface: {:?}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let surface = self.surface.as_mut()?;
|
||||||
|
|
||||||
|
// Create Cairo context
|
||||||
|
let cr = match cairo::Context::new(surface) {
|
||||||
|
Ok(cr) => cr,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to create Cairo context: {:?}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear the surface
|
||||||
|
cr.set_operator(cairo::Operator::Clear);
|
||||||
|
cr.paint().ok()?;
|
||||||
|
cr.set_operator(cairo::Operator::Over);
|
||||||
|
|
||||||
|
// Set the font face and initial size
|
||||||
|
cr.set_font_face(&cairo_face);
|
||||||
|
|
||||||
|
// Target dimensions for the glyph (2 cells wide, 1 cell tall for emoji)
|
||||||
|
let target_width = render_width as f64;
|
||||||
|
let target_height = render_height as f64;
|
||||||
|
|
||||||
|
// Start with the requested font size and reduce until glyph fits
|
||||||
|
// This matches Kitty's fit_cairo_glyph() approach
|
||||||
|
let mut current_size = font_size_px as f64;
|
||||||
|
let min_size = 2.0;
|
||||||
|
|
||||||
|
cr.set_font_size(current_size);
|
||||||
|
let mut glyph = cairo::Glyph::new(glyph_index as u64, 0.0, 0.0);
|
||||||
|
let mut text_extents = cr.glyph_extents(&[glyph]).ok()?;
|
||||||
|
|
||||||
|
while current_size > min_size && (text_extents.width() > target_width || text_extents.height() > target_height) {
|
||||||
|
let ratio = (target_width / text_extents.width()).min(target_height / text_extents.height());
|
||||||
|
let new_size = (ratio * current_size).max(min_size);
|
||||||
|
if new_size >= current_size {
|
||||||
|
current_size -= 2.0;
|
||||||
|
} else {
|
||||||
|
current_size = new_size;
|
||||||
|
}
|
||||||
|
cr.set_font_size(current_size);
|
||||||
|
text_extents = cr.glyph_extents(&[glyph]).ok()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("render_color_glyph: fitted font size {:.1} (from {:.1}), glyph extents {:.1}x{:.1}",
|
||||||
|
current_size, font_size_px, text_extents.width(), text_extents.height());
|
||||||
|
|
||||||
|
// Get font metrics for positioning with the final size
|
||||||
|
let font_extents = cr.font_extents().ok()?;
|
||||||
|
log::debug!("render_color_glyph: font extents - ascent={:.1}, descent={:.1}, height={:.1}",
|
||||||
|
font_extents.ascent(), font_extents.descent(), font_extents.height());
|
||||||
|
|
||||||
|
// Create glyph with positioning at baseline
|
||||||
|
// y position should be at baseline (ascent from top)
|
||||||
|
glyph = cairo::Glyph::new(glyph_index as u64, 0.0, font_extents.ascent());
|
||||||
|
|
||||||
|
// Get final glyph extents for sizing
|
||||||
|
text_extents = cr.glyph_extents(&[glyph]).ok()?;
|
||||||
|
log::debug!("render_color_glyph: text extents - width={:.1}, height={:.1}, x_bearing={:.1}, y_bearing={:.1}, x_advance={:.1}",
|
||||||
|
text_extents.width(), text_extents.height(),
|
||||||
|
text_extents.x_bearing(), text_extents.y_bearing(),
|
||||||
|
text_extents.x_advance());
|
||||||
|
|
||||||
|
// Set source color to white - the atlas stores colors directly for emoji
|
||||||
|
cr.set_source_rgba(1.0, 1.0, 1.0, 1.0);
|
||||||
|
|
||||||
|
// Render the glyph
|
||||||
|
if let Err(e) = cr.show_glyphs(&[glyph]) {
|
||||||
|
log::warn!("render_color_glyph: show_glyphs failed: {:?}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
log::debug!("render_color_glyph: cairo show_glyphs succeeded");
|
||||||
|
|
||||||
|
// Flush and get surface reference again
|
||||||
|
drop(cr); // Drop the context before accessing surface data
|
||||||
|
let surface = self.surface.as_mut()?;
|
||||||
|
surface.flush();
|
||||||
|
|
||||||
|
// Calculate actual glyph bounds
|
||||||
|
let glyph_width = text_extents.width().ceil() as u32;
|
||||||
|
let glyph_height = text_extents.height().ceil() as u32;
|
||||||
|
|
||||||
|
log::debug!("render_color_glyph: glyph size {}x{}", glyph_width, glyph_height);
|
||||||
|
|
||||||
|
if glyph_width == 0 || glyph_height == 0 {
|
||||||
|
log::debug!("render_color_glyph: zero size glyph, returning None");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The actual rendered area - use the text extents to determine position
|
||||||
|
let x_offset = text_extents.x_bearing();
|
||||||
|
let y_offset = text_extents.y_bearing();
|
||||||
|
|
||||||
|
// Calculate source rectangle in the surface
|
||||||
|
let src_x = x_offset.max(0.0) as i32;
|
||||||
|
let src_y = (font_extents.ascent() + y_offset).max(0.0) as i32;
|
||||||
|
|
||||||
|
log::debug!("render_color_glyph: source rect starts at ({}, {})", src_x, src_y);
|
||||||
|
|
||||||
|
// Get surface data
|
||||||
|
let stride = surface.stride() as usize;
|
||||||
|
let surface_data = surface.data().ok()?;
|
||||||
|
|
||||||
|
// Extract the glyph region and convert ARGB -> RGBA
|
||||||
|
let out_width = glyph_width.min(render_width as u32);
|
||||||
|
let out_height = glyph_height.min(render_height as u32);
|
||||||
|
|
||||||
|
let mut rgba = vec![0u8; (out_width * out_height * 4) as usize];
|
||||||
|
let mut non_zero_pixels = 0u32;
|
||||||
|
let mut has_color = false;
|
||||||
|
|
||||||
|
for y in 0..out_height as i32 {
|
||||||
|
for x in 0..out_width as i32 {
|
||||||
|
let src_pixel_x = src_x + x;
|
||||||
|
let src_pixel_y = src_y + y;
|
||||||
|
|
||||||
|
if src_pixel_x >= 0 && src_pixel_x < self.surface_size.0
|
||||||
|
&& src_pixel_y >= 0 && src_pixel_y < self.surface_size.1 {
|
||||||
|
let src_idx = (src_pixel_y as usize) * stride + (src_pixel_x as usize) * 4;
|
||||||
|
let dst_idx = (y as usize * out_width as usize + x as usize) * 4;
|
||||||
|
|
||||||
|
if src_idx + 3 < surface_data.len() {
|
||||||
|
// Cairo uses ARGB in native byte order (on little-endian: BGRA in memory)
|
||||||
|
// We need to convert to RGBA
|
||||||
|
let b = surface_data[src_idx];
|
||||||
|
let g = surface_data[src_idx + 1];
|
||||||
|
let r = surface_data[src_idx + 2];
|
||||||
|
let a = surface_data[src_idx + 3];
|
||||||
|
|
||||||
|
if a > 0 {
|
||||||
|
non_zero_pixels += 1;
|
||||||
|
// Check if this is actual color (not just white/gray)
|
||||||
|
if r != g || g != b {
|
||||||
|
has_color = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Un-premultiply alpha if needed (Cairo uses premultiplied alpha)
|
||||||
|
if a > 0 && a < 255 {
|
||||||
|
let inv_alpha = 255.0 / a as f32;
|
||||||
|
rgba[dst_idx] = (r as f32 * inv_alpha).min(255.0) as u8;
|
||||||
|
rgba[dst_idx + 1] = (g as f32 * inv_alpha).min(255.0) as u8;
|
||||||
|
rgba[dst_idx + 2] = (b as f32 * inv_alpha).min(255.0) as u8;
|
||||||
|
rgba[dst_idx + 3] = a;
|
||||||
|
} else {
|
||||||
|
rgba[dst_idx] = r;
|
||||||
|
rgba[dst_idx + 1] = g;
|
||||||
|
rgba[dst_idx + 2] = b;
|
||||||
|
rgba[dst_idx + 3] = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::debug!("render_color_glyph: extracted {}x{} pixels, {} non-zero, has_color={}",
|
||||||
|
out_width, out_height, non_zero_pixels, has_color);
|
||||||
|
|
||||||
|
// Check if we actually got any non-transparent pixels
|
||||||
|
let has_content = rgba.chunks(4).any(|p| p[3] > 0);
|
||||||
|
if !has_content {
|
||||||
|
log::debug!("render_color_glyph: no visible content, returning None");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kitty convention: bitmap_top = -y_bearing (distance from baseline to glyph top)
|
||||||
|
let offset_x = text_extents.x_bearing() as f32;
|
||||||
|
let offset_y = -text_extents.y_bearing() as f32;
|
||||||
|
|
||||||
|
log::debug!("render_color_glyph: SUCCESS - returning {}x{} glyph, offset=({:.1}, {:.1})",
|
||||||
|
out_width, out_height, offset_x, offset_y);
|
||||||
|
|
||||||
|
Some((out_width, out_height, rgba, offset_x, offset_y))
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
-127
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
//! Edge glow animation for visual feedback.
|
||||||
|
//!
|
||||||
|
//! Creates an organic glow effect when navigation fails: a single light node appears at center,
|
||||||
|
//! then splits into two that travel outward to the corners while fading.
|
||||||
|
|
||||||
|
use crate::terminal::Direction;
|
||||||
|
|
||||||
|
/// Edge glow animation state for visual feedback when navigation fails.
|
||||||
|
/// Creates an organic glow effect: a single light node appears at center,
|
||||||
|
/// then splits into two that travel outward to the corners while fading.
|
||||||
|
/// Animation logic is handled in the shader (shader.wgsl).
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct EdgeGlow {
|
||||||
|
/// Which edge to glow (based on the direction the user tried to navigate).
|
||||||
|
pub direction: Direction,
|
||||||
|
/// When the animation started.
|
||||||
|
pub start_time: std::time::Instant,
|
||||||
|
/// Pane bounds - left edge in pixels.
|
||||||
|
pub pane_x: f32,
|
||||||
|
/// Pane bounds - top edge in pixels.
|
||||||
|
pub pane_y: f32,
|
||||||
|
/// Pane bounds - width in pixels.
|
||||||
|
pub pane_width: f32,
|
||||||
|
/// Pane bounds - height in pixels.
|
||||||
|
pub pane_height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EdgeGlow {
|
||||||
|
/// Duration of the glow animation in milliseconds.
|
||||||
|
pub const DURATION_MS: u64 = 500;
|
||||||
|
|
||||||
|
/// Create a new edge glow animation constrained to a pane's bounds.
|
||||||
|
pub fn new(direction: Direction, pane_x: f32, pane_y: f32, pane_width: f32, pane_height: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
direction,
|
||||||
|
start_time: std::time::Instant::now(),
|
||||||
|
pane_x,
|
||||||
|
pane_y,
|
||||||
|
pane_width,
|
||||||
|
pane_height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current animation progress (0.0 to 1.0).
|
||||||
|
pub fn progress(&self) -> f32 {
|
||||||
|
let elapsed = self.start_time.elapsed().as_millis() as f32;
|
||||||
|
let duration = Self::DURATION_MS as f32;
|
||||||
|
(elapsed / duration).min(1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the animation has completed.
|
||||||
|
pub fn is_finished(&self) -> bool {
|
||||||
|
self.progress() >= 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
//! Font loading and discovery using fontconfig.
|
||||||
|
//!
|
||||||
|
//! This module provides font loading utilities including:
|
||||||
|
//! - Finding fonts by family name with style variants (regular, bold, italic, bold-italic)
|
||||||
|
//! - Finding fonts that contain specific characters (for fallback)
|
||||||
|
//! - Loading font data for use with ab_glyph and rustybuzz
|
||||||
|
|
||||||
|
use ab_glyph::FontRef;
|
||||||
|
use fontconfig::Fontconfig;
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// FONT VARIANT
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// A font variant with its data and parsed references.
|
||||||
|
pub struct FontVariant {
|
||||||
|
/// Owned font data (kept alive for the lifetime of the font references).
|
||||||
|
#[allow(dead_code)]
|
||||||
|
data: Box<[u8]>,
|
||||||
|
/// ab_glyph font reference for rasterization.
|
||||||
|
font: FontRef<'static>,
|
||||||
|
/// rustybuzz face for text shaping.
|
||||||
|
face: rustybuzz::Face<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontVariant {
|
||||||
|
/// Get a reference to the ab_glyph font.
|
||||||
|
pub fn font(&self) -> &FontRef<'static> {
|
||||||
|
&self.font
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to the rustybuzz face.
|
||||||
|
pub fn face(&self) -> &rustybuzz::Face<'static> {
|
||||||
|
&self.face
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clone the font reference (ab_glyph FontRef is Clone).
|
||||||
|
pub fn clone_font(&self) -> FontRef<'static> {
|
||||||
|
self.font.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clone the font data.
|
||||||
|
pub fn clone_data(&self) -> Box<[u8]> {
|
||||||
|
self.data.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// FONT DISCOVERY
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Find a font that contains the given character using fontconfig.
|
||||||
|
/// Returns the path to the font file if found.
|
||||||
|
///
|
||||||
|
/// Note: For emoji, use `find_color_font_for_char` from the color_font module instead,
|
||||||
|
/// which explicitly requests color fonts.
|
||||||
|
pub fn find_font_for_char(_fc: &Fontconfig, c: char) -> Option<PathBuf> {
|
||||||
|
use fontconfig_sys as fcsys;
|
||||||
|
use fcsys::*;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// Create a pattern
|
||||||
|
let pat = FcPatternCreate();
|
||||||
|
if pat.is_null() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a charset with the target character
|
||||||
|
let charset = FcCharSetCreate();
|
||||||
|
if charset.is_null() {
|
||||||
|
FcPatternDestroy(pat);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the character to the charset
|
||||||
|
FcCharSetAddChar(charset, c as u32);
|
||||||
|
|
||||||
|
// Add the charset to the pattern
|
||||||
|
let fc_charset_cstr = CStr::from_bytes_with_nul(b"charset\0").unwrap();
|
||||||
|
FcPatternAddCharSet(pat, fc_charset_cstr.as_ptr(), charset);
|
||||||
|
|
||||||
|
// Run substitutions
|
||||||
|
FcConfigSubstitute(std::ptr::null_mut(), pat, FcMatchPattern);
|
||||||
|
FcDefaultSubstitute(pat);
|
||||||
|
|
||||||
|
// Find matching font
|
||||||
|
let mut result = FcResultNoMatch;
|
||||||
|
let matched = FcFontMatch(std::ptr::null_mut(), pat, &mut result);
|
||||||
|
|
||||||
|
let font_result = if !matched.is_null() && result == FcResultMatch {
|
||||||
|
// Get the file path from the matched pattern
|
||||||
|
let mut file_ptr: *mut FcChar8 = std::ptr::null_mut();
|
||||||
|
let fc_file_cstr = CStr::from_bytes_with_nul(b"file\0").unwrap();
|
||||||
|
if FcPatternGetString(matched, fc_file_cstr.as_ptr(), 0, &mut file_ptr) == FcResultMatch
|
||||||
|
{
|
||||||
|
let path_cstr = CStr::from_ptr(file_ptr as *const i8);
|
||||||
|
Some(PathBuf::from(path_cstr.to_string_lossy().into_owned()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
if !matched.is_null() {
|
||||||
|
FcPatternDestroy(matched);
|
||||||
|
}
|
||||||
|
FcCharSetDestroy(charset);
|
||||||
|
FcPatternDestroy(pat);
|
||||||
|
|
||||||
|
font_result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find font files for a font family using fontconfig.
|
||||||
|
/// Returns paths for (regular, bold, italic, bold_italic).
|
||||||
|
/// Any variant that can't be found will be None.
|
||||||
|
pub fn find_font_family_variants(family: &str) -> [Option<PathBuf>; 4] {
|
||||||
|
use fontconfig_sys as fcsys;
|
||||||
|
use fcsys::*;
|
||||||
|
use fcsys::constants::{FC_FAMILY, FC_WEIGHT, FC_SLANT, FC_FILE};
|
||||||
|
use std::ffi::CString;
|
||||||
|
|
||||||
|
let mut results: [Option<PathBuf>; 4] = [None, None, None, None];
|
||||||
|
|
||||||
|
// Style queries: (weight, slant) pairs for each variant
|
||||||
|
// FC_WEIGHT_REGULAR = 80, FC_WEIGHT_BOLD = 200
|
||||||
|
// FC_SLANT_ROMAN = 0, FC_SLANT_ITALIC = 100
|
||||||
|
let styles: [(i32, i32); 4] = [
|
||||||
|
(80, 0), // Regular
|
||||||
|
(200, 0), // Bold
|
||||||
|
(80, 100), // Italic
|
||||||
|
(200, 100), // BoldItalic
|
||||||
|
];
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let family_cstr = match CString::new(family) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return results,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (idx, (weight, slant)) in styles.iter().enumerate() {
|
||||||
|
let pat = FcPatternCreate();
|
||||||
|
if pat.is_null() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set family name
|
||||||
|
FcPatternAddString(pat, FC_FAMILY.as_ptr() as *const i8, family_cstr.as_ptr() as *const u8);
|
||||||
|
// Set weight
|
||||||
|
FcPatternAddInteger(pat, FC_WEIGHT.as_ptr() as *const i8, *weight);
|
||||||
|
// Set slant
|
||||||
|
FcPatternAddInteger(pat, FC_SLANT.as_ptr() as *const i8, *slant);
|
||||||
|
|
||||||
|
FcConfigSubstitute(std::ptr::null_mut(), pat, FcMatchPattern);
|
||||||
|
FcDefaultSubstitute(pat);
|
||||||
|
|
||||||
|
let mut result: FcResult = FcResultMatch;
|
||||||
|
let matched = FcFontMatch(std::ptr::null_mut(), pat, &mut result);
|
||||||
|
|
||||||
|
if result == FcResultMatch && !matched.is_null() {
|
||||||
|
let mut file_ptr: *mut u8 = std::ptr::null_mut();
|
||||||
|
if FcPatternGetString(matched, FC_FILE.as_ptr() as *const i8, 0, &mut file_ptr) == FcResultMatch {
|
||||||
|
if !file_ptr.is_null() {
|
||||||
|
let path_cstr = std::ffi::CStr::from_ptr(file_ptr as *const i8);
|
||||||
|
if let Ok(path_str) = path_cstr.to_str() {
|
||||||
|
results[idx] = Some(PathBuf::from(path_str));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FcPatternDestroy(matched);
|
||||||
|
}
|
||||||
|
|
||||||
|
FcPatternDestroy(pat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// FONT LOADING
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Try to load a font file and create both ab_glyph and rustybuzz handles.
|
||||||
|
/// Returns None if the file doesn't exist or can't be parsed.
|
||||||
|
pub fn load_font_variant(path: &std::path::Path) -> Option<FontVariant> {
|
||||||
|
let data = std::fs::read(path).ok()?.into_boxed_slice();
|
||||||
|
|
||||||
|
// Parse with ab_glyph
|
||||||
|
let font: FontRef<'static> = {
|
||||||
|
let font = FontRef::try_from_slice(&data).ok()?;
|
||||||
|
// SAFETY: We keep data alive in the FontVariant struct
|
||||||
|
unsafe { std::mem::transmute(font) }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse with rustybuzz
|
||||||
|
let face: rustybuzz::Face<'static> = {
|
||||||
|
let face = rustybuzz::Face::from_slice(&data, 0)?;
|
||||||
|
// SAFETY: We keep data alive in the FontVariant struct
|
||||||
|
unsafe { std::mem::transmute(face) }
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(FontVariant { data, font, face })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load font variants for a font family.
|
||||||
|
/// Returns array of font variants, with index 0 being the regular font.
|
||||||
|
/// Falls back to hardcoded paths if fontconfig fails.
|
||||||
|
pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'static>, [Option<FontVariant>; 4]) {
|
||||||
|
// Try to use fontconfig to find the font family
|
||||||
|
if let Some(family) = font_family {
|
||||||
|
let paths = find_font_family_variants(family);
|
||||||
|
log::info!("Font family '{}' resolved to:", family);
|
||||||
|
for (i, path) in paths.iter().enumerate() {
|
||||||
|
let style = match i {
|
||||||
|
0 => "Regular",
|
||||||
|
1 => "Bold",
|
||||||
|
2 => "Italic",
|
||||||
|
3 => "BoldItalic",
|
||||||
|
_ => "Unknown",
|
||||||
|
};
|
||||||
|
if let Some(p) = path {
|
||||||
|
log::info!(" {}: {:?}", style, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the regular font (required)
|
||||||
|
if let Some(regular_path) = &paths[0] {
|
||||||
|
if let Some(regular) = load_font_variant(regular_path) {
|
||||||
|
let primary_font = regular.clone_font();
|
||||||
|
let font_data = regular.clone_data();
|
||||||
|
|
||||||
|
// Load other variants
|
||||||
|
let variants: [Option<FontVariant>; 4] = [
|
||||||
|
Some(regular),
|
||||||
|
paths[1].as_ref().and_then(|p| load_font_variant(p)),
|
||||||
|
paths[2].as_ref().and_then(|p| load_font_variant(p)),
|
||||||
|
paths[3].as_ref().and_then(|p| load_font_variant(p)),
|
||||||
|
];
|
||||||
|
|
||||||
|
return (font_data, primary_font, variants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log::warn!("Failed to load font family '{}', falling back to defaults", family);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try hardcoded paths
|
||||||
|
let fallback_fonts = [
|
||||||
|
("/usr/share/fonts/TTF/0xProtoNerdFont-Regular.ttf",
|
||||||
|
"/usr/share/fonts/TTF/0xProtoNerdFont-Bold.ttf",
|
||||||
|
"/usr/share/fonts/TTF/0xProtoNerdFont-Italic.ttf",
|
||||||
|
"/usr/share/fonts/TTF/0xProtoNerdFont-BoldItalic.ttf"),
|
||||||
|
("/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Regular.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Bold.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-Italic.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMonoNerdFont-BoldItalic.ttf"),
|
||||||
|
("/usr/share/fonts/TTF/JetBrainsMono-Regular.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMono-Bold.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMono-Italic.ttf",
|
||||||
|
"/usr/share/fonts/TTF/JetBrainsMono-BoldItalic.ttf"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (regular, bold, italic, bold_italic) in fallback_fonts {
|
||||||
|
let regular_path = std::path::Path::new(regular);
|
||||||
|
if let Some(regular_variant) = load_font_variant(regular_path) {
|
||||||
|
let primary_font = regular_variant.clone_font();
|
||||||
|
let font_data = regular_variant.clone_data();
|
||||||
|
|
||||||
|
let variants: [Option<FontVariant>; 4] = [
|
||||||
|
Some(regular_variant),
|
||||||
|
load_font_variant(std::path::Path::new(bold)),
|
||||||
|
load_font_variant(std::path::Path::new(italic)),
|
||||||
|
load_font_variant(std::path::Path::new(bold_italic)),
|
||||||
|
];
|
||||||
|
|
||||||
|
log::info!("Loaded font from fallback paths:");
|
||||||
|
log::info!(" Regular: {}", regular);
|
||||||
|
if variants[1].is_some() { log::info!(" Bold: {}", bold); }
|
||||||
|
if variants[2].is_some() { log::info!(" Italic: {}", italic); }
|
||||||
|
if variants[3].is_some() { log::info!(" BoldItalic: {}", bold_italic); }
|
||||||
|
|
||||||
|
return (font_data, primary_font, variants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort: try NotoSansMono
|
||||||
|
let noto_regular = std::path::Path::new("/usr/share/fonts/noto/NotoSansMono-Regular.ttf");
|
||||||
|
if let Some(regular_variant) = load_font_variant(noto_regular) {
|
||||||
|
let primary_font = regular_variant.clone_font();
|
||||||
|
let font_data = regular_variant.clone_data();
|
||||||
|
let variants: [Option<FontVariant>; 4] = [Some(regular_variant), None, None, None];
|
||||||
|
log::info!("Loaded NotoSansMono as fallback");
|
||||||
|
return (font_data, primary_font, variants);
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("Failed to load any monospace font");
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ fn vs_main(in: VertexInput) -> VertexOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@group(0) @binding(0)
|
@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 {
|
||||||
|
|||||||
@@ -0,0 +1,291 @@
|
|||||||
|
//! GPU data structures for terminal rendering.
|
||||||
|
//!
|
||||||
|
//! Contains vertex formats, uniform structures, and constants for wgpu rendering.
|
||||||
|
//! All structures use `#[repr(C)]` and implement `bytemuck::Pod` for GPU compatibility.
|
||||||
|
|
||||||
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// CONSTANTS
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Size of the glyph atlas texture (like Kitty's max_texture_size).
|
||||||
|
/// 8192x8192 provides massive capacity before needing additional layers.
|
||||||
|
pub const ATLAS_SIZE: u32 = 8192;
|
||||||
|
|
||||||
|
/// Maximum number of atlas layers (like Kitty's max_array_len).
|
||||||
|
/// With 8192x8192 per layer, this provides virtually unlimited glyph storage.
|
||||||
|
pub const MAX_ATLAS_LAYERS: u32 = 64;
|
||||||
|
|
||||||
|
/// Bytes per pixel in the RGBA atlas (4 for RGBA8).
|
||||||
|
pub const ATLAS_BPP: u32 = 4;
|
||||||
|
|
||||||
|
/// Maximum number of simultaneous edge glows.
|
||||||
|
pub const MAX_EDGE_GLOWS: usize = 16;
|
||||||
|
|
||||||
|
/// Color type constants for packed color encoding.
|
||||||
|
pub const COLOR_TYPE_DEFAULT: u32 = 0;
|
||||||
|
pub const COLOR_TYPE_INDEXED: u32 = 1;
|
||||||
|
pub const COLOR_TYPE_RGB: u32 = 2;
|
||||||
|
|
||||||
|
/// Attribute bit flags.
|
||||||
|
pub const ATTR_BOLD: u32 = 0x8;
|
||||||
|
pub const ATTR_ITALIC: u32 = 0x10;
|
||||||
|
pub const ATTR_REVERSE: u32 = 0x20;
|
||||||
|
pub const ATTR_STRIKE: u32 = 0x40;
|
||||||
|
pub const ATTR_DIM: u32 = 0x80;
|
||||||
|
pub const ATTR_UNDERLINE: u32 = 0x1; // Part of decoration mask
|
||||||
|
pub const ATTR_SELECTED: u32 = 0x100; // Cell is selected (for selection highlighting)
|
||||||
|
|
||||||
|
/// Flag for colored glyphs (emoji).
|
||||||
|
pub const COLORED_GLYPH_FLAG: u32 = 0x80000000;
|
||||||
|
|
||||||
|
/// Pre-rendered cursor sprite indices (like Kitty's cursor_shape_map).
|
||||||
|
/// These sprites are created at fixed indices in the sprite array after initialization.
|
||||||
|
/// Index 0 is reserved for "no glyph" (empty cell).
|
||||||
|
pub const CURSOR_SPRITE_BEAM: u32 = 1; // Bar/beam cursor (vertical line on left)
|
||||||
|
pub const CURSOR_SPRITE_UNDERLINE: u32 = 2; // Underline cursor (horizontal line at bottom)
|
||||||
|
pub const CURSOR_SPRITE_HOLLOW: u32 = 3; // Hollow/unfocused cursor (outline rectangle)
|
||||||
|
|
||||||
|
/// Pre-rendered decoration sprite indices (like Kitty's decoration sprites).
|
||||||
|
/// These are created after cursor sprites and used for text decorations.
|
||||||
|
/// The shader uses these to render underlines, strikethrough, etc.
|
||||||
|
pub const DECORATION_SPRITE_STRIKETHROUGH: u32 = 4; // Strikethrough line
|
||||||
|
pub const DECORATION_SPRITE_UNDERLINE: u32 = 5; // Single underline
|
||||||
|
pub const DECORATION_SPRITE_DOUBLE_UNDERLINE: u32 = 6; // Double underline
|
||||||
|
pub const DECORATION_SPRITE_UNDERCURL: u32 = 7; // Wavy/curly underline
|
||||||
|
pub const DECORATION_SPRITE_DOTTED: u32 = 8; // Dotted underline
|
||||||
|
pub const DECORATION_SPRITE_DASHED: u32 = 9; // Dashed underline
|
||||||
|
|
||||||
|
/// First available sprite index for regular glyphs (after reserved cursor and decoration sprites)
|
||||||
|
pub const FIRST_GLYPH_SPRITE: u32 = 10;
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// VERTEX STRUCTURES
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Vertex for rendering textured quads.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||||
|
pub struct GlyphVertex {
|
||||||
|
pub position: [f32; 2],
|
||||||
|
pub uv: [f32; 2],
|
||||||
|
pub color: [f32; 4],
|
||||||
|
pub bg_color: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlyphVertex {
|
||||||
|
pub const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
|
||||||
|
0 => Float32x2, // position
|
||||||
|
1 => Float32x2, // uv
|
||||||
|
2 => Float32x4, // color (fg)
|
||||||
|
3 => Float32x4, // bg_color
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
|
||||||
|
wgpu::VertexBufferLayout {
|
||||||
|
array_stride: std::mem::size_of::<GlyphVertex>() as wgpu::BufferAddress,
|
||||||
|
step_mode: wgpu::VertexStepMode::Vertex,
|
||||||
|
attributes: &Self::ATTRIBS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// EDGE GLOW STRUCTURES
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Per-glow instance data (48 bytes, aligned to 16 bytes).
|
||||||
|
/// Must match GlowInstance in shader.wgsl exactly.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||||
|
pub struct GlowInstance {
|
||||||
|
pub direction: u32,
|
||||||
|
pub progress: f32,
|
||||||
|
pub color_r: f32,
|
||||||
|
pub color_g: f32,
|
||||||
|
pub color_b: f32,
|
||||||
|
// Pane bounds in pixels
|
||||||
|
pub pane_x: f32,
|
||||||
|
pub pane_y: f32,
|
||||||
|
pub pane_width: f32,
|
||||||
|
pub pane_height: f32,
|
||||||
|
pub _padding1: f32,
|
||||||
|
pub _padding2: f32,
|
||||||
|
pub _padding3: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GPU-compatible edge glow uniform data.
|
||||||
|
/// Must match the layout in shader.wgsl exactly.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||||
|
pub struct EdgeGlowUniforms {
|
||||||
|
pub screen_width: f32,
|
||||||
|
pub screen_height: f32,
|
||||||
|
pub terminal_y_offset: f32,
|
||||||
|
pub glow_intensity: f32,
|
||||||
|
pub glow_count: u32,
|
||||||
|
pub _padding: [u32; 3], // Pad to 16-byte alignment before array
|
||||||
|
pub glows: [GlowInstance; MAX_EDGE_GLOWS],
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// IMAGE STRUCTURES
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// GPU-compatible image uniform data.
|
||||||
|
/// Must match the layout in image_shader.wgsl exactly.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, Pod, Zeroable)]
|
||||||
|
pub struct ImageUniforms {
|
||||||
|
pub screen_width: f32,
|
||||||
|
pub screen_height: f32,
|
||||||
|
pub pos_x: f32,
|
||||||
|
pub pos_y: f32,
|
||||||
|
pub display_width: f32,
|
||||||
|
pub display_height: f32,
|
||||||
|
pub src_x: f32,
|
||||||
|
pub src_y: f32,
|
||||||
|
pub src_width: f32,
|
||||||
|
pub src_height: f32,
|
||||||
|
pub _padding1: f32,
|
||||||
|
pub _padding2: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// INSTANCED CELL RENDERING STRUCTURES
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// GPU cell data for instanced rendering.
|
||||||
|
/// Matches GPUCell in glyph_shader.wgsl exactly.
|
||||||
|
///
|
||||||
|
/// Like Kitty, we store a sprite_idx that references pre-rendered glyphs in the atlas.
|
||||||
|
/// This allows us to update GPU buffers with a simple memcpy when content changes,
|
||||||
|
/// rather than rebuilding vertex buffers every frame.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||||
|
pub struct GPUCell {
|
||||||
|
/// Foreground color (packed: type in low byte, then RGB or index)
|
||||||
|
pub fg: u32,
|
||||||
|
/// Background color (packed: type in low byte, then RGB or index)
|
||||||
|
pub bg: u32,
|
||||||
|
/// Decoration foreground color (for underlines, etc.)
|
||||||
|
pub decoration_fg: u32,
|
||||||
|
/// Sprite index in the sprite info array. High bit set = colored glyph.
|
||||||
|
/// 0 = no glyph (space or empty)
|
||||||
|
pub sprite_idx: u32,
|
||||||
|
/// Cell attributes (bold, italic, reverse, etc.)
|
||||||
|
pub attrs: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sprite info for glyph positioning.
|
||||||
|
/// Matches SpriteInfo in glyph_shader.wgsl exactly.
|
||||||
|
///
|
||||||
|
/// In Kitty's model, sprites are always cell-sized and glyphs are pre-positioned
|
||||||
|
/// within the sprite at the correct baseline. The shader just maps the sprite
|
||||||
|
/// to the cell 1:1, with no offset math needed.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||||
|
pub struct SpriteInfo {
|
||||||
|
/// UV coordinates in atlas (x, y, width, height) - normalized 0-1
|
||||||
|
pub uv: [f32; 4],
|
||||||
|
/// Atlas layer index (z-coordinate for texture array) and padding
|
||||||
|
/// layer is the first f32, second f32 is unused padding
|
||||||
|
pub layer: f32,
|
||||||
|
pub _padding: f32,
|
||||||
|
/// Size in pixels (width, height) - always matches cell dimensions
|
||||||
|
pub size: [f32; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Font cell metrics with integer dimensions (like Kitty's FontCellMetrics).
|
||||||
|
/// Using integers ensures pixel-perfect alignment and avoids floating-point precision issues.
|
||||||
|
#[derive(Copy, Clone, Debug)]
|
||||||
|
pub struct FontCellMetrics {
|
||||||
|
/// Cell width in pixels (computed using ceil from font advance).
|
||||||
|
pub cell_width: u32,
|
||||||
|
/// Cell height in pixels (computed using ceil from font height).
|
||||||
|
pub cell_height: u32,
|
||||||
|
/// Baseline offset from top of cell in pixels.
|
||||||
|
pub baseline: u32,
|
||||||
|
/// Y position for underline (from top of cell, in pixels).
|
||||||
|
/// Computed from font metrics: ascender - underline_position.
|
||||||
|
pub underline_position: u32,
|
||||||
|
/// Thickness of underline in pixels.
|
||||||
|
pub underline_thickness: u32,
|
||||||
|
/// Y position for strikethrough (from top of cell, in pixels).
|
||||||
|
/// Typically around 65% of baseline from top.
|
||||||
|
pub strikethrough_position: u32,
|
||||||
|
/// Thickness of strikethrough in pixels.
|
||||||
|
pub strikethrough_thickness: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grid parameters uniform for instanced rendering.
|
||||||
|
/// Matches GridParams in glyph_shader.wgsl exactly.
|
||||||
|
/// Uses Kitty-style NDC positioning: viewport is set per-pane, so shader
|
||||||
|
/// works in pure NDC space without needing pixel offsets.
|
||||||
|
/// Cell dimensions are integers like Kitty for pixel-perfect rendering.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||||
|
pub struct GridParams {
|
||||||
|
pub cols: u32,
|
||||||
|
pub rows: u32,
|
||||||
|
pub cell_width: u32,
|
||||||
|
pub cell_height: u32,
|
||||||
|
pub cursor_col: i32,
|
||||||
|
pub cursor_row: i32,
|
||||||
|
pub cursor_style: u32,
|
||||||
|
pub background_opacity: f32,
|
||||||
|
// Selection range (-1 values mean no selection)
|
||||||
|
pub selection_start_col: i32,
|
||||||
|
pub selection_start_row: i32,
|
||||||
|
pub selection_end_col: i32,
|
||||||
|
pub selection_end_row: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GPU quad instance for instanced rectangle rendering.
|
||||||
|
/// Matches Quad in glyph_shader.wgsl exactly.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||||
|
pub struct Quad {
|
||||||
|
/// X position in pixels
|
||||||
|
pub x: f32,
|
||||||
|
/// Y position in pixels
|
||||||
|
pub y: f32,
|
||||||
|
/// Width in pixels
|
||||||
|
pub width: f32,
|
||||||
|
/// Height in pixels
|
||||||
|
pub height: f32,
|
||||||
|
/// Color (linear RGBA)
|
||||||
|
pub color: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters for quad rendering.
|
||||||
|
/// Matches QuadParams in glyph_shader.wgsl exactly.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||||
|
pub struct QuadParams {
|
||||||
|
pub screen_width: f32,
|
||||||
|
pub screen_height: f32,
|
||||||
|
pub _padding: [f32; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters for statusline rendering.
|
||||||
|
/// Matches StatuslineParams in statusline_shader.wgsl exactly.
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)]
|
||||||
|
pub struct StatuslineParams {
|
||||||
|
/// Number of characters in statusline
|
||||||
|
pub char_count: u32,
|
||||||
|
/// Cell width in pixels
|
||||||
|
pub cell_width: f32,
|
||||||
|
/// Cell height in pixels
|
||||||
|
pub cell_height: f32,
|
||||||
|
/// Screen width in pixels
|
||||||
|
pub screen_width: f32,
|
||||||
|
/// Screen height in pixels
|
||||||
|
pub screen_height: f32,
|
||||||
|
/// Y offset from top of screen in pixels
|
||||||
|
pub y_offset: f32,
|
||||||
|
/// Padding for alignment (to match shader struct layout)
|
||||||
|
pub _padding: [f32; 2],
|
||||||
|
}
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
//! Image rendering for the Kitty Graphics Protocol.
|
||||||
|
//!
|
||||||
|
//! This module handles GPU-accelerated rendering of images in the terminal,
|
||||||
|
//! supporting the Kitty Graphics Protocol for inline image display.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use crate::gpu_types::ImageUniforms;
|
||||||
|
use crate::graphics::{ImageData, ImagePlacement, ImageStorage};
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// GPU IMAGE
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Cached GPU texture for an image.
|
||||||
|
pub struct GpuImage {
|
||||||
|
pub texture: wgpu::Texture,
|
||||||
|
pub view: wgpu::TextureView,
|
||||||
|
pub uniform_buffer: wgpu::Buffer,
|
||||||
|
pub bind_group: wgpu::BindGroup,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// IMAGE RENDERER
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Manages GPU resources for image rendering.
|
||||||
|
/// Handles uploading, caching, and preparing images for rendering.
|
||||||
|
pub struct ImageRenderer {
|
||||||
|
/// Bind group layout for image rendering.
|
||||||
|
bind_group_layout: wgpu::BindGroupLayout,
|
||||||
|
/// Sampler for image textures.
|
||||||
|
sampler: wgpu::Sampler,
|
||||||
|
/// Cached GPU textures for images, keyed by image ID.
|
||||||
|
textures: HashMap<u32, GpuImage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImageRenderer {
|
||||||
|
/// Create a new ImageRenderer with the necessary GPU resources.
|
||||||
|
pub fn new(device: &wgpu::Device) -> Self {
|
||||||
|
// Create sampler for images (linear filtering for smooth scaling)
|
||||||
|
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||||
|
label: Some("Image Sampler"),
|
||||||
|
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||||
|
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||||
|
mag_filter: wgpu::FilterMode::Linear,
|
||||||
|
min_filter: wgpu::FilterMode::Linear,
|
||||||
|
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create bind group layout for images
|
||||||
|
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("Image Bind Group Layout"),
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 0,
|
||||||
|
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Buffer {
|
||||||
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
|
has_dynamic_offset: false,
|
||||||
|
min_binding_size: None,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 1,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Texture {
|
||||||
|
sample_type: wgpu::TextureSampleType::Float { filterable: true },
|
||||||
|
view_dimension: wgpu::TextureViewDimension::D2,
|
||||||
|
multisampled: false,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
wgpu::BindGroupLayoutEntry {
|
||||||
|
binding: 2,
|
||||||
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
|
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
bind_group_layout,
|
||||||
|
sampler,
|
||||||
|
textures: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the bind group layout for creating the image pipeline.
|
||||||
|
pub fn bind_group_layout(&self) -> &wgpu::BindGroupLayout {
|
||||||
|
&self.bind_group_layout
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a GPU image by ID.
|
||||||
|
pub fn get(&self, image_id: &u32) -> Option<&GpuImage> {
|
||||||
|
self.textures.get(image_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload an image to the GPU, creating or updating its texture.
|
||||||
|
pub fn upload_image(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, image: &ImageData) {
|
||||||
|
// Get current frame data (handles animation frames automatically)
|
||||||
|
let data = image.current_frame_data();
|
||||||
|
|
||||||
|
// Check if we already have this image
|
||||||
|
if let Some(existing) = self.textures.get(&image.id) {
|
||||||
|
if existing.width == image.width && existing.height == image.height {
|
||||||
|
// Same dimensions, just update the data
|
||||||
|
queue.write_texture(
|
||||||
|
wgpu::TexelCopyTextureInfo {
|
||||||
|
texture: &existing.texture,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: wgpu::Origin3d::ZERO,
|
||||||
|
aspect: wgpu::TextureAspect::All,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
wgpu::TexelCopyBufferLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: Some(image.width * 4),
|
||||||
|
rows_per_image: Some(image.height),
|
||||||
|
},
|
||||||
|
wgpu::Extent3d {
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Different dimensions, need to recreate
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new texture
|
||||||
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some(&format!("Image {}", image.id)),
|
||||||
|
size: wgpu::Extent3d {
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||||
|
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload the data
|
||||||
|
queue.write_texture(
|
||||||
|
wgpu::TexelCopyTextureInfo {
|
||||||
|
texture: &texture,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: wgpu::Origin3d::ZERO,
|
||||||
|
aspect: wgpu::TextureAspect::All,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
wgpu::TexelCopyBufferLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: Some(image.width * 4),
|
||||||
|
rows_per_image: Some(image.height),
|
||||||
|
},
|
||||||
|
wgpu::Extent3d {
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
// Create per-image uniform buffer
|
||||||
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some(&format!("Image {} Uniform Buffer", image.id)),
|
||||||
|
size: std::mem::size_of::<ImageUniforms>() as u64,
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create bind group for this image with its own uniform buffer
|
||||||
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some(&format!("Image {} Bind Group", image.id)),
|
||||||
|
layout: &self.bind_group_layout,
|
||||||
|
entries: &[
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: uniform_buffer.as_entire_binding(),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 1,
|
||||||
|
resource: wgpu::BindingResource::TextureView(&view),
|
||||||
|
},
|
||||||
|
wgpu::BindGroupEntry {
|
||||||
|
binding: 2,
|
||||||
|
resource: wgpu::BindingResource::Sampler(&self.sampler),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
self.textures.insert(image.id, GpuImage {
|
||||||
|
texture,
|
||||||
|
view,
|
||||||
|
uniform_buffer,
|
||||||
|
bind_group,
|
||||||
|
width: image.width,
|
||||||
|
height: image.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
log::debug!(
|
||||||
|
"Uploaded image {} ({}x{}) to GPU",
|
||||||
|
image.id,
|
||||||
|
image.width,
|
||||||
|
image.height
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove an image from the GPU.
|
||||||
|
pub fn remove_image(&mut self, image_id: u32) {
|
||||||
|
if self.textures.remove(&image_id).is_some() {
|
||||||
|
log::debug!("Removed image {} from GPU", image_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sync images from terminal's image storage to GPU.
|
||||||
|
/// Uploads new/changed images and removes deleted ones.
|
||||||
|
/// Also updates animation frames.
|
||||||
|
pub fn sync_images(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, storage: &mut ImageStorage) {
|
||||||
|
// Update animations and get list of changed image IDs
|
||||||
|
let changed_ids = storage.update_animations();
|
||||||
|
|
||||||
|
// Re-upload frames that changed due to animation
|
||||||
|
for id in &changed_ids {
|
||||||
|
if let Some(image) = storage.get_image(*id) {
|
||||||
|
self.upload_image(device, queue, image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !storage.dirty && changed_ids.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload all images (upload_image handles deduplication)
|
||||||
|
for image in storage.images().values() {
|
||||||
|
self.upload_image(device, queue, image);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove textures for deleted images
|
||||||
|
let current_ids: std::collections::HashSet<u32> = storage.images().keys().copied().collect();
|
||||||
|
let gpu_ids: Vec<u32> = self.textures.keys().copied().collect();
|
||||||
|
for id in gpu_ids {
|
||||||
|
if !current_ids.contains(&id) {
|
||||||
|
self.remove_image(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storage.clear_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare image renders for a pane.
|
||||||
|
/// Returns a Vec of (image_id, uniforms) for deferred rendering.
|
||||||
|
pub fn prepare_image_renders(
|
||||||
|
&self,
|
||||||
|
placements: &[ImagePlacement],
|
||||||
|
pane_x: f32,
|
||||||
|
pane_y: f32,
|
||||||
|
cell_width: f32,
|
||||||
|
cell_height: f32,
|
||||||
|
screen_width: f32,
|
||||||
|
screen_height: f32,
|
||||||
|
scrollback_len: usize,
|
||||||
|
scroll_offset: usize,
|
||||||
|
visible_rows: usize,
|
||||||
|
) -> Vec<(u32, ImageUniforms)> {
|
||||||
|
let mut renders = Vec::new();
|
||||||
|
|
||||||
|
for placement in placements {
|
||||||
|
// Check if we have the GPU texture for this image
|
||||||
|
let gpu_image = match self.textures.get(&placement.image_id) {
|
||||||
|
Some(img) => img,
|
||||||
|
None => continue, // Skip if not uploaded yet
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert absolute row to visible screen row
|
||||||
|
// placement.row is absolute (scrollback_len_at_placement + cursor_row)
|
||||||
|
// visible_row = absolute_row - scrollback_len + scroll_offset
|
||||||
|
let absolute_row = placement.row as isize;
|
||||||
|
let visible_row = absolute_row - scrollback_len as isize + scroll_offset as isize;
|
||||||
|
|
||||||
|
// Check if image is visible on screen
|
||||||
|
// Image spans from visible_row to visible_row + placement.rows
|
||||||
|
let image_bottom = visible_row + placement.rows as isize;
|
||||||
|
if image_bottom < 0 || visible_row >= visible_rows as isize {
|
||||||
|
continue; // Image is completely off-screen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate display position in pixels
|
||||||
|
let pos_x = pane_x + (placement.col as f32 * cell_width) + placement.x_offset as f32;
|
||||||
|
let pos_y = pane_y + (visible_row as f32 * cell_height) + placement.y_offset as f32;
|
||||||
|
|
||||||
|
log::debug!(
|
||||||
|
"Image render: pane_x={} col={} cell_width={} x_offset={} => pos_x={}",
|
||||||
|
pane_x, placement.col, cell_width, placement.x_offset, pos_x
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate display size in pixels
|
||||||
|
let display_width = placement.cols as f32 * cell_width;
|
||||||
|
let display_height = placement.rows as f32 * cell_height;
|
||||||
|
|
||||||
|
// Calculate source rectangle in normalized coordinates
|
||||||
|
let src_x = placement.src_x as f32 / gpu_image.width as f32;
|
||||||
|
let src_y = placement.src_y as f32 / gpu_image.height as f32;
|
||||||
|
let src_width = if placement.src_width == 0 {
|
||||||
|
1.0 - src_x
|
||||||
|
} else {
|
||||||
|
placement.src_width as f32 / gpu_image.width as f32
|
||||||
|
};
|
||||||
|
let src_height = if placement.src_height == 0 {
|
||||||
|
1.0 - src_y
|
||||||
|
} else {
|
||||||
|
placement.src_height as f32 / gpu_image.height as f32
|
||||||
|
};
|
||||||
|
|
||||||
|
let uniforms = ImageUniforms {
|
||||||
|
screen_width,
|
||||||
|
screen_height,
|
||||||
|
pos_x,
|
||||||
|
pos_y,
|
||||||
|
display_width,
|
||||||
|
display_height,
|
||||||
|
src_x,
|
||||||
|
src_y,
|
||||||
|
src_width,
|
||||||
|
src_height,
|
||||||
|
_padding1: 0.0,
|
||||||
|
_padding2: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
renders.push((placement.image_id, uniforms));
|
||||||
|
}
|
||||||
|
|
||||||
|
renders
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -2,10 +2,21 @@
|
|||||||
//!
|
//!
|
||||||
//! Single-process architecture: one process owns PTY, terminal state, and rendering.
|
//! 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
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
|||||||
|
//! Per-pane GPU resources for multi-pane terminal rendering.
|
||||||
|
//!
|
||||||
|
//! This module provides GPU resource management for individual terminal panes,
|
||||||
|
//! following Kitty's VAO-per-window approach where each pane gets its own
|
||||||
|
//! buffers and bind group for independent rendering.
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// PER-PANE GPU RESOURCES (Like Kitty's VAO per window)
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// GPU resources for a single pane.
|
||||||
|
/// Like Kitty's VAO, each pane gets its own buffers and bind group.
|
||||||
|
/// This allows uploading each pane's cell data independently before rendering.
|
||||||
|
pub struct PaneGpuResources {
|
||||||
|
/// Cell storage buffer - contains GPUCell array for this pane's visible cells.
|
||||||
|
pub cell_buffer: wgpu::Buffer,
|
||||||
|
/// Grid parameters uniform buffer for this pane.
|
||||||
|
pub grid_params_buffer: wgpu::Buffer,
|
||||||
|
/// Bind group for instanced rendering (@group(1)) - references this pane's buffers.
|
||||||
|
pub bind_group: wgpu::BindGroup,
|
||||||
|
/// Buffer capacity (max cells) - used to detect when buffer needs resizing.
|
||||||
|
pub capacity: usize,
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
//! Render pipeline builder for wgpu.
|
||||||
|
//!
|
||||||
|
//! This module provides a builder pattern for creating wgpu render pipelines
|
||||||
|
//! with common settings, reducing boilerplate when creating multiple pipelines.
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// PIPELINE BUILDER
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/// Builder for creating render pipelines with common settings.
|
||||||
|
/// Captures the device, shader, layout, and format that are shared across many pipelines.
|
||||||
|
pub struct PipelineBuilder<'a> {
|
||||||
|
device: &'a wgpu::Device,
|
||||||
|
shader: &'a wgpu::ShaderModule,
|
||||||
|
layout: &'a wgpu::PipelineLayout,
|
||||||
|
format: wgpu::TextureFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> PipelineBuilder<'a> {
|
||||||
|
/// Create a new pipeline builder with shared settings.
|
||||||
|
pub fn new(
|
||||||
|
device: &'a wgpu::Device,
|
||||||
|
shader: &'a wgpu::ShaderModule,
|
||||||
|
layout: &'a wgpu::PipelineLayout,
|
||||||
|
format: wgpu::TextureFormat,
|
||||||
|
) -> Self {
|
||||||
|
Self { device, shader, layout, format }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a pipeline with TriangleStrip topology and no vertex buffers (most common case).
|
||||||
|
pub fn build(&self, label: &str, vs_entry: &str, fs_entry: &str, blend: wgpu::BlendState) -> wgpu::RenderPipeline {
|
||||||
|
self.build_full(label, vs_entry, fs_entry, blend, wgpu::PrimitiveTopology::TriangleStrip, &[])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a pipeline with custom topology and vertex buffers.
|
||||||
|
pub fn build_full(
|
||||||
|
&self,
|
||||||
|
label: &str,
|
||||||
|
vs_entry: &str,
|
||||||
|
fs_entry: &str,
|
||||||
|
blend: wgpu::BlendState,
|
||||||
|
topology: wgpu::PrimitiveTopology,
|
||||||
|
vertex_buffers: &[wgpu::VertexBufferLayout<'_>],
|
||||||
|
) -> wgpu::RenderPipeline {
|
||||||
|
self.device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some(label),
|
||||||
|
layout: Some(self.layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: self.shader,
|
||||||
|
entry_point: Some(vs_entry),
|
||||||
|
buffers: vertex_buffers,
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: self.shader,
|
||||||
|
entry_point: Some(fs_entry),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: self.format,
|
||||||
|
blend: Some(blend),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: wgpu::PipelineCompilationOptions::default(),
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology,
|
||||||
|
strip_index_format: None,
|
||||||
|
front_face: wgpu::FrontFace::Ccw,
|
||||||
|
cull_mode: None,
|
||||||
|
polygon_mode: wgpu::PolygonMode::Fill,
|
||||||
|
unclipped_depth: false,
|
||||||
|
conservative: false,
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState::default(),
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+457
-3917
File diff suppressed because it is too large
Load Diff
+1476
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
|||||||
|
//! Statusline types and rendering.
|
||||||
|
//!
|
||||||
|
//! Provides data structures for building structured statusline content with
|
||||||
|
//! powerline-style sections and components.
|
||||||
|
|
||||||
|
/// Color specification for statusline components.
|
||||||
|
/// Uses the terminal's indexed color palette (0-255), RGB, or default fg.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum StatuslineColor {
|
||||||
|
/// Use the default foreground color.
|
||||||
|
Default,
|
||||||
|
/// Use an indexed color from the 256-color palette (0-15 for ANSI colors).
|
||||||
|
Indexed(u8),
|
||||||
|
/// Use an RGB color.
|
||||||
|
Rgb(u8, u8, u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StatuslineColor {
|
||||||
|
fn default() -> Self {
|
||||||
|
StatuslineColor::Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single component/segment of the statusline.
|
||||||
|
/// Components are rendered left-to-right with optional separators.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StatuslineComponent {
|
||||||
|
/// The text content of this component.
|
||||||
|
pub text: String,
|
||||||
|
/// Foreground color for this component.
|
||||||
|
pub fg: StatuslineColor,
|
||||||
|
/// Whether this text should be bold.
|
||||||
|
pub bold: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatuslineComponent {
|
||||||
|
/// Create a new statusline component with default styling.
|
||||||
|
pub fn new(text: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.into(),
|
||||||
|
fg: StatuslineColor::Default,
|
||||||
|
bold: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the foreground color using an indexed palette color.
|
||||||
|
pub fn fg(mut self, color_index: u8) -> Self {
|
||||||
|
self.fg = StatuslineColor::Indexed(color_index);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the foreground color using RGB values.
|
||||||
|
pub fn rgb_fg(mut self, r: u8, g: u8, b: u8) -> Self {
|
||||||
|
self.fg = StatuslineColor::Rgb(r, g, b);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set bold styling.
|
||||||
|
pub fn bold(mut self) -> Self {
|
||||||
|
self.bold = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a separator component (e.g., "/", " > ", etc.).
|
||||||
|
pub fn separator(text: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.into(),
|
||||||
|
fg: StatuslineColor::Indexed(8), // Dim gray by default
|
||||||
|
bold: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A section of the statusline with its own background color.
|
||||||
|
/// Sections are rendered left-to-right and end with a powerline transition arrow.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StatuslineSection {
|
||||||
|
/// The components within this section.
|
||||||
|
pub components: Vec<StatuslineComponent>,
|
||||||
|
/// Background color for this section.
|
||||||
|
pub bg: StatuslineColor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatuslineSection {
|
||||||
|
/// Create a new section with the given indexed background color.
|
||||||
|
pub fn new(bg_color: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
components: Vec::new(),
|
||||||
|
bg: StatuslineColor::Indexed(bg_color),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new section with an RGB background color.
|
||||||
|
pub fn with_rgb_bg(r: u8, g: u8, b: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
components: Vec::new(),
|
||||||
|
bg: StatuslineColor::Rgb(r, g, b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new section with the default (transparent) background.
|
||||||
|
pub fn transparent() -> Self {
|
||||||
|
Self {
|
||||||
|
components: Vec::new(),
|
||||||
|
bg: StatuslineColor::Default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a component to this section.
|
||||||
|
pub fn push(mut self, component: StatuslineComponent) -> Self {
|
||||||
|
self.components.push(component);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add multiple components to this section.
|
||||||
|
pub fn with_components(mut self, components: Vec<StatuslineComponent>) -> Self {
|
||||||
|
self.components = components;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Content to display in the statusline.
|
||||||
|
/// Either structured sections (for ZTerm's default CWD/git display) or raw ANSI
|
||||||
|
/// content (from neovim or other programs that provide their own statusline).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum StatuslineContent {
|
||||||
|
/// Structured sections with powerline-style transitions.
|
||||||
|
Sections(Vec<StatuslineSection>),
|
||||||
|
/// Raw ANSI-formatted string (rendered as-is without section styling).
|
||||||
|
Raw(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StatuslineContent {
|
||||||
|
fn default() -> Self {
|
||||||
|
StatuslineContent::Sections(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,7 +103,7 @@ struct ColorTable {
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
@group(0) @binding(0)
|
@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
@@ -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 = ¶ms.params;
|
||||||
|
let is_sub = ¶ms.is_sub_param;
|
||||||
|
|
||||||
|
// Check for sub-parameter format (38:2:r:g:b or 38:5:idx)
|
||||||
|
if i + 1 < num && is_sub[i + 1] {
|
||||||
|
let mode = p[i + 1];
|
||||||
|
if mode == 5 && i + 2 < num {
|
||||||
|
return Some((Color::Indexed(p[i + 2] as u8), 2));
|
||||||
|
} else if mode == 2 && i + 4 < num {
|
||||||
|
return Some((Color::Rgb(
|
||||||
|
p[i + 2] as u8,
|
||||||
|
p[i + 3] as u8,
|
||||||
|
p[i + 4] as u8,
|
||||||
|
), 4));
|
||||||
|
}
|
||||||
|
} else if i + 2 < num {
|
||||||
|
// Regular format (38;2;r;g;b or 38;5;idx)
|
||||||
|
let mode = p[i + 1];
|
||||||
|
if mode == 5 {
|
||||||
|
return Some((Color::Indexed(p[i + 2] as u8), 2));
|
||||||
|
} else if mode == 2 && i + 4 < num {
|
||||||
|
return Some((Color::Rgb(
|
||||||
|
p[i + 2] as u8,
|
||||||
|
p[i + 3] as u8,
|
||||||
|
p[i + 4] as u8,
|
||||||
|
), 4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle SGR (Select Graphic Rendition) parameters.
|
/// 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 = ¶ms.params;
|
||||||
|
let is_sub = ¶ms.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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user