Compare commits
9 Commits
3715ecf841
...
feat/0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 17794aba0b | |||
| aed22a3dbc | |||
| 5ba5a52c14 | |||
| e5ce246760 | |||
| a3fa7eb9d2 | |||
| fdc923621e | |||
| ebd3d4008b | |||
| ae56f78075 | |||
| b0ecc5623c |
17
AGENTS.md
Normal file
17
AGENTS.md
Normal file
@@ -0,0 +1,17 @@
|
||||
## Agent Instructions
|
||||
|
||||
### Build, Lint, and Test
|
||||
|
||||
- Build: `cargo build`
|
||||
- Lint: `cargo clippy`
|
||||
- Test: `cargo test`
|
||||
- Run a single test: `cargo test --test <test_name>`
|
||||
|
||||
### Code Style
|
||||
|
||||
- **Formatting**: Use `cargo fmt` to format the code.
|
||||
- **Imports**: Group imports by standard library, external crates, and project modules.
|
||||
- **Types**: Use explicit types.
|
||||
- **Naming Conventions**: Follow Rust's naming conventions (e.g., `snake_case` for variables and functions, `PascalCase` for types).
|
||||
- **Error Handling**: Use `Result` and `?` for error propagation. Use `panic!` only for unrecoverable errors.
|
||||
- **Logging**: Use the `log` crate for logging.
|
||||
184
Cargo.lock
generated
184
Cargo.lock
generated
@@ -91,6 +91,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
@@ -271,6 +277,12 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
@@ -370,6 +382,15 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
@@ -472,6 +493,17 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filedescriptor"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"thiserror 1.0.69",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
@@ -802,6 +834,12 @@ version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
@@ -937,7 +975,7 @@ dependencies = [
|
||||
"arrayvec",
|
||||
"bit-set",
|
||||
"bitflags 2.9.0",
|
||||
"cfg_aliases",
|
||||
"cfg_aliases 0.2.1",
|
||||
"codespan-reporting",
|
||||
"half",
|
||||
"hashbrown",
|
||||
@@ -992,6 +1030,18 @@ dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases 0.1.1",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -1023,12 +1073,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numtoa"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f"
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
@@ -1365,12 +1409,39 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "portable-pty"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 1.3.2",
|
||||
"downcast-rs",
|
||||
"filedescriptor",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"nix",
|
||||
"serial2",
|
||||
"shared_library",
|
||||
"shell-words",
|
||||
"winapi",
|
||||
"winreg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "presser"
|
||||
version = "0.3.1"
|
||||
@@ -1471,12 +1542,6 @@ dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_termios"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
@@ -1632,6 +1697,33 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial2"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7d1d08630509d69f90eff4afcd02c3bd974d979225cbd815ff5942351b14375"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_library"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -1651,11 +1743,14 @@ dependencies = [
|
||||
name = "simplicitty"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"env_logger",
|
||||
"glyphon",
|
||||
"log",
|
||||
"termion",
|
||||
"pollster",
|
||||
"portable-pty",
|
||||
"tokio",
|
||||
"vte",
|
||||
"wgpu",
|
||||
"winit",
|
||||
]
|
||||
@@ -1827,18 +1922,6 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termion"
|
||||
version = "4.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3669a69de26799d6321a5aa713f55f7e2cd37bd47be044b50f2acafc42c122bb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libredox",
|
||||
"numtoa",
|
||||
"redox_termios",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@@ -2059,6 +2142,16 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "vte"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
@@ -2292,7 +2385,7 @@ checksum = "ca6049eb2014a0e0d8689f9b787605dd71d5bbfdc74095ead499f3cff705c229"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags 2.9.0",
|
||||
"cfg_aliases",
|
||||
"cfg_aliases 0.2.1",
|
||||
"document-features",
|
||||
"hashbrown",
|
||||
"js-sys",
|
||||
@@ -2322,7 +2415,7 @@ dependencies = [
|
||||
"bit-set",
|
||||
"bit-vec",
|
||||
"bitflags 2.9.0",
|
||||
"cfg_aliases",
|
||||
"cfg_aliases 0.2.1",
|
||||
"document-features",
|
||||
"hashbrown",
|
||||
"indexmap",
|
||||
@@ -2384,7 +2477,7 @@ dependencies = [
|
||||
"block",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"cfg_aliases 0.2.1",
|
||||
"core-graphics-types",
|
||||
"glow",
|
||||
"glutin_wgl_sys",
|
||||
@@ -2431,6 +2524,22 @@ dependencies = [
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
@@ -2440,6 +2549,12 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.58.0"
|
||||
@@ -2722,7 +2837,7 @@ dependencies = [
|
||||
"block2",
|
||||
"bytemuck",
|
||||
"calloop",
|
||||
"cfg_aliases",
|
||||
"cfg_aliases 0.2.1",
|
||||
"concurrent-queue",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
@@ -2770,6 +2885,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
|
||||
@@ -9,8 +9,13 @@ tokio = { version = "1.44", features = ["full"] }
|
||||
winit = { version = "0.30", features = ["wayland", "x11"] }
|
||||
wgpu = "25"
|
||||
glyphon = "0.9"
|
||||
portable-pty = "0.9"
|
||||
vte = "0.15"
|
||||
|
||||
termion = "4"
|
||||
# For reading from the PTY in a non-blocking way
|
||||
crossbeam-channel = "0.5"
|
||||
|
||||
# For logging and running the event loop
|
||||
env_logger = "0.10"
|
||||
log = "0.4"
|
||||
pollster = "0.3"
|
||||
|
||||
357
README.md
357
README.md
@@ -1,350 +1,11 @@
|
||||
# simplicitty
|
||||
# Simplicitty
|
||||
|
||||
A simple terminal emulator
|
||||
## Roadmap
|
||||
|
||||
## 1. Implement Incremental Rendering
|
||||
|
||||
Currently, your renderer appears to redraw the entire terminal content on each
|
||||
frame. For a terminal emulator, this is inefficient since only small portions
|
||||
typically change at once.
|
||||
|
||||
rust // Add to TerminalText struct dirty_regions: Vec<Rectangle>, // Store
|
||||
regions that need updating last_frame_content: String, // Store previous frame's
|
||||
content
|
||||
|
||||
Only redraw areas that have changed since the last frame by tracking dirty
|
||||
regions.
|
||||
|
||||
## 2. Implement Text Caching
|
||||
|
||||
The current implementation reshapes all text on every frame. Instead:
|
||||
|
||||
rust // Add to TerminalText cached_lines: HashMap<String, CachedTextLine>,
|
||||
|
||||
Cache shaped text lines and only reshape when content changes or window resizes.
|
||||
|
||||
## 3. Use Double Buffering for Text
|
||||
|
||||
Maintain two buffers - one for the current frame and one for the next:
|
||||
|
||||
rust // In TerminalText front_buffer: Buffer, back_buffer: Buffer,
|
||||
|
||||
Prepare the back buffer while rendering the front buffer, then swap them.
|
||||
|
||||
## 4. Optimize Atlas Trimming
|
||||
|
||||
The trim() call after every frame can be expensive. Consider:
|
||||
|
||||
rust // In TerminalText frames_since_trim: u32,
|
||||
|
||||
pub fn trim(&mut self) { self.frames_since_trim += 1; if
|
||||
self.frames_since_trim > 30 { // Only trim every 30 frames self.atlas.trim();
|
||||
self.frames_since_trim = 0; } }
|
||||
|
||||
## 5. Implement View Culling
|
||||
|
||||
Only render text that's actually visible:
|
||||
|
||||
rust // In prepare() method let visible_area = TextBounds { left: 0, top:
|
||||
scroll_position, right: config.width as i32, bottom: scroll_position +
|
||||
config.height as i32, };
|
||||
|
||||
## 6. Use Asynchronous Text Preparation
|
||||
|
||||
Move text shaping to a background thread:
|
||||
|
||||
rust // In Application struct text_preparation_pool: ThreadPool,
|
||||
|
||||
// Then queue text preparation tasks self.text_preparation_pool.spawn(move || {
|
||||
// Shape text here // Signal completion via channel });
|
||||
|
||||
## 7. Optimize GPU Memory Usage
|
||||
|
||||
The current implementation might be creating new GPU resources frequently.
|
||||
Implement resource pooling:
|
||||
|
||||
rust // In TerminalText texture_pool: Vec<wgpu::Texture>, // Reuse textures
|
||||
instead of creating new ones
|
||||
|
||||
## 8. Implement Partial Updates for the Text Atlas
|
||||
|
||||
Instead of rebuilding the entire text atlas, update only changed glyphs:
|
||||
|
||||
rust // Add method to TextAtlas pub fn update_region(&mut self, device: &Device,
|
||||
queue: &Queue, region: Rectangle) { // Update only the specified region }
|
||||
|
||||
## 9. Use Compute Shaders for Text Processing
|
||||
|
||||
For complex text operations, consider using compute shaders:
|
||||
|
||||
rust // Create a compute pipeline for text processing let compute_pipeline =
|
||||
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { // Configure
|
||||
for text processing });
|
||||
|
||||
## 10. Implement Frame Skipping
|
||||
|
||||
Skip rendering frames when nothing has changed:
|
||||
|
||||
rust pub fn render(&mut self) -> Result<(), SurfaceError> { if !self.has_changes
|
||||
{ return Ok(()); } // Render as normal }
|
||||
|
||||
## 11. Profile and Optimize Hot Paths
|
||||
|
||||
Use a profiler to identify bottlenecks in your rendering pipeline. Common issues
|
||||
might be: • Excessive GPU state changes • Inefficient text shaping • Unnecessary
|
||||
buffer updates
|
||||
|
||||
## 12. Implement Proper Vsync Control
|
||||
|
||||
Your current implementation uses PresentMode::Fifo (VSync), which is good for
|
||||
power efficiency but might not be optimal for all cases:
|
||||
|
||||
rust // Allow configurable present mode based on user preference present_mode:
|
||||
if vsync_enabled { wgpu::PresentMode::Fifo } else { wgpu::PresentMode::Immediate
|
||||
},
|
||||
|
||||
Based on your project structure and the README.md optimization suggestions,
|
||||
here's how you can implement incremental text buffer updates:
|
||||
|
||||
## 1. Track Changes with Dirty Regions
|
||||
|
||||
First, modify your TerminalText struct to track which parts of the text have
|
||||
changed:
|
||||
|
||||
rust pub struct TerminalText { font_system: FontSystem, cache: SwashCache,
|
||||
viewport: Viewport, atlas: TextAtlas, buffer: Buffer, renderer: TextRenderer,
|
||||
|
||||
// Add these fields to track changes
|
||||
last_content: String,
|
||||
dirty_regions: Vec<TextBounds>,
|
||||
cursor_position: usize,
|
||||
|
||||
}
|
||||
|
||||
## 2. Implement Incremental Text Updates
|
||||
|
||||
Instead of replacing the entire text on each update, implement methods to
|
||||
insert, delete, or modify text at specific positions:
|
||||
|
||||
```rust
|
||||
impl TerminalText { // Add a character at the cursor position pub fn
|
||||
insert_char(&mut self, c: char) { let current_text =
|
||||
self.buffer.text().to_string(); let pos =
|
||||
self.cursor_position.min(current_text.len());
|
||||
|
||||
// Create new text with the character inserted
|
||||
let mut new_text = current_text.clone();
|
||||
new_text.insert(pos, c);
|
||||
|
||||
// Calculate the affected region
|
||||
let affected_line = self.calculate_line_for_position(pos);
|
||||
let line_bounds = self.get_line_bounds(affected_line);
|
||||
self.dirty_regions.push(line_bounds);
|
||||
|
||||
// Update the buffer with the new text
|
||||
self.buffer.set_text(
|
||||
&mut self.font_system,
|
||||
&new_text,
|
||||
&Attrs::new().family(Family::Monospace),
|
||||
Shaping::Advanced,
|
||||
);
|
||||
|
||||
// Update cursor position
|
||||
self.cursor_position = pos + 1;
|
||||
self.last_content = new_text;
|
||||
}
|
||||
|
||||
// Delete a character before the cursor
|
||||
pub fn delete_char(&mut self) {
|
||||
let current_text = self.buffer.text().to_string();
|
||||
if self.cursor_position > 0 && !current_text.is_empty() {
|
||||
let pos = self.cursor_position.min(current_text.len());
|
||||
|
||||
// Create new text with the character deleted
|
||||
let mut new_text = current_text.clone();
|
||||
new_text.remove(pos - 1);
|
||||
|
||||
// Calculate the affected region
|
||||
let affected_line = self.calculate_line_for_position(pos - 1);
|
||||
let line_bounds = self.get_line_bounds(affected_line);
|
||||
self.dirty_regions.push(line_bounds);
|
||||
|
||||
// Update the buffer with the new text
|
||||
self.buffer.set_text(
|
||||
&mut self.font_system,
|
||||
&new_text,
|
||||
&Attrs::new().family(Family::Monospace),
|
||||
Shaping::Advanced,
|
||||
);
|
||||
|
||||
// Update cursor position
|
||||
self.cursor_position = pos - 1;
|
||||
self.last_content = new_text;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods to calculate affected regions
|
||||
fn calculate_line_for_position(&self, pos: usize) -> usize {
|
||||
// Count newlines up to the position
|
||||
self.buffer.text()[..pos].matches('\n').count()
|
||||
}
|
||||
|
||||
fn get_line_bounds(&self, line: usize) -> TextBounds {
|
||||
// Calculate the bounds for a specific line
|
||||
// This is a simplified version - you'll need to implement this based on your text layout
|
||||
TextBounds {
|
||||
left: 0,
|
||||
top: (line * self.get_line_height()) as i32,
|
||||
right: self.viewport.width() as i32,
|
||||
bottom: ((line + 1) * self.get_line_height()) as i32,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_line_height(&self) -> usize {
|
||||
// Get the line height based on your font metrics
|
||||
self.buffer.metrics().line_height.ceil() as usize
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Optimize the Rendering Process
|
||||
|
||||
Modify your prepare and render methods to only update the dirty regions:
|
||||
|
||||
```rust
|
||||
pub fn prepare(&mut self, device: &Device, queue: &Queue, config:
|
||||
&SurfaceConfiguration) { // Update viewport with new dimensions
|
||||
self.viewport.update( queue, Resolution { width: config.width, height:
|
||||
config.height, }, );
|
||||
|
||||
// Make sure text is properly wrapped for the current width
|
||||
self.resize(config.width, config.height);
|
||||
|
||||
// Only reshape if there are dirty regions
|
||||
if !self.dirty_regions.is_empty() {
|
||||
// Prepare only the dirty regions
|
||||
for region in &self.dirty_regions {
|
||||
self.renderer
|
||||
.prepare(
|
||||
device,
|
||||
queue,
|
||||
&mut self.font_system,
|
||||
&mut self.atlas,
|
||||
&self.viewport,
|
||||
[TextArea {
|
||||
buffer: &self.buffer,
|
||||
left: 0.0,
|
||||
top: 0.0,
|
||||
scale: 1.0,
|
||||
bounds: *region,
|
||||
default_color: Color::rgb(255, 255, 255),
|
||||
custom_glyphs: &[],
|
||||
}],
|
||||
&mut self.cache,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Clear dirty regions after preparing
|
||||
self.dirty_regions.clear();
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Implement a Double Buffer Approach
|
||||
|
||||
To further optimize, implement a double buffer approach:
|
||||
|
||||
```rust
|
||||
pub struct TerminalText { // ... existing fields front_buffer: Buffer,
|
||||
back_buffer: Buffer, buffer_swap_needed: bool, }
|
||||
|
||||
impl TerminalText { // ... existing methods
|
||||
|
||||
pub fn swap_buffers(&mut self) {
|
||||
if self.buffer_swap_needed {
|
||||
std::mem::swap(&mut self.front_buffer, &mut self.back_buffer);
|
||||
self.buffer_swap_needed = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prepare_back_buffer(&mut self, device: &Device, queue: &Queue) {
|
||||
// Prepare the back buffer while the front buffer is being rendered
|
||||
if !self.dirty_regions.is_empty() {
|
||||
// Apply changes to the back buffer
|
||||
// ...
|
||||
|
||||
self.buffer_swap_needed = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Implement Cursor Rendering
|
||||
|
||||
Add cursor rendering to provide visual feedback:
|
||||
|
||||
rust pub fn render_cursor(&mut self, render_pass: &mut RenderPass) { //
|
||||
Calculate cursor position in screen coordinates let (x, y) =
|
||||
self.get_cursor_screen_position();
|
||||
|
||||
// Render cursor using a simple rectangle
|
||||
// You'll need to implement this based on your rendering system
|
||||
|
||||
}
|
||||
|
||||
fn get_cursor_screen_position(&self) -> (f32, f32) { // Calculate screen
|
||||
position based on cursor_position // This will depend on your text layout system
|
||||
// ... }
|
||||
|
||||
## 6. Handle Special Keys
|
||||
|
||||
In your keyboard event handler, handle special keys like backspace, enter, and
|
||||
arrow keys:
|
||||
|
||||
rust fn handle*key_event(&mut self, key_event: KeyEvent) { match key_event.key {
|
||||
Key::Character(c) => { if key_event.state == ElementState::Pressed {
|
||||
self.text.insert_char(c.chars().next().unwrap_or(' '));
|
||||
self.window.request_redraw(); } }, Key::Named(NamedKey::Backspace) => { if
|
||||
key_event.state == ElementState::Pressed { self.text.delete_char();
|
||||
self.window.request_redraw(); } }, Key::Named(NamedKey::Enter) => { if
|
||||
key_event.state == ElementState::Pressed { self.text.insert_char('\n');
|
||||
self.window.request_redraw(); } }, // Handle arrow keys for cursor movement
|
||||
Key::Named(NamedKey::ArrowLeft) => { if key_event.state == ElementState::Pressed
|
||||
{ self.text.move_cursor_left(); self.window.request_redraw(); } },
|
||||
Key::Named(NamedKey::ArrowRight) => { if key_event.state ==
|
||||
ElementState::Pressed { self.text.move_cursor_right();
|
||||
self.window.request_redraw(); } }, * => {} } }
|
||||
|
||||
## 7. Implement Frame Skipping
|
||||
|
||||
Skip rendering frames when nothing has changed:
|
||||
|
||||
rust pub fn render(&mut self) -> Result<(), SurfaceError> { // Skip rendering if
|
||||
nothing has changed if self.dirty_regions.is_empty() && !self.buffer_swap_needed
|
||||
{ return Ok(()); }
|
||||
|
||||
// Swap buffers if needed
|
||||
self.text.swap_buffers();
|
||||
|
||||
// Render as normal
|
||||
// ...
|
||||
|
||||
Ok(())
|
||||
|
||||
}
|
||||
|
||||
These changes will significantly improve the efficiency of your text rendering
|
||||
by:
|
||||
|
||||
1. Only updating parts of the text that have changed
|
||||
2. Using double buffering to prepare updates while rendering
|
||||
3. Skipping frames when nothing has changed
|
||||
4. Optimizing the text atlas updates
|
||||
|
||||
This approach aligns with the optimization suggestions in your README.md and
|
||||
should provide a much more efficient text rendering system for your terminal
|
||||
emulator.
|
||||
1. [ ] Complete keyboard input handling (arrow keys, backspace, etc.)
|
||||
2. [ ] Implement a PTY interface to spawn and communicate with processes
|
||||
3. [ ] Add support for terminal protocols (ANSI escape sequences)
|
||||
4. [ ] Implement scrollback buffer and viewport scrolling
|
||||
5. [ ] Add color and text styling support
|
||||
6. [ ] Implement clipboard integration
|
||||
7. [ ] Add configuration options
|
||||
|
||||
84
project.md
Normal file
84
project.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Module Breakdown and Responsibilities
|
||||
|
||||
## 1. src/pty.rs (The Shell Interface)
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Use portable-pty to create a new PTY.
|
||||
- Spawn a default shell process (e.g., bash) connected to the PTY.
|
||||
- Provide a way to write user input (from winit) to the PTY.
|
||||
- Provide a way to read shell output from the PTY.
|
||||
|
||||
Key Implementation Detail: Reading from the PTY is a blocking operation. You
|
||||
should do this on a separate thread. This thread will read data and send it back
|
||||
to the main UI thread using a channel (like crossbeam-channel). This prevents
|
||||
the UI from freezing while waiting for the shell to output something.
|
||||
|
||||
## 2. src/terminal.rs (The Brains / State Model)
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Hold the state of the terminal grid: a 2D array of cells, where each cell
|
||||
contains a character, foreground color, background color, and style flags
|
||||
(bold, italic, etc.).
|
||||
|
||||
- Contain a vte::Parser.
|
||||
|
||||
- Implement the vte::Perform trait. This is the core of this module. The vte
|
||||
parser will call methods on your implementation as it processes the byte
|
||||
stream from the PTY. For example:
|
||||
- print(char): You update the character at the current cursor position in your
|
||||
grid and advance the cursor.
|
||||
- execute(byte): You handle control characters like newline (\n) or backspace
|
||||
(\b).
|
||||
- csi_dispatch(...): You handle complex ANSI escape sequences for changing
|
||||
colors, moving the cursor, clearing the screen, etc.
|
||||
|
||||
- Keep track of the cursor's position and the current color/style settings.
|
||||
|
||||
## 3. src/renderer.rs (The Painter)
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Initialize wgpu: get an adapter and device, configure a surface on the winit
|
||||
window.
|
||||
- Initialize glyphon: create a FontSystem, Cache, Atlas, and TextRenderer.
|
||||
- Contain the main render() function. This function will be called on every
|
||||
frame.
|
||||
- Inside render():
|
||||
- Get the current terminal state.
|
||||
- Iterate through the terminal grid.
|
||||
- For each line of text, create a glyphon::Buffer. Set its text content,
|
||||
colors, and styles based on the data in your grid.
|
||||
- Use the glyphon::TextRenderer to prepare and draw all the buffers to the
|
||||
wgpu surface.
|
||||
- Draw the cursor as a solid block or underline.
|
||||
|
||||
## 4. src/main.rs (The Conductor)
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- The main function.
|
||||
- Initialize logging (env_logger).
|
||||
- Create the winit event loop and window.
|
||||
- Initialize your Pty, Terminal, and Renderer modules.
|
||||
- Start the PTY reader thread.
|
||||
- Run the main event loop.
|
||||
|
||||
### The Main Event Loop (winit::event_loop::run)
|
||||
|
||||
This is where everything comes together. You'll have a match statement to handle
|
||||
different events:
|
||||
|
||||
- Event::WindowEvent::Resized: Tell the renderer about the new size. You'll also
|
||||
need to inform the PTY of the new dimensions so that applications running in
|
||||
the shell (like vim) can reflow correctly.
|
||||
- Event::WindowEvent::KeyboardInput: Translate the keyboard event into
|
||||
characters or escape sequences and write them to the pty module.
|
||||
- Event::UserEvent: This is how your PTY thread will communicate with the main
|
||||
thread. When the PTY thread reads new data, it sends it through the channel,
|
||||
and you receive it here. You then feed this data into the terminal.vte_parser.
|
||||
- Event::WindowEvent::RedrawRequested: This is the signal to render a new frame.
|
||||
You call your renderer.render() function here. After processing PTY data, you
|
||||
should always request a redraw.
|
||||
- Event::WindowEvent::CloseRequested: Terminate the application.
|
||||
123
src/app/mod.rs
123
src/app/mod.rs
@@ -1,29 +1,61 @@
|
||||
use crossbeam_channel::Receiver;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use std::sync::Arc;
|
||||
use tokio::runtime::Runtime;
|
||||
use vte::Parser;
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::event::WindowEvent;
|
||||
use winit::keyboard::{Key, NamedKey};
|
||||
use winit::window::Window;
|
||||
use winit::{
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, EventLoopProxy},
|
||||
window::{Window, WindowId},
|
||||
};
|
||||
|
||||
mod rendering;
|
||||
mod pty;
|
||||
mod renderer;
|
||||
mod terminal;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UserEvent {
|
||||
PtyOutput(String),
|
||||
}
|
||||
|
||||
struct TermState {
|
||||
pty: pty::Pty,
|
||||
terminal: terminal::Terminal,
|
||||
renderer: renderer::Renderer,
|
||||
vte_parser: vte::Parser,
|
||||
}
|
||||
|
||||
pub struct Application {
|
||||
window_state: Option<rendering::State>,
|
||||
runtime: Runtime,
|
||||
event_loop_proxy: EventLoopProxy<UserEvent>,
|
||||
term_state: Option<TermState>,
|
||||
}
|
||||
|
||||
impl Application {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(event_loop_proxy: EventLoopProxy<UserEvent>) -> Self {
|
||||
trace!("Creating new Application");
|
||||
Self {
|
||||
window_state: None,
|
||||
runtime: Runtime::new().expect("Failed to create tokio runtime"),
|
||||
event_loop_proxy,
|
||||
term_state: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn start_pty_output_recv_thread(&self, recv: Receiver<String>) {
|
||||
let proxy = self.event_loop_proxy.clone();
|
||||
std::thread::spawn(move || {
|
||||
while let Ok(output) = recv.recv() {
|
||||
trace!("Read from the PTY output: {}", output);
|
||||
proxy.send_event(UserEvent::PtyOutput(output)).ok();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl winit::application::ApplicationHandler for Application {
|
||||
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
|
||||
if self.window_state.is_some() {
|
||||
impl winit::application::ApplicationHandler<UserEvent> for Application {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
if self.term_state.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,55 +64,48 @@ impl winit::application::ApplicationHandler for Application {
|
||||
let window_attributes = Window::default_attributes()
|
||||
.with_inner_size(LogicalSize::new(width as f64, height as f64))
|
||||
.with_title("Terminal emulator");
|
||||
debug!("Creating a 800x600 window");
|
||||
let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
|
||||
|
||||
self.window_state = Some(self.runtime.block_on(rendering::State::new(window)));
|
||||
trace!("Creating crossbeam channel for PTY output rendering");
|
||||
let (send, recv) = crossbeam_channel::unbounded();
|
||||
|
||||
self.term_state = Some(TermState {
|
||||
pty: pty::Pty::new(send),
|
||||
terminal: terminal::Terminal::new(),
|
||||
renderer: self.runtime.block_on(renderer::Renderer::new(window)),
|
||||
vte_parser: vte::Parser::new(),
|
||||
});
|
||||
|
||||
self.start_pty_output_recv_thread(recv);
|
||||
}
|
||||
|
||||
fn user_event(&mut self, _event_loop: &ActiveEventLoop, event: UserEvent) {
|
||||
if let Some(state) = &mut self.term_state {
|
||||
match event {
|
||||
UserEvent::PtyOutput(output) => {
|
||||
for byte in output.bytes() {
|
||||
state.vte_parser.advance(&mut state.terminal, &[byte]);
|
||||
}
|
||||
state.renderer.request_redraw();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &winit::event_loop::ActiveEventLoop,
|
||||
_window_id: winit::window::WindowId,
|
||||
_event_loop: &ActiveEventLoop,
|
||||
_window_id: WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
let Some(state) = &mut self.window_state else {
|
||||
return;
|
||||
};
|
||||
|
||||
match event {
|
||||
WindowEvent::Resized(size) => {
|
||||
state.resize(size);
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
state.render().unwrap();
|
||||
}
|
||||
WindowEvent::KeyboardInput {
|
||||
device_id: _,
|
||||
event,
|
||||
is_synthetic: _,
|
||||
} => {
|
||||
if event.state.is_pressed() {
|
||||
match event.logical_key {
|
||||
Key::Named(k) => match k {
|
||||
NamedKey::Enter => {
|
||||
state.handle_enter();
|
||||
}
|
||||
_ => todo!(),
|
||||
},
|
||||
Key::Character(c) => {
|
||||
state.add_text(&event.text.unwrap());
|
||||
}
|
||||
Key::Dead(_) => {
|
||||
todo!()
|
||||
}
|
||||
Key::Unidentified(_) => {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
if let Some(state) = &mut self.term_state {
|
||||
match event {
|
||||
WindowEvent::RedrawRequested => {
|
||||
state.renderer.render(&state.terminal);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
90
src/app/pty/mod.rs
Normal file
90
src/app/pty/mod.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use crossbeam_channel::Sender;
|
||||
use portable_pty::{Child, CommandBuilder, PtySize, native_pty_system};
|
||||
use std::io::{Read, Write};
|
||||
use std::thread;
|
||||
|
||||
pub struct Pty {
|
||||
writer: Box<dyn Write + Send + 'static>,
|
||||
child: Box<dyn Child>,
|
||||
}
|
||||
|
||||
pub enum PtyError {
|
||||
Io(std::io::Error),
|
||||
}
|
||||
//
|
||||
// You need this `From` implementation!
|
||||
impl From<std::io::Error> for PtyError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
PtyError::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl Pty {
|
||||
pub fn new(output_sender: crossbeam_channel::Sender<String>) -> Self {
|
||||
let pty_system = native_pty_system();
|
||||
|
||||
// Create a new pty
|
||||
let pair = pty_system
|
||||
.openpty(PtySize {
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
// Not all systems support pixel_width, pixel_height,
|
||||
// but it is good practice to set it to something
|
||||
// that matches the size of the selected font. That
|
||||
// is more complex than can be shown here in this
|
||||
// brief example though!
|
||||
pixel_width: 0,
|
||||
pixel_height: 0,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Spawn a shell into the pty
|
||||
let cmd = CommandBuilder::new_default_prog();
|
||||
|
||||
// The `slave` end of the PTY is given to the shell process.
|
||||
// The shell thinks it's talking to a real terminal.
|
||||
let mut child = pair.slave.spawn_command(cmd).unwrap();
|
||||
// We can now drop the slave, as it's been consumed by the child process.
|
||||
drop(pair.slave);
|
||||
|
||||
// Our application will use the `master` end to communicate with the shell.
|
||||
// We are cloning the master so we can use it in a separate thread for reading.
|
||||
let writer = pair.master.take_writer().unwrap();
|
||||
let reader = pair.master.try_clone_reader().unwrap();
|
||||
|
||||
start_reader_thread(output_sender.clone(), reader);
|
||||
|
||||
Self { writer, child }
|
||||
}
|
||||
|
||||
pub fn send_command(&mut self, cmd: String) -> Result<(), PtyError> {
|
||||
self.writer.write_all(cmd.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn start_reader_thread(sender: Sender<String>, mut reader: Box<dyn Read + Send + 'static>) {
|
||||
thread::spawn(move || {
|
||||
let mut buffer = [0u8; 1024];
|
||||
loop {
|
||||
match reader.read(&mut buffer) {
|
||||
Ok(0) => {
|
||||
// EOF, the child process has exited.
|
||||
break;
|
||||
}
|
||||
Ok(n) => {
|
||||
// Append the read data to our output string.
|
||||
// We use from_utf8_lossy to handle potential invalid UTF-8 sequences.
|
||||
sender
|
||||
.send(String::from_utf8_lossy(&buffer[..n]).to_string())
|
||||
.unwrap();
|
||||
}
|
||||
Err(e) => {
|
||||
// An error occurred.
|
||||
eprintln!("Error reading from PTY: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use log::{debug, trace};
|
||||
use std::sync::Arc;
|
||||
use wgpu::{
|
||||
CommandEncoderDescriptor, CompositeAlphaMode, Device, DeviceDescriptor, Instance,
|
||||
@@ -8,9 +9,7 @@ use wgpu::{
|
||||
use winit::dpi::PhysicalSize;
|
||||
use winit::window::Window;
|
||||
|
||||
mod terminal_text;
|
||||
|
||||
pub struct State {
|
||||
pub struct Renderer {
|
||||
surface: Surface<'static>,
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
@@ -18,63 +17,11 @@ pub struct State {
|
||||
|
||||
size: PhysicalSize<u32>,
|
||||
|
||||
text: terminal_text::TerminalText,
|
||||
|
||||
/// text: terminal_text::TerminalText,
|
||||
window: Arc<Window>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
|
||||
if new_size.width > 0 && new_size.height > 0 {
|
||||
self.size = new_size;
|
||||
self.config.width = new_size.width;
|
||||
self.config.height = new_size.height;
|
||||
self.surface.configure(&self.device, &self.config);
|
||||
self.window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self) -> Result<(), SurfaceError> {
|
||||
// Prepare with current configuration (which includes the current window size)
|
||||
self.text.prepare(&self.device, &self.queue, &self.config);
|
||||
|
||||
let output = self.surface.get_current_texture()?;
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&TextureViewDescriptor::default());
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("Render Encoder"),
|
||||
});
|
||||
|
||||
{
|
||||
let mut _render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
label: Some("Render Pass"),
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
|
||||
self.text.render(&mut _render_pass);
|
||||
}
|
||||
|
||||
self.queue.submit(Some(encoder.finish()));
|
||||
output.present();
|
||||
|
||||
self.text.trim();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
pub async fn new(window: Arc<Window>) -> Self {
|
||||
let size = window.inner_size();
|
||||
|
||||
@@ -170,32 +117,65 @@ impl State {
|
||||
};
|
||||
surface.configure(&device, &config);
|
||||
|
||||
// Set up text renderer
|
||||
let text = terminal_text::TerminalText::new(&device, &queue, surface_format);
|
||||
|
||||
Self {
|
||||
surface,
|
||||
device,
|
||||
queue,
|
||||
config,
|
||||
|
||||
size,
|
||||
|
||||
text,
|
||||
|
||||
window,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_text(&mut self, text: &str) {
|
||||
for c in text.chars() {
|
||||
self.text.insert_char(c);
|
||||
}
|
||||
self.window.request_redraw();
|
||||
pub fn request_redraw(&self) {
|
||||
self.window.request_redraw()
|
||||
}
|
||||
|
||||
pub fn handle_enter(&mut self) {
|
||||
self.text.handle_enter();
|
||||
self.window.request_redraw();
|
||||
// The renderer takes the terminal state as an argument to its render method.
|
||||
// It does not store a copy of the grid itself.
|
||||
pub fn render(&mut self, terminal: &super::terminal::Terminal) -> Result<(), SurfaceError> {
|
||||
let grid = terminal.get_grid();
|
||||
|
||||
// 1. Iterate through the `grid`.
|
||||
// 2. For each row, create a glyphon::Buffer.
|
||||
// 3. Set the text, colors, and styles for that buffer from the row's `Cell` data.
|
||||
// 4. Draw all the buffers to the screen.
|
||||
// 5. Draw the cursor at terminal.get_cursor_pos().
|
||||
trace!("Rendering requested");
|
||||
// Prepare with current configuration (which includes the current window size)
|
||||
|
||||
let output = self.surface.get_current_texture()?;
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&TextureViewDescriptor::default());
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("Render Encoder"),
|
||||
});
|
||||
|
||||
{
|
||||
let mut _render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
label: Some("Render Pass"),
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
|
||||
trace!("Rendering...");
|
||||
}
|
||||
|
||||
self.queue.submit(Some(encoder.finish()));
|
||||
output.present();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,275 +0,0 @@
|
||||
use glyphon::{
|
||||
Attrs, Buffer, BufferLine, Cache, Color, Cursor, Family, FontSystem, Metrics, Resolution,
|
||||
Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Wrap,
|
||||
};
|
||||
|
||||
use wgpu::{Device, MultisampleState, Queue, RenderPass, SurfaceConfiguration, TextureFormat};
|
||||
|
||||
pub struct TerminalText {
|
||||
font_system: FontSystem,
|
||||
cache: SwashCache,
|
||||
viewport: Viewport,
|
||||
atlas: TextAtlas,
|
||||
buffer: Buffer,
|
||||
renderer: TextRenderer,
|
||||
dirty_regions: Vec<TextBounds>,
|
||||
cursor: Cursor,
|
||||
}
|
||||
|
||||
use log::info;
|
||||
|
||||
impl TerminalText {
|
||||
pub fn new(device: &Device, queue: &Queue, surface_format: TextureFormat) -> TerminalText {
|
||||
let mut font_system = FontSystem::new();
|
||||
let swash_cache = SwashCache::new();
|
||||
|
||||
let cache = Cache::new(device);
|
||||
let viewport = Viewport::new(device, &cache);
|
||||
let mut atlas = TextAtlas::new(device, queue, &cache, surface_format);
|
||||
|
||||
let renderer = TextRenderer::new(&mut atlas, device, MultisampleState::default(), None);
|
||||
|
||||
let mut buffer = Buffer::new(&mut font_system, Metrics::relative(13.0, 1.25));
|
||||
buffer.set_wrap(&mut font_system, Wrap::Glyph);
|
||||
|
||||
// Add a default font
|
||||
font_system.db_mut().load_system_fonts();
|
||||
|
||||
Self {
|
||||
font_system,
|
||||
cache: swash_cache,
|
||||
viewport,
|
||||
atlas,
|
||||
buffer,
|
||||
renderer,
|
||||
dirty_regions: vec![],
|
||||
cursor: Cursor {
|
||||
line: 0,
|
||||
index: 0,
|
||||
affinity: glyphon::Affinity::After,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, text: &str) {
|
||||
// Clear the buffer and set new text
|
||||
self.buffer.set_text(
|
||||
&mut self.font_system,
|
||||
text,
|
||||
&Attrs::new().family(Family::Monospace),
|
||||
Shaping::Advanced,
|
||||
);
|
||||
self.buffer.set_wrap(&mut self.font_system, Wrap::Glyph);
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
// Update the buffer's wrapping based on the new width
|
||||
self.buffer.set_wrap(&mut self.font_system, Wrap::Glyph);
|
||||
self.buffer.set_size(
|
||||
&mut self.font_system,
|
||||
Some(width as f32),
|
||||
Some(height as f32),
|
||||
);
|
||||
// Reshape the text with the new dimensions
|
||||
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
||||
}
|
||||
|
||||
pub fn prepare(&mut self, device: &Device, queue: &Queue, config: &SurfaceConfiguration) {
|
||||
// Update viewport with new dimensions
|
||||
self.viewport.update(
|
||||
queue,
|
||||
Resolution {
|
||||
width: config.width,
|
||||
height: config.height,
|
||||
},
|
||||
);
|
||||
|
||||
// Make sure text is properly wrapped for the current width
|
||||
self.resize(config.width, config.height);
|
||||
|
||||
if !self.dirty_regions.is_empty() {
|
||||
for region in &self.dirty_regions {
|
||||
self.renderer
|
||||
.prepare(
|
||||
device,
|
||||
queue,
|
||||
&mut self.font_system,
|
||||
&mut self.atlas,
|
||||
&self.viewport,
|
||||
[TextArea {
|
||||
buffer: &self.buffer,
|
||||
left: 0.0,
|
||||
top: 0.0,
|
||||
scale: 1.0,
|
||||
bounds: *region,
|
||||
default_color: Color::rgb(255, 255, 255),
|
||||
custom_glyphs: &[],
|
||||
}],
|
||||
&mut self.cache,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
} else {
|
||||
// If no dirty regions, prepare the entire buffer (first render or after resize)
|
||||
self.renderer
|
||||
.prepare(
|
||||
device,
|
||||
queue,
|
||||
&mut self.font_system,
|
||||
&mut self.atlas,
|
||||
&self.viewport,
|
||||
[TextArea {
|
||||
buffer: &self.buffer,
|
||||
left: 0.0,
|
||||
top: 0.0,
|
||||
scale: 1.0,
|
||||
bounds: TextBounds {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: config.width as i32,
|
||||
bottom: config.height as i32,
|
||||
},
|
||||
default_color: Color::rgb(255, 255, 255),
|
||||
custom_glyphs: &[],
|
||||
}],
|
||||
&mut self.cache,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
self.dirty_regions.clear();
|
||||
}
|
||||
|
||||
pub fn render(&self, render_pass: &mut RenderPass) {
|
||||
self.renderer
|
||||
.render(&self.atlas, &self.viewport, render_pass)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn trim(&mut self) {
|
||||
self.atlas.trim()
|
||||
}
|
||||
|
||||
pub fn insert_char(&mut self, c: char) {
|
||||
let line = self.cursor.line;
|
||||
|
||||
if line >= self.buffer.lines.len() {
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
info!("line nr: {line}");
|
||||
|
||||
let current_line = self.buffer.lines[line].text().to_string();
|
||||
let pos = self.cursor.index.min(current_line.len());
|
||||
|
||||
info!("cursor position: {pos}");
|
||||
|
||||
info!("Line text: {current_line}");
|
||||
|
||||
let mut new_text = current_line.clone();
|
||||
new_text.insert(pos, c);
|
||||
|
||||
let line_bounds = self.get_line_bounds(line);
|
||||
self.dirty_regions.push(line_bounds);
|
||||
|
||||
let ending = self.buffer.lines[line].ending();
|
||||
let attrs = self.buffer.lines[line].attrs_list().clone();
|
||||
|
||||
// Update the buffer with the new text
|
||||
self.buffer.lines[line].set_text(&new_text, ending, attrs);
|
||||
|
||||
// Update cursor position
|
||||
self.cursor.index = pos + 1;
|
||||
|
||||
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
||||
}
|
||||
|
||||
fn get_line_bounds(&self, line: usize) -> TextBounds {
|
||||
// Calculate the bounds for a specific line
|
||||
// This is a simplified version - you'll need to implement this based on your text layout
|
||||
TextBounds {
|
||||
left: 0,
|
||||
top: (line * self.get_line_height()) as i32,
|
||||
right: self.viewport.resolution().width as i32,
|
||||
bottom: ((line + 1) * self.get_line_height()) as i32,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_line_height(&self) -> usize {
|
||||
// Get the line height based on your font metrics
|
||||
self.buffer.metrics().line_height.ceil() as usize
|
||||
}
|
||||
|
||||
pub fn handle_enter(&mut self) {
|
||||
let line = self.cursor.line;
|
||||
|
||||
// Ensure line is within bounds
|
||||
if line >= self.buffer.lines.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current line text
|
||||
let current_line = self.buffer.lines[line].text().to_string();
|
||||
let pos = self.cursor.index.min(current_line.len());
|
||||
|
||||
// Split the line at cursor position
|
||||
let text_before = ¤t_line[..pos];
|
||||
let text_after = ¤t_line[pos..];
|
||||
|
||||
// Update the current line with text before cursor
|
||||
let ending = self.buffer.lines[line].ending();
|
||||
let attrs = self.buffer.lines[line].attrs_list().clone();
|
||||
self.buffer.lines[line].set_text(text_before, ending, attrs.clone());
|
||||
|
||||
// Create a new line with text after cursor
|
||||
let new_line = BufferLine::new(text_after, ending, attrs, Shaping::Advanced);
|
||||
|
||||
// Insert the new line after the current one
|
||||
self.buffer.lines.insert(line + 1, new_line);
|
||||
|
||||
self.mark_all_dirty();
|
||||
|
||||
// Update cursor position to beginning of next line
|
||||
self.cursor.line = line + 1;
|
||||
self.cursor.index = 0;
|
||||
|
||||
// Reshape the buffer
|
||||
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
||||
}
|
||||
|
||||
pub fn mark_all_dirty(&mut self) {
|
||||
// Create a single region covering the entire viewport
|
||||
let full_bounds = TextBounds {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: self.viewport.resolution().width as i32,
|
||||
bottom: self.viewport.resolution().height as i32,
|
||||
};
|
||||
self.dirty_regions = vec![full_bounds];
|
||||
}
|
||||
|
||||
pub fn move_cursor_left(&mut self) {
|
||||
if self.cursor.index > 0 {
|
||||
self.cursor.index -= 1;
|
||||
} else if self.cursor.line > 0 {
|
||||
// Move to end of previous line
|
||||
self.cursor.line -= 1;
|
||||
let prev_line_len = self.buffer.lines[self.cursor.line].text().len();
|
||||
self.cursor.index = prev_line_len;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_cursor_right(&mut self) {
|
||||
if self.cursor.line < self.buffer.lines.len() {
|
||||
let current_line_len = self.buffer.lines[self.cursor.line].text().len();
|
||||
|
||||
if self.cursor.index < current_line_len {
|
||||
self.cursor.index += 1;
|
||||
} else if self.cursor.line < self.buffer.lines.len() - 1 {
|
||||
// Move to beginning of next line
|
||||
self.cursor.line += 1;
|
||||
self.cursor.index = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/app/terminal/mod.rs
Normal file
78
src/app/terminal/mod.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use vte::{Parser, Perform};
|
||||
|
||||
// A struct to represent a single cell on the screen
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Cell {
|
||||
pub char: char,
|
||||
// pub fg_color: Color,
|
||||
// pub bg_color: Color,
|
||||
// pub flags: StyleFlags,
|
||||
}
|
||||
|
||||
pub struct CellGrid(Vec<Vec<Cell>>);
|
||||
|
||||
impl CellGrid {
|
||||
pub fn new(width: usize, height: usize) -> Self {
|
||||
CellGrid(vec![vec![Cell::default(); width]; height])
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Terminal {
|
||||
grid: CellGrid,
|
||||
cursor_x: usize,
|
||||
cursor_y: usize,
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
pub fn new() -> Self {
|
||||
let width = 80;
|
||||
let height = 24;
|
||||
Terminal {
|
||||
grid: CellGrid::new(width, height),
|
||||
cursor_x: 0,
|
||||
cursor_y: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_grid(&self) -> &CellGrid {
|
||||
&self.grid
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform for Terminal {
|
||||
// this method will update the CellGrid and the cursor position
|
||||
fn print(&mut self, c: char) {
|
||||
// todo!()
|
||||
()
|
||||
}
|
||||
|
||||
/// This method is called when an "execute" control character is seen.
|
||||
/// These are single-byte control codes like newline, backspace, etc.
|
||||
fn execute(&mut self, byte: u8) {
|
||||
match byte {
|
||||
b'\n' => {
|
||||
// Newline
|
||||
println!("Execute: Newline");
|
||||
self.cursor_y += 1;
|
||||
self.cursor_x = 0; // Carriage return is implicit with newline
|
||||
}
|
||||
_ => {
|
||||
println!("Execute: Unhandled control character: 0x{:02x}", byte);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This method is called for "Control Sequence Introducer" (CSI) sequences.
|
||||
/// These are the complex `ESC [ ...` sequences for moving the cursor,
|
||||
/// changing colors, clearing the screen, etc.
|
||||
fn csi_dispatch(
|
||||
&mut self,
|
||||
params: &vte::Params,
|
||||
_intermediates: &[u8],
|
||||
_ignore: bool,
|
||||
action: char,
|
||||
) {
|
||||
//todo!()
|
||||
()
|
||||
}
|
||||
}
|
||||
16
src/main.rs
16
src/main.rs
@@ -1,4 +1,4 @@
|
||||
use log::info;
|
||||
use log::{debug, info};
|
||||
use winit::event_loop::EventLoop;
|
||||
|
||||
mod app;
|
||||
@@ -10,9 +10,17 @@ fn main() {
|
||||
// Log platform information
|
||||
info!("Starting simplicitty terminal emulator");
|
||||
|
||||
// Create event loop with explicit backend preference
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
debug!("Creating winit event loop");
|
||||
let event_loop = EventLoop::<app::UserEvent>::with_user_event()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Create event loop proxy to send pty output events to the window
|
||||
let event_loop_proxy = event_loop.create_proxy();
|
||||
|
||||
debug!("Starting winit event loop");
|
||||
// Run the application
|
||||
event_loop.run_app(&mut app::Application::new()).unwrap();
|
||||
event_loop
|
||||
.run_app(&mut app::Application::new(event_loop_proxy))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user