Compare commits
2 Commits
legacy/fir
...
feat/0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 17794aba0b | |||
| aed22a3dbc |
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.
|
||||||
41
AmazonQ.md
41
AmazonQ.md
@@ -1,41 +0,0 @@
|
|||||||
# Text Scrolling Implementation in Simplicitty
|
|
||||||
|
|
||||||
This document outlines the implementation of text scrolling in the Simplicitty terminal emulator project.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The implementation adds the ability to scroll through text content when it doesn't fit completely in the window. The scrolling is implemented on a line-by-line basis, allowing users to navigate through the content using keyboard shortcuts.
|
|
||||||
|
|
||||||
## Key Components
|
|
||||||
|
|
||||||
1. **Scroll State Management**:
|
|
||||||
- Added `scroll_offset` to track the current scroll position (in visual lines)
|
|
||||||
- Added `max_scroll_offset` to limit scrolling to available content
|
|
||||||
|
|
||||||
2. **Viewport Calculation**:
|
|
||||||
- Modified `get_visible_line_range()` to consider the scroll offset when determining visible lines
|
|
||||||
- Added `update_max_scroll_offset()` to recalculate the maximum scroll position based on content and viewport size
|
|
||||||
|
|
||||||
3. **Scrolling Methods**:
|
|
||||||
- `scroll_up(lines)`: Move viewport up by specified number of lines
|
|
||||||
- `scroll_down(lines)`: Move viewport down by specified number of lines
|
|
||||||
- `page_up()`: Scroll up by one page (viewport height)
|
|
||||||
- `page_down()`: Scroll down by one page (viewport height)
|
|
||||||
|
|
||||||
4. **Keyboard Controls**:
|
|
||||||
- Ctrl+Up/Down: Scroll one line up/down
|
|
||||||
- Page Up/Down: Scroll one page up/down
|
|
||||||
- Ctrl+Home/End: Scroll to top/bottom of content
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
The scrolling implementation works by adjusting the `scroll_offset` value, which determines which visual lines are rendered in the viewport. The `get_visible_line_range()` function uses this offset to calculate which logical lines should be rendered.
|
|
||||||
|
|
||||||
When the window is resized or content changes, the `max_scroll_offset` is recalculated to ensure scrolling remains within valid bounds.
|
|
||||||
|
|
||||||
## Future Improvements
|
|
||||||
|
|
||||||
1. Add mouse wheel support for scrolling
|
|
||||||
2. Implement smooth scrolling animations
|
|
||||||
3. Add a visual scrollbar indicator
|
|
||||||
4. Preserve horizontal scroll position when navigating vertically
|
|
||||||
184
Cargo.lock
generated
184
Cargo.lock
generated
@@ -91,6 +91,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.98"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayref"
|
name = "arrayref"
|
||||||
version = "0.3.9"
|
version = "0.3.9"
|
||||||
@@ -271,6 +277,12 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg_aliases"
|
name = "cfg_aliases"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -370,6 +382,15 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"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]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.21"
|
version = "0.8.21"
|
||||||
@@ -472,6 +493,17 @@ dependencies = [
|
|||||||
"num-traits",
|
"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]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -802,6 +834,12 @@ version = "3.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lazy_static"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.172"
|
version = "0.2.172"
|
||||||
@@ -937,7 +975,7 @@ dependencies = [
|
|||||||
"arrayvec",
|
"arrayvec",
|
||||||
"bit-set",
|
"bit-set",
|
||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
"cfg_aliases",
|
"cfg_aliases 0.2.1",
|
||||||
"codespan-reporting",
|
"codespan-reporting",
|
||||||
"half",
|
"half",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
@@ -992,6 +1030,18 @@ dependencies = [
|
|||||||
"jni-sys",
|
"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]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@@ -1023,12 +1073,6 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "numtoa"
|
|
||||||
version = "0.2.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6aa2c4e539b869820a2b82e1aef6ff40aa85e65decdd5185e83fb4b1249cd00f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc"
|
name = "objc"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
@@ -1365,12 +1409,39 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "portable-atomic"
|
name = "portable-atomic"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
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]]
|
[[package]]
|
||||||
name = "presser"
|
name = "presser"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -1471,12 +1542,6 @@ dependencies = [
|
|||||||
"bitflags 2.9.0",
|
"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]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@@ -1632,6 +1697,33 @@ dependencies = [
|
|||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@@ -1651,11 +1743,14 @@ dependencies = [
|
|||||||
name = "simplicitty"
|
name = "simplicitty"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"glyphon",
|
"glyphon",
|
||||||
"log",
|
"log",
|
||||||
"termion",
|
"pollster",
|
||||||
|
"portable-pty",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"vte",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
"winit",
|
"winit",
|
||||||
]
|
]
|
||||||
@@ -1827,18 +1922,6 @@ dependencies = [
|
|||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
@@ -2059,6 +2142,16 @@ version = "0.9.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
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]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@@ -2292,7 +2385,7 @@ checksum = "ca6049eb2014a0e0d8689f9b787605dd71d5bbfdc74095ead499f3cff705c229"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
"cfg_aliases",
|
"cfg_aliases 0.2.1",
|
||||||
"document-features",
|
"document-features",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
@@ -2322,7 +2415,7 @@ dependencies = [
|
|||||||
"bit-set",
|
"bit-set",
|
||||||
"bit-vec",
|
"bit-vec",
|
||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
"cfg_aliases",
|
"cfg_aliases 0.2.1",
|
||||||
"document-features",
|
"document-features",
|
||||||
"hashbrown",
|
"hashbrown",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
@@ -2384,7 +2477,7 @@ dependencies = [
|
|||||||
"block",
|
"block",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cfg_aliases",
|
"cfg_aliases 0.2.1",
|
||||||
"core-graphics-types",
|
"core-graphics-types",
|
||||||
"glow",
|
"glow",
|
||||||
"glutin_wgl_sys",
|
"glutin_wgl_sys",
|
||||||
@@ -2431,6 +2524,22 @@ dependencies = [
|
|||||||
"web-sys",
|
"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]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
@@ -2440,6 +2549,12 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.58.0"
|
version = "0.58.0"
|
||||||
@@ -2722,7 +2837,7 @@ dependencies = [
|
|||||||
"block2",
|
"block2",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"calloop",
|
"calloop",
|
||||||
"cfg_aliases",
|
"cfg_aliases 0.2.1",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
@@ -2770,6 +2885,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winreg"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wit-bindgen-rt"
|
name = "wit-bindgen-rt"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
|
|||||||
@@ -9,8 +9,13 @@ tokio = { version = "1.44", features = ["full"] }
|
|||||||
winit = { version = "0.30", features = ["wayland", "x11"] }
|
winit = { version = "0.30", features = ["wayland", "x11"] }
|
||||||
wgpu = "25"
|
wgpu = "25"
|
||||||
glyphon = "0.9"
|
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"
|
env_logger = "0.10"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
pollster = "0.3"
|
||||||
|
|||||||
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.
|
||||||
109
src/app/mod.rs
109
src/app/mod.rs
@@ -1,28 +1,61 @@
|
|||||||
|
use crossbeam_channel::Receiver;
|
||||||
|
use log::{debug, error, info, trace, warn};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
|
use vte::Parser;
|
||||||
use winit::dpi::LogicalSize;
|
use winit::dpi::LogicalSize;
|
||||||
use winit::event::WindowEvent;
|
use winit::{
|
||||||
use winit::window::Window;
|
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 {
|
pub struct Application {
|
||||||
window_state: Option<rendering::State>,
|
|
||||||
runtime: Runtime,
|
runtime: Runtime,
|
||||||
|
event_loop_proxy: EventLoopProxy<UserEvent>,
|
||||||
|
term_state: Option<TermState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Application {
|
impl Application {
|
||||||
pub fn new() -> Self {
|
pub fn new(event_loop_proxy: EventLoopProxy<UserEvent>) -> Self {
|
||||||
|
trace!("Creating new Application");
|
||||||
Self {
|
Self {
|
||||||
window_state: None,
|
|
||||||
runtime: Runtime::new().expect("Failed to create tokio runtime"),
|
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 {
|
impl winit::application::ApplicationHandler<UserEvent> for Application {
|
||||||
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
|
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||||
if self.window_state.is_some() {
|
if self.term_state.is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,40 +64,48 @@ impl winit::application::ApplicationHandler for Application {
|
|||||||
let window_attributes = Window::default_attributes()
|
let window_attributes = Window::default_attributes()
|
||||||
.with_inner_size(LogicalSize::new(width as f64, height as f64))
|
.with_inner_size(LogicalSize::new(width as f64, height as f64))
|
||||||
.with_title("Terminal emulator");
|
.with_title("Terminal emulator");
|
||||||
|
debug!("Creating a 800x600 window");
|
||||||
let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
|
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(
|
fn window_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event_loop: &winit::event_loop::ActiveEventLoop,
|
_event_loop: &ActiveEventLoop,
|
||||||
_window_id: winit::window::WindowId,
|
_window_id: WindowId,
|
||||||
event: WindowEvent,
|
event: WindowEvent,
|
||||||
) {
|
) {
|
||||||
let Some(state) = &mut self.window_state else {
|
if let Some(state) = &mut self.term_state {
|
||||||
return;
|
match event {
|
||||||
};
|
WindowEvent::RedrawRequested => {
|
||||||
|
state.renderer.render(&state.terminal);
|
||||||
match event {
|
}
|
||||||
WindowEvent::Resized(size) => {
|
_ => (),
|
||||||
state.resize(size);
|
|
||||||
}
|
}
|
||||||
WindowEvent::RedrawRequested => {
|
|
||||||
state.render().unwrap();
|
|
||||||
}
|
|
||||||
WindowEvent::KeyboardInput {
|
|
||||||
device_id: _,
|
|
||||||
event,
|
|
||||||
is_synthetic: _,
|
|
||||||
} => {
|
|
||||||
state.handle_keyboard_input(&event);
|
|
||||||
}
|
|
||||||
WindowEvent::ModifiersChanged(modifiers) => {
|
|
||||||
state.update_modifiers(modifiers.state());
|
|
||||||
}
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
181
src/app/renderer.rs
Normal file
181
src/app/renderer.rs
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
use log::{debug, trace};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use wgpu::{
|
||||||
|
CommandEncoderDescriptor, CompositeAlphaMode, Device, DeviceDescriptor, Instance,
|
||||||
|
InstanceDescriptor, Queue, RenderPassColorAttachment, RenderPassDescriptor,
|
||||||
|
RequestAdapterOptions, Surface, SurfaceConfiguration, SurfaceError, TextureFormat,
|
||||||
|
TextureViewDescriptor,
|
||||||
|
};
|
||||||
|
use winit::dpi::PhysicalSize;
|
||||||
|
use winit::window::Window;
|
||||||
|
|
||||||
|
pub struct Renderer {
|
||||||
|
surface: Surface<'static>,
|
||||||
|
device: Device,
|
||||||
|
queue: Queue,
|
||||||
|
config: SurfaceConfiguration,
|
||||||
|
|
||||||
|
size: PhysicalSize<u32>,
|
||||||
|
|
||||||
|
/// text: terminal_text::TerminalText,
|
||||||
|
window: Arc<Window>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Renderer {
|
||||||
|
pub async fn new(window: Arc<Window>) -> Self {
|
||||||
|
let size = window.inner_size();
|
||||||
|
|
||||||
|
// The instance is needed to create an `Adapter` and a `Surface`
|
||||||
|
// Backends::all => Vulkan + Metal + DX12 + Browser WebGPU
|
||||||
|
let instance = Instance::new(&InstanceDescriptor {
|
||||||
|
backends: wgpu::Backends::PRIMARY,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is the part of the window where we draw
|
||||||
|
// The window needs to implement the `HasRawWindowHandle` trait
|
||||||
|
// We also need the surface to request the `Adapter`
|
||||||
|
let surface = instance
|
||||||
|
.create_surface(window.clone())
|
||||||
|
.expect("Failed to create surface");
|
||||||
|
|
||||||
|
// The adapter is the handle to our graphics card. We can get information about the device
|
||||||
|
// with this. We will use it to create the `Device` and `Queue` later
|
||||||
|
// We could enumerate the adapters and find the one that suits us:
|
||||||
|
//
|
||||||
|
// let adapter = instance
|
||||||
|
// .enumerate_adapters(wgpu::Backends::all())
|
||||||
|
// .filter(|adapter| {
|
||||||
|
// // Check if this adapter supports our surface
|
||||||
|
// adapter.is_surface_supported(&surface)
|
||||||
|
// })
|
||||||
|
// .next()
|
||||||
|
// .unwrap()
|
||||||
|
//
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&RequestAdapterOptions {
|
||||||
|
power_preference: wgpu::PowerPreference::default(),
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
force_fallback_adapter: false,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (device, queue) = adapter
|
||||||
|
.request_device(&DeviceDescriptor {
|
||||||
|
// The features field allows to specify the extra features we want
|
||||||
|
// The graphics card you have limits the features you can use. If you want to use certain
|
||||||
|
// features, you may need to limit what devices you support or provide workarounds.
|
||||||
|
// You can get a list of features supported by your device using `adapter.features()` or
|
||||||
|
// `device.features()`.
|
||||||
|
// See: https://docs.rs/wgpu/latest/wgpu/struct.Features.html
|
||||||
|
required_features: wgpu::Features::empty(),
|
||||||
|
// The limits field describes the limits of certain types of resources we can
|
||||||
|
// create. See: https://docs.rs/wgpu/latest/wgpu/struct.Limits.html
|
||||||
|
required_limits: wgpu::Limits::default(),
|
||||||
|
label: None,
|
||||||
|
// Provides the adapter with the preferred memory allocation strategy.
|
||||||
|
// See: https://wgpu.rs/doc/wgpu/enum.MemoryHints.html
|
||||||
|
memory_hints: Default::default(),
|
||||||
|
trace: wgpu::Trace::Off,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// let surface_caps = surface.get_capabilities(&adapter);
|
||||||
|
// // This will define how the surface creates its underlying `SurfaceTexture`
|
||||||
|
// // This assumes an sRGB surface texture. Using a different one will result in all the colors
|
||||||
|
// // coming out darker. If we want to support non sRGB surfaces, we'll need to account for that
|
||||||
|
// // when drawing to the frame.
|
||||||
|
// let surface_format = surface_caps
|
||||||
|
// .formats
|
||||||
|
// .iter()
|
||||||
|
// .find(|f| f.is_srgb())
|
||||||
|
// .copied()
|
||||||
|
// .unwrap_or(surface_caps.formats[0]); // We get the first sRGB format
|
||||||
|
|
||||||
|
let surface_format = TextureFormat::Bgra8UnormSrgb;
|
||||||
|
let config = SurfaceConfiguration {
|
||||||
|
// This field describes how SurfaceTextures will be used. RENDER_ATTACHMENT
|
||||||
|
// specifies that the textures will be used to write to the screen.
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
// The format defines how SurfaceTextures will be stored on the GPU.
|
||||||
|
// We can get a supported format from the SurfaceCapabilities
|
||||||
|
format: surface_format,
|
||||||
|
width: size.width, // Must be > 0
|
||||||
|
height: size.height, // Must be > 0
|
||||||
|
// Takes a `wgpu::PresentMode` option, which determines how to sync the surface with
|
||||||
|
// the display. `PresentMode::Fifo` is VSync.
|
||||||
|
// A list of available modes can be obtained with `&surface_caps.present_modes`
|
||||||
|
// See: https://docs.rs/wgpu/latest/wgpu/enum.PresentMode.html
|
||||||
|
present_mode: wgpu::PresentMode::Fifo,
|
||||||
|
// ???
|
||||||
|
alpha_mode: CompositeAlphaMode::Opaque,
|
||||||
|
//
|
||||||
|
view_formats: vec![],
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
};
|
||||||
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
surface,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
config,
|
||||||
|
size,
|
||||||
|
window,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request_redraw(&self) {
|
||||||
|
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,393 +0,0 @@
|
|||||||
use log::{debug, trace};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use wgpu::{
|
|
||||||
CommandEncoderDescriptor, CompositeAlphaMode, Device, DeviceDescriptor, Instance,
|
|
||||||
InstanceDescriptor, Queue, RenderPassColorAttachment, RenderPassDescriptor,
|
|
||||||
RequestAdapterOptions, Surface, SurfaceConfiguration, SurfaceError, TextureFormat,
|
|
||||||
TextureViewDescriptor,
|
|
||||||
};
|
|
||||||
use winit::dpi::PhysicalSize;
|
|
||||||
use winit::event::{ElementState, KeyEvent};
|
|
||||||
use winit::keyboard::{Key, KeyLocation, ModifiersState, NamedKey};
|
|
||||||
use winit::window::Window;
|
|
||||||
|
|
||||||
mod terminal_text;
|
|
||||||
|
|
||||||
pub struct State {
|
|
||||||
surface: Surface<'static>,
|
|
||||||
device: Device,
|
|
||||||
queue: Queue,
|
|
||||||
config: SurfaceConfiguration,
|
|
||||||
|
|
||||||
size: PhysicalSize<u32>,
|
|
||||||
|
|
||||||
text: terminal_text::TerminalText,
|
|
||||||
|
|
||||||
window: Arc<Window>,
|
|
||||||
|
|
||||||
// Keyboard state tracking
|
|
||||||
modifiers: ModifiersState,
|
|
||||||
dead_key_state: Option<char>,
|
|
||||||
caps_lock_enabled: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl State {
|
|
||||||
pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
|
|
||||||
trace!("Resizing window");
|
|
||||||
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.text.resize(new_size.width, new_size.height);
|
|
||||||
self.window.request_redraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render(&mut self) -> Result<(), SurfaceError> {
|
|
||||||
trace!("Rendering requested");
|
|
||||||
// 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,
|
|
||||||
});
|
|
||||||
|
|
||||||
trace!("Rendering...");
|
|
||||||
self.text.render(&mut _render_pass);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.queue.submit(Some(encoder.finish()));
|
|
||||||
output.present();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn new(window: Arc<Window>) -> Self {
|
|
||||||
let size = window.inner_size();
|
|
||||||
|
|
||||||
// The instance is needed to create an `Adapter` and a `Surface`
|
|
||||||
// Backends::all => Vulkan + Metal + DX12 + Browser WebGPU
|
|
||||||
let instance = Instance::new(&InstanceDescriptor {
|
|
||||||
backends: wgpu::Backends::PRIMARY,
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
// This is the part of the window where we draw
|
|
||||||
// The window needs to implement the `HasRawWindowHandle` trait
|
|
||||||
// We also need the surface to request the `Adapter`
|
|
||||||
let surface = instance
|
|
||||||
.create_surface(window.clone())
|
|
||||||
.expect("Failed to create surface");
|
|
||||||
|
|
||||||
// The adapter is the handle to our graphics card. We can get information about the device
|
|
||||||
// with this. We will use it to create the `Device` and `Queue` later
|
|
||||||
// We could enumerate the adapters and find the one that suits us:
|
|
||||||
//
|
|
||||||
// let adapter = instance
|
|
||||||
// .enumerate_adapters(wgpu::Backends::all())
|
|
||||||
// .filter(|adapter| {
|
|
||||||
// // Check if this adapter supports our surface
|
|
||||||
// adapter.is_surface_supported(&surface)
|
|
||||||
// })
|
|
||||||
// .next()
|
|
||||||
// .unwrap()
|
|
||||||
//
|
|
||||||
let adapter = instance
|
|
||||||
.request_adapter(&RequestAdapterOptions {
|
|
||||||
power_preference: wgpu::PowerPreference::default(),
|
|
||||||
compatible_surface: Some(&surface),
|
|
||||||
force_fallback_adapter: false,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (device, queue) = adapter
|
|
||||||
.request_device(&DeviceDescriptor {
|
|
||||||
// The features field allows to specify the extra features we want
|
|
||||||
// The graphics card you have limits the features you can use. If you want to use certain
|
|
||||||
// features, you may need to limit what devices you support or provide workarounds.
|
|
||||||
// You can get a list of features supported by your device using `adapter.features()` or
|
|
||||||
// `device.features()`.
|
|
||||||
// See: https://docs.rs/wgpu/latest/wgpu/struct.Features.html
|
|
||||||
required_features: wgpu::Features::empty(),
|
|
||||||
// The limits field describes the limits of certain types of resources we can
|
|
||||||
// create. See: https://docs.rs/wgpu/latest/wgpu/struct.Limits.html
|
|
||||||
required_limits: wgpu::Limits::default(),
|
|
||||||
label: None,
|
|
||||||
// Provides the adapter with the preferred memory allocation strategy.
|
|
||||||
// See: https://wgpu.rs/doc/wgpu/enum.MemoryHints.html
|
|
||||||
memory_hints: Default::default(),
|
|
||||||
trace: wgpu::Trace::Off,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// let surface_caps = surface.get_capabilities(&adapter);
|
|
||||||
// // This will define how the surface creates its underlying `SurfaceTexture`
|
|
||||||
// // This assumes an sRGB surface texture. Using a different one will result in all the colors
|
|
||||||
// // coming out darker. If we want to support non sRGB surfaces, we'll need to account for that
|
|
||||||
// // when drawing to the frame.
|
|
||||||
// let surface_format = surface_caps
|
|
||||||
// .formats
|
|
||||||
// .iter()
|
|
||||||
// .find(|f| f.is_srgb())
|
|
||||||
// .copied()
|
|
||||||
// .unwrap_or(surface_caps.formats[0]); // We get the first sRGB format
|
|
||||||
|
|
||||||
let surface_format = TextureFormat::Bgra8UnormSrgb;
|
|
||||||
let config = SurfaceConfiguration {
|
|
||||||
// This field describes how SurfaceTextures will be used. RENDER_ATTACHMENT
|
|
||||||
// specifies that the textures will be used to write to the screen.
|
|
||||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
|
||||||
// The format defines how SurfaceTextures will be stored on the GPU.
|
|
||||||
// We can get a supported format from the SurfaceCapabilities
|
|
||||||
format: surface_format,
|
|
||||||
width: size.width, // Must be > 0
|
|
||||||
height: size.height, // Must be > 0
|
|
||||||
// Takes a `wgpu::PresentMode` option, which determines how to sync the surface with
|
|
||||||
// the display. `PresentMode::Fifo` is VSync.
|
|
||||||
// A list of available modes can be obtained with `&surface_caps.present_modes`
|
|
||||||
// See: https://docs.rs/wgpu/latest/wgpu/enum.PresentMode.html
|
|
||||||
present_mode: wgpu::PresentMode::Fifo,
|
|
||||||
// ???
|
|
||||||
alpha_mode: CompositeAlphaMode::Opaque,
|
|
||||||
//
|
|
||||||
view_formats: vec![],
|
|
||||||
desired_maximum_frame_latency: 2,
|
|
||||||
};
|
|
||||||
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,
|
|
||||||
|
|
||||||
// Initialize keyboard state
|
|
||||||
modifiers: ModifiersState::empty(),
|
|
||||||
dead_key_state: None,
|
|
||||||
caps_lock_enabled: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_modifiers(&mut self, state: ModifiersState) {
|
|
||||||
self.modifiers = state;
|
|
||||||
debug!("Modifiers updated: {:?}", self.modifiers);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_keyboard_input(&mut self, event: &KeyEvent) {
|
|
||||||
// Only process key presses, not releases (except for tracking caps lock)
|
|
||||||
if event.state == ElementState::Released {
|
|
||||||
// Special case for caps lock which toggles on release
|
|
||||||
if let Key::Named(NamedKey::CapsLock) = event.logical_key {
|
|
||||||
self.caps_lock_enabled = !self.caps_lock_enabled;
|
|
||||||
debug!("Caps lock toggled: {}", self.caps_lock_enabled);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
match &event.logical_key {
|
|
||||||
Key::Named(k) => {
|
|
||||||
match k {
|
|
||||||
NamedKey::Enter => {
|
|
||||||
debug!("Receiver enter");
|
|
||||||
self.text.handle_enter();
|
|
||||||
}
|
|
||||||
NamedKey::Backspace => {
|
|
||||||
// Clear dead key state if backspace is pressed
|
|
||||||
if self.dead_key_state.is_some() {
|
|
||||||
self.dead_key_state = None;
|
|
||||||
debug!("Dead key state cleared");
|
|
||||||
} else if self.text.backspace() {
|
|
||||||
debug!("Backspace processed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NamedKey::Tab => {
|
|
||||||
// Insert tab character
|
|
||||||
self.text.insert_char('\t');
|
|
||||||
}
|
|
||||||
NamedKey::Escape => {
|
|
||||||
// Clear any pending dead key state
|
|
||||||
if self.dead_key_state.is_some() {
|
|
||||||
self.dead_key_state = None;
|
|
||||||
debug!("Dead key state cleared by Escape");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NamedKey::ArrowLeft => {
|
|
||||||
if self.text.move_cursor(terminal_text::Motion::Left) {
|
|
||||||
debug!("Cursor moved left");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NamedKey::ArrowRight => {
|
|
||||||
if self.text.move_cursor(terminal_text::Motion::Right) {
|
|
||||||
debug!("Cursor moved right");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NamedKey::ArrowUp => {
|
|
||||||
if self.text.move_cursor(terminal_text::Motion::Up) {
|
|
||||||
debug!("Cursor moved left");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NamedKey::ArrowDown => {
|
|
||||||
if self.text.move_cursor(terminal_text::Motion::Down) {
|
|
||||||
debug!("Cursor moved right");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
debug!("Unhandled named key: {:?}", k);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Key::Character(s) => {
|
|
||||||
// Handle character input, considering dead keys
|
|
||||||
if let Some(dead_char) = self.dead_key_state.take() {
|
|
||||||
// Combine dead key with the current character if possible
|
|
||||||
if let Some(combined) = self.combine_with_dead_key(dead_char, s) {
|
|
||||||
for c in combined.chars() {
|
|
||||||
debug!("Inserting character {c}");
|
|
||||||
self.text.insert_char(c);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If combination not possible, insert both separately
|
|
||||||
self.text.insert_char(dead_char);
|
|
||||||
for c in s.chars() {
|
|
||||||
debug!("Inserting character {c}");
|
|
||||||
self.text.insert_char(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Normal character input
|
|
||||||
for c in s.chars() {
|
|
||||||
debug!("Inserting character {c}");
|
|
||||||
self.text.insert_char(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Key::Dead(dead_key) => {
|
|
||||||
// Store dead key for later combination
|
|
||||||
if let Some(c) = dead_key {
|
|
||||||
self.dead_key_state = Some(*c);
|
|
||||||
debug!("Dead key registered: {:?}", c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Key::Unidentified(unidentified) => {
|
|
||||||
debug!(
|
|
||||||
"Unidentified key: {:?}, location: {:?}",
|
|
||||||
unidentified, event.physical_key
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.window.request_redraw();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to combine a dead key with a character
|
|
||||||
fn combine_with_dead_key(&self, dead_key: char, input: &str) -> Option<String> {
|
|
||||||
// This is a simplified implementation - for a complete solution,
|
|
||||||
// you would need a more comprehensive mapping of dead key combinations
|
|
||||||
|
|
||||||
if input.chars().count() != 1 {
|
|
||||||
return None; // Can only combine with single characters
|
|
||||||
}
|
|
||||||
|
|
||||||
let base_char = input.chars().next().unwrap();
|
|
||||||
|
|
||||||
// Common dead key combinations
|
|
||||||
match (dead_key, base_char) {
|
|
||||||
// Acute accent (´)
|
|
||||||
('´', 'a') => Some("á".to_string()),
|
|
||||||
('´', 'e') => Some("é".to_string()),
|
|
||||||
('´', 'i') => Some("í".to_string()),
|
|
||||||
('´', 'o') => Some("ó".to_string()),
|
|
||||||
('´', 'u') => Some("ú".to_string()),
|
|
||||||
('´', 'A') => Some("Á".to_string()),
|
|
||||||
('´', 'E') => Some("É".to_string()),
|
|
||||||
('´', 'I') => Some("Í".to_string()),
|
|
||||||
('´', 'O') => Some("Ó".to_string()),
|
|
||||||
('´', 'U') => Some("Ú".to_string()),
|
|
||||||
|
|
||||||
// Grave accent (`)
|
|
||||||
('`', 'a') => Some("à".to_string()),
|
|
||||||
('`', 'e') => Some("è".to_string()),
|
|
||||||
('`', 'i') => Some("ì".to_string()),
|
|
||||||
('`', 'o') => Some("ò".to_string()),
|
|
||||||
('`', 'u') => Some("ù".to_string()),
|
|
||||||
('`', 'A') => Some("À".to_string()),
|
|
||||||
('`', 'E') => Some("È".to_string()),
|
|
||||||
('`', 'I') => Some("Ì".to_string()),
|
|
||||||
('`', 'O') => Some("Ò".to_string()),
|
|
||||||
('`', 'U') => Some("Ù".to_string()),
|
|
||||||
|
|
||||||
// Circumflex (^)
|
|
||||||
('^', 'a') => Some("â".to_string()),
|
|
||||||
('^', 'e') => Some("ê".to_string()),
|
|
||||||
('^', 'i') => Some("î".to_string()),
|
|
||||||
('^', 'o') => Some("ô".to_string()),
|
|
||||||
('^', 'u') => Some("û".to_string()),
|
|
||||||
('^', 'A') => Some("Â".to_string()),
|
|
||||||
('^', 'E') => Some("Ê".to_string()),
|
|
||||||
('^', 'I') => Some("Î".to_string()),
|
|
||||||
('^', 'O') => Some("Ô".to_string()),
|
|
||||||
('^', 'U') => Some("Û".to_string()),
|
|
||||||
|
|
||||||
// Diaeresis/umlaut (¨)
|
|
||||||
('¨', 'a') => Some("ä".to_string()),
|
|
||||||
('¨', 'e') => Some("ë".to_string()),
|
|
||||||
('¨', 'i') => Some("ï".to_string()),
|
|
||||||
('¨', 'o') => Some("ö".to_string()),
|
|
||||||
('¨', 'u') => Some("ü".to_string()),
|
|
||||||
('¨', 'A') => Some("Ä".to_string()),
|
|
||||||
('¨', 'E') => Some("Ë".to_string()),
|
|
||||||
('¨', 'I') => Some("Ï".to_string()),
|
|
||||||
('¨', 'O') => Some("Ö".to_string()),
|
|
||||||
('¨', 'U') => Some("Ü".to_string()),
|
|
||||||
|
|
||||||
// Tilde (~)
|
|
||||||
('~', 'a') => Some("ã".to_string()),
|
|
||||||
('~', 'n') => Some("ñ".to_string()),
|
|
||||||
('~', 'o') => Some("õ".to_string()),
|
|
||||||
('~', 'A') => Some("Ã".to_string()),
|
|
||||||
('~', 'N') => Some("Ñ".to_string()),
|
|
||||||
('~', 'O') => Some("Õ".to_string()),
|
|
||||||
|
|
||||||
// Cedilla (¸)
|
|
||||||
('¸', 'c') => Some("ç".to_string()),
|
|
||||||
('¸', 'C') => Some("Ç".to_string()),
|
|
||||||
|
|
||||||
// If no combination is defined, return None
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
use glyphon::{
|
|
||||||
Buffer, Cache, Color, Cursor, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
|
|
||||||
TextBounds, TextRenderer, Viewport, Wrap,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use glyphon::cosmic_text::Motion;
|
|
||||||
|
|
||||||
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,
|
|
||||||
max_scroll_lines: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// - need to update scroll when
|
|
||||||
// - inserting into a line and it causes line wrap, creating a new visual line (currently the
|
|
||||||
// scroll is only updated when creating a logical line or removing the last logical line)
|
|
||||||
//
|
|
||||||
// - need to keep track of the scroll vertical offset in two ways:
|
|
||||||
// - first logical line displayed
|
|
||||||
// - extra offset that covers possible wrapped lines before the first logical line
|
|
||||||
//
|
|
||||||
// - need to remove lines from the top of the buffer when the first logical line displayed is
|
|
||||||
// bigger than max_scroll_offset
|
|
||||||
|
|
||||||
mod safe_casts;
|
|
||||||
|
|
||||||
use log::{debug, error, trace};
|
|
||||||
|
|
||||||
impl TerminalText {
|
|
||||||
pub fn new(device: &Device, queue: &Queue, surface_format: TextureFormat) -> TerminalText {
|
|
||||||
debug!("Creating a new TerminalText object");
|
|
||||||
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);
|
|
||||||
|
|
||||||
trace!("Loading system fonts");
|
|
||||||
// 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,
|
|
||||||
},
|
|
||||||
max_scroll_lines: 100,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the bounds delimited by the `start` and `end` lines
|
|
||||||
fn get_text_bounds(&self, start: usize, end: usize) -> TextBounds {
|
|
||||||
if start >= self.buffer.lines.len() || end > self.buffer.lines.len() {
|
|
||||||
// Return default bounds for invalid line
|
|
||||||
return TextBounds {
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let line_height = self.buffer.metrics().line_height;
|
|
||||||
let viewport_width = self.viewport.resolution().width;
|
|
||||||
|
|
||||||
let layout_iter = self.buffer.layout_runs();
|
|
||||||
let mut top_line = 0;
|
|
||||||
let mut bottom_line = 0;
|
|
||||||
|
|
||||||
let mut found_start_line = false;
|
|
||||||
let mut found_end_line = false;
|
|
||||||
|
|
||||||
for (visual_line_count, run) in layout_iter.enumerate() {
|
|
||||||
if run.line_i == start && !found_start_line {
|
|
||||||
top_line = visual_line_count;
|
|
||||||
found_start_line = true;
|
|
||||||
}
|
|
||||||
if run.line_i == end {
|
|
||||||
if !found_end_line {
|
|
||||||
found_end_line = true;
|
|
||||||
}
|
|
||||||
bottom_line = visual_line_count + 1;
|
|
||||||
} else if found_end_line {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate bounds based on visual line positions
|
|
||||||
let top_f32 = (safe_casts::usize_to_f32_or_max(top_line) * line_height).floor();
|
|
||||||
let bottom_f32 = (safe_casts::usize_to_f32_or_max(bottom_line) * line_height).ceil();
|
|
||||||
|
|
||||||
// Safe conversions with overflow checks
|
|
||||||
let top = safe_casts::f32_to_i32_or_bound(top_f32);
|
|
||||||
let bottom = safe_casts::f32_to_i32_or_bound(bottom_f32);
|
|
||||||
|
|
||||||
// Ensure viewport width doesn't exceed i32::MAX
|
|
||||||
let right = safe_casts::u32_to_i32_or_max(viewport_width.min(i32::MAX as u32));
|
|
||||||
|
|
||||||
TextBounds {
|
|
||||||
left: 0,
|
|
||||||
top,
|
|
||||||
right,
|
|
||||||
bottom,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_visible_line_range(&self) -> (usize, usize) {
|
|
||||||
let viewport_height = safe_casts::u32_to_f32_or_max(self.viewport.resolution().height);
|
|
||||||
let line_height = self.buffer.metrics().line_height;
|
|
||||||
|
|
||||||
let start_line = self.buffer.scroll().line;
|
|
||||||
|
|
||||||
trace!("Current start line: {start_line}");
|
|
||||||
|
|
||||||
// Calculate visible lines based on wrapped text
|
|
||||||
let layout_iter = self.buffer.layout_runs();
|
|
||||||
let mut last_logical_line = 0;
|
|
||||||
|
|
||||||
// Count how many visual lines we have and map to logical lines
|
|
||||||
for (visual_line_count, run) in layout_iter.enumerate() {
|
|
||||||
last_logical_line = run.line_i;
|
|
||||||
|
|
||||||
// If we've exceeded the viewport height, we can stop counting
|
|
||||||
if (safe_casts::usize_to_f32_or_max(visual_line_count) * line_height) > viewport_height
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add 1 to include partially visible lines at the bottom
|
|
||||||
let end_line = last_logical_line;
|
|
||||||
|
|
||||||
trace!("visible line range goes from {start_line} to {end_line}");
|
|
||||||
|
|
||||||
(start_line, end_line)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_visible_text_rendered(&mut self) {
|
|
||||||
let (start_line, end_line) = self.get_visible_line_range();
|
|
||||||
self.dirty_regions
|
|
||||||
.push(self.get_text_bounds(start_line, end_line));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn merge_dirty_regions(&self) -> TextBounds {
|
|
||||||
if self.dirty_regions.is_empty() {
|
|
||||||
return TextBounds::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.dirty_regions.iter().fold(
|
|
||||||
TextBounds {
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
},
|
|
||||||
|a, b| TextBounds {
|
|
||||||
left: a.left.min(b.left),
|
|
||||||
top: a.top.min(b.top),
|
|
||||||
right: a.right.max(b.right),
|
|
||||||
bottom: a.bottom.max(b.bottom),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let region = self.merge_dirty_regions();
|
|
||||||
trace!("Preparing region {:?}", region);
|
|
||||||
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();
|
|
||||||
|
|
||||||
self.dirty_regions.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render(&mut self, render_pass: &mut RenderPass) {
|
|
||||||
self.renderer
|
|
||||||
.render(&self.atlas, &self.viewport, render_pass)
|
|
||||||
.unwrap();
|
|
||||||
self.atlas.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_update<F, R>(&mut self, operation: F) -> R
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Self) -> R,
|
|
||||||
{
|
|
||||||
let result = operation(self);
|
|
||||||
|
|
||||||
let mut scroll = self.buffer.scroll();
|
|
||||||
if self.buffer.lines.len() > self.max_scroll_lines {
|
|
||||||
self.buffer.lines.remove(0);
|
|
||||||
self.cursor.line -= 1;
|
|
||||||
}
|
|
||||||
scroll.line = self.buffer.lines.len();
|
|
||||||
self.buffer.set_scroll(scroll);
|
|
||||||
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
|
||||||
self.ensure_visible_text_rendered();
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resize(&mut self, width: u32, height: u32) {
|
|
||||||
trace!("Resizing window - Width: {width} Height: {height}");
|
|
||||||
|
|
||||||
self.with_update(|this| {
|
|
||||||
this.buffer.set_size(
|
|
||||||
&mut this.font_system,
|
|
||||||
Some(safe_casts::u32_to_f32_or_max(width)),
|
|
||||||
Some(safe_casts::u32_to_f32_or_max(height)),
|
|
||||||
);
|
|
||||||
// Update the buffer's wrapping based on the new width
|
|
||||||
this.buffer.set_wrap(&mut this.font_system, Wrap::Glyph)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insert_char(&mut self, c: char) {
|
|
||||||
self.with_update(|this| {
|
|
||||||
let line = this.cursor.line;
|
|
||||||
|
|
||||||
if line >= this.buffer.lines.len() {
|
|
||||||
error!(
|
|
||||||
"insert_char called when cursor is out of bounds! lines: {}, cursor: {}",
|
|
||||||
this.buffer.lines.len(),
|
|
||||||
line
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut text = this.buffer.lines[line].text().to_string().clone();
|
|
||||||
let pos = this.cursor.index.min(text.len());
|
|
||||||
trace!("Inserting char {c} in line {line}, position {pos}");
|
|
||||||
text.insert(pos, c);
|
|
||||||
|
|
||||||
let ending = this.buffer.lines[line].ending();
|
|
||||||
let attrs = this.buffer.lines[line].attrs_list().clone();
|
|
||||||
|
|
||||||
// Update the buffer with the new text
|
|
||||||
this.buffer.lines[line].set_text(&text, ending, attrs);
|
|
||||||
// Update cursor position
|
|
||||||
this.cursor.index = pos + 1;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_enter(&mut self) {
|
|
||||||
self.with_update(|this| {
|
|
||||||
let line = this.cursor.line;
|
|
||||||
|
|
||||||
// Ensure line is within bounds
|
|
||||||
if line >= this.buffer.lines.len() {
|
|
||||||
error!(
|
|
||||||
"handle_enter called when cursor is out of bounds! lines: {}, cursor: {}",
|
|
||||||
this.buffer.lines.len(),
|
|
||||||
line
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pos = this.cursor.index.min(this.buffer.lines[line].text().len());
|
|
||||||
trace!("Inserting newline in line {line}, position {pos}");
|
|
||||||
let new_line = this.buffer.lines[line].split_off(pos);
|
|
||||||
|
|
||||||
// Update cursor position to beginning of next line
|
|
||||||
this.cursor.line = line + 1;
|
|
||||||
this.cursor.index = 0;
|
|
||||||
// Create a new line with text after cursor
|
|
||||||
this.buffer.lines.insert(this.cursor.line, new_line);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn backspace(&mut self) -> bool {
|
|
||||||
self.with_update(|this| {
|
|
||||||
let line = this.cursor.line;
|
|
||||||
|
|
||||||
// Ensure line is within bounds
|
|
||||||
if line >= this.buffer.lines.len() {
|
|
||||||
error!(
|
|
||||||
"handle_enter called when cursor is out of bounds! lines: {}, cursor: {}",
|
|
||||||
this.buffer.lines.len(),
|
|
||||||
line
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_line = this.buffer.lines[line].text().to_string();
|
|
||||||
let pos = this.cursor.index;
|
|
||||||
|
|
||||||
let mut ret = false;
|
|
||||||
|
|
||||||
if pos > 0 {
|
|
||||||
// Delete character before cursor in current line
|
|
||||||
let mut new_text = current_line.clone();
|
|
||||||
new_text.remove(pos - 1);
|
|
||||||
|
|
||||||
// Update the line
|
|
||||||
let ending = this.buffer.lines[line].ending();
|
|
||||||
let attrs = this.buffer.lines[line].attrs_list().clone();
|
|
||||||
this.buffer.lines[line].set_text(&new_text, ending, attrs);
|
|
||||||
|
|
||||||
// Move cursor back
|
|
||||||
this.cursor.index = pos - 1;
|
|
||||||
|
|
||||||
ret = true;
|
|
||||||
} else if line > 0 {
|
|
||||||
// Move cursor to join point
|
|
||||||
this.cursor.line = line - 1;
|
|
||||||
this.cursor.index = this.buffer.lines[this.cursor.line].text().len();
|
|
||||||
let current_line = this.buffer.lines[line].clone();
|
|
||||||
// At beginning of line, join with previous line
|
|
||||||
this.buffer.lines[this.cursor.line].append(current_line);
|
|
||||||
// Remove current line
|
|
||||||
this.buffer.lines.remove(line);
|
|
||||||
|
|
||||||
ret = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
ret
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cursor movement methods
|
|
||||||
pub fn move_cursor(&mut self, motion: Motion) -> bool {
|
|
||||||
self.with_update(|this| {
|
|
||||||
match this.buffer.cursor_motion(
|
|
||||||
&mut this.font_system,
|
|
||||||
this.cursor,
|
|
||||||
Some(safe_casts::usize_to_i32_or_max(this.cursor.index)),
|
|
||||||
motion,
|
|
||||||
) {
|
|
||||||
None => false,
|
|
||||||
Some((cursor, _cursor_x_opt)) => {
|
|
||||||
this.cursor = cursor;
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
use log::trace;
|
|
||||||
|
|
||||||
// Safe conversions with overflow checks
|
|
||||||
|
|
||||||
pub fn u32_to_i32_or_max(n: u32) -> i32 {
|
|
||||||
if n > i32::MAX as u32 {
|
|
||||||
trace!(
|
|
||||||
"Overflow casting {n}::u32 as i32, defaulting to {}",
|
|
||||||
i32::MAX
|
|
||||||
);
|
|
||||||
i32::MAX
|
|
||||||
} else {
|
|
||||||
n as i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn u32_to_f32_or_max(n: u32) -> f32 {
|
|
||||||
if n > f32::MAX as u32 {
|
|
||||||
trace!(
|
|
||||||
"Overflow casting {n}::u32 as f32, defaulting to {}",
|
|
||||||
f32::MAX
|
|
||||||
);
|
|
||||||
f32::MAX
|
|
||||||
} else {
|
|
||||||
n as f32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn usize_to_f32_or_max(n: usize) -> f32 {
|
|
||||||
if n > f32::MAX as usize {
|
|
||||||
trace!(
|
|
||||||
"Overflow casting {n}::usize as f32, defaulting to {}",
|
|
||||||
f32::MAX
|
|
||||||
);
|
|
||||||
f32::MAX
|
|
||||||
} else {
|
|
||||||
n as f32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn usize_to_i32_or_max(n: usize) -> i32 {
|
|
||||||
if n > i32::MAX as usize {
|
|
||||||
trace!(
|
|
||||||
"Overflow casting {n}::usize as i32, defaulting to {}",
|
|
||||||
i32::MAX
|
|
||||||
);
|
|
||||||
i32::MAX
|
|
||||||
} else {
|
|
||||||
n as i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn f32_to_i32_or_bound(n: f32) -> i32 {
|
|
||||||
if n > i32::MAX as f32 {
|
|
||||||
trace!(
|
|
||||||
"Overflow casting {n}::f32 as i32, defaulting to {}",
|
|
||||||
i32::MAX
|
|
||||||
);
|
|
||||||
i32::MAX
|
|
||||||
} else if n < i32::MIN as f32 {
|
|
||||||
trace!(
|
|
||||||
"Underflow casting {n}::f32 as i32, defaulting to {}",
|
|
||||||
i32::MIN
|
|
||||||
);
|
|
||||||
i32::MIN
|
|
||||||
} else {
|
|
||||||
n as i32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
use winit::event_loop::EventLoop;
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
@@ -10,9 +10,17 @@ fn main() {
|
|||||||
// Log platform information
|
// Log platform information
|
||||||
info!("Starting simplicitty terminal emulator");
|
info!("Starting simplicitty terminal emulator");
|
||||||
|
|
||||||
// Create event loop with explicit backend preference
|
debug!("Creating winit event loop");
|
||||||
let event_loop = EventLoop::new().unwrap();
|
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
|
// 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