feat(terminal): port terminal search and the tmux control-mode viewer
search/: sliding-window matcher over pages, active-area, page list, viewport and screen searches, and the background search thread (std thread + crossbeam-channel mailbox, 24 ms refresh, terminal behind Arc<Mutex<_>>). Searches take &mut Screen explicitly; ScreenSearch::deinit must be called to untrack pins. The sliding window uses a VecDeque instead of CircBuf (same results; two layout-only assertions dropped). tmux/: layout parsing, output formatting and the control-mode viewer state machine, reusing ghostty_vt::tmux::control for parsing. The viewer is not wired into termio yet; steps are in the notes. The renderer must set terminal.flags.search_viewport_dirty for the search thread to re-search the viewport. Test Plan: - cargo test -p ghostty-terminal --lib search (54; 51/53 Zig tests, 2 allocator-failure tests skipped) - cargo test -p ghostty-terminal --lib tmux (96; all Zig tests)
This commit is contained in:
1 parent
3742a131b8
commit
db1d4fe41d
14 files changed
+6784
No files matched your search
@@ -0,0 +1,64 @@
|
||||
# ghostty-terminal: search/* and tmux/* (agent T5)
|
||||
|
||||
## Ported
|
||||
| Zig | Rust | tests (ported/Zig) |
|
||||
|---|---|---|
|
||||
| search.zig | `search/mod.rs` | - |
|
||||
| search/sliding_window.zig | `search/sliding_window.rs` | 25/25 |
|
||||
| search/active.zig | `search/active.rs` | 2/2 |
|
||||
| search/pagelist.zig | `search/pagelist.rs` | 7/7 |
|
||||
| search/viewport.zig | `search/viewport.rs` | 4/4 |
|
||||
| search/screen.zig | `search/screen.rs` | 11/13 |
|
||||
| search/Thread.zig | `search/thread.rs` | 1/1 (unnamed `test {}` -> `search_thread_basic`) + 1 extra (`search_thread_select_and_state`) |
|
||||
| tmux.zig | `tmux/mod.rs` (re-exports `ghostty_vt::tmux::control`) | - |
|
||||
| tmux/layout.zig | `tmux/layout.rs` | 43 (24 layout + 19 checksum) |
|
||||
| tmux/output.zig | `tmux/output.rs` | 37 |
|
||||
| tmux/viewer.zig | `tmux/viewer.rs` | 8/8 |
|
||||
|
||||
Skipped (Zig-only): the two `reloadActive partial history cleanup on ... error` tests in search/screen.zig
|
||||
(tripwire allocator-failure injection). output.zig comptime-format variants are covered by the runtime `format()` tests.
|
||||
|
||||
## Search design / deviations
|
||||
- Uses `crate::highlight::{Flattened, Untracked, Tracked, Chunk}` and `crate::formatter::{PageFormatter, Options}`
|
||||
(plain + `unwrap` with a point map) from T4. `NodeId` carries the node, `Chunk.serial` is the node serial.
|
||||
- Nothing stores `*Screen` / `*PageList`. Everything touching tracked pins takes the screen explicitly:
|
||||
`ScreenSearch::new(&mut Screen, needle)`, `.deinit(&mut Screen)` (must be called to untrack pins),
|
||||
`.feed(&mut Screen)`, `.reload_active(&mut Screen)`, `.select(&mut Screen, Select)`, `.search_all(&mut Screen)`.
|
||||
`tick()`, `matches()`, `selected_match()`, `matches_len()` need no screen. `PageListSearch::{new,feed,deinit}` take `&mut PageList`.
|
||||
`ActiveSearch::update(&PageList) -> Option<NodeId>`, `ViewportSearch::update(&PageList) -> bool` (infallible).
|
||||
`SlidingWindow::append(&PageList, NodeId) -> usize`.
|
||||
- `SlidingWindow` uses `VecDeque` (made contiguous for searching) instead of `CircBuf` + overlap buffer; results are identical.
|
||||
The Zig assertions about circular-buffer slice layout in the "across circular buffer boundary" tests are dropped (behaviour kept).
|
||||
Row counts are cached per page at `append` time. Needle must be non-empty (Zig has the same underflow).
|
||||
`next()` returns an owned `Flattened`, so Zig's `clone()` dance is unnecessary.
|
||||
- `ScreenSearch` caches `HistorySearch.start_node` (Zig reads `start_pin.node` lock-free in `tick`); refreshed in `feed`/`reload_active`.
|
||||
Screen identity (`screen_id`, address) replaces Zig's `*Screen` pointer comparison for "screen was reinitialized" in the thread.
|
||||
- `Thread`: libxev replaced by crossbeam-channel: bounded(64) mailbox + control channel (wakeup/stop) + 24 ms deadline refresh timer.
|
||||
API: `Thread::new(Options)`, `.handle() -> Handle {send,try_send,wakeup,stop}`, `.thread_main()` or `Thread::spawn(opts) -> (Handle, JoinHandle)`.
|
||||
`Options { terminal: Arc<Mutex<Terminal>>, event_cb: Option<Arc<dyn Fn(Event)+Send+Sync>> }`;
|
||||
`Message::{ChangeNeedle(Vec<u8>), Select(Select)}` (Zig `MessageData(u8,255)` -> `Vec<u8>`); `Event` carries owned data
|
||||
(`ViewportMatches(Vec<Flattened>)`). The search is torn down under the lock (pins untracked) when the thread exits or the needle changes.
|
||||
Dependency added: `crossbeam-channel`.
|
||||
- Terminal hook already present: `Terminal.flags.search_viewport_dirty` (the thread reads/clears it).
|
||||
- `cargo fmt -p ghostty-terminal` was run for the whole crate (not just my files).
|
||||
|
||||
## tmux design / deviations
|
||||
- `ghostty_vt::tmux::control` is reused (`tmux::control`, plus `ControlParser`/`ControlNotification` aliases).
|
||||
- output.rs: comptime `FormatStruct` -> `Variable` enum + `FormatValues` (`.bool()/.usize()/.str()`); `Variable::parse` keeps
|
||||
Zig's `InvalidCharacter`/`Overflow`/`FormatError` distinction; `parse_format_struct` folds into `ParseError::FormatError`.
|
||||
- layout.rs: `Layout` owns children (`Vec`), `ParseError::{SyntaxError, ChecksumMismatch}`.
|
||||
- viewer.rs: `Viewer::new()`, `next(Input) -> Vec<Action>` (owned; no arena); `Action::{Command(Vec<u8>), Windows(Vec<..> clone), Exit}`;
|
||||
public `session_id`, `tmux_version`, `windows`, `panes` (insertion-ordered `PanesMap`). `sync_layouts` creates terminals before
|
||||
committing; any command-processing failure goes defunct like Zig. `Notification::Enter` must NOT be fed to the viewer (unreachable, as Zig).
|
||||
Unknown extra `Notification` variants are ignored.
|
||||
|
||||
## TODO(integration) - tmux viewer wiring (nothing in terminal.rs is required)
|
||||
1. In the DCS state kept by `ghostty_vt::dcs`/stream, on DCS `1000p` (`Notification::Enter`) create `Viewer::new()` (do not pass Enter to it).
|
||||
2. Feed every later `ghostty_vt::tmux::control::Parser` notification via `viewer.next(Input::Tmux(n))`.
|
||||
3. Execute returned actions in termio: `Command(bytes)` -> write to pty; `Windows(w)` -> diff in apprt; `Exit` -> drop viewer, leave control mode.
|
||||
4. Pane contents are in `viewer.panes.get(id).terminal`; renderer/apprt render from there.
|
||||
|
||||
## TODO(integration) - search wiring
|
||||
- Core owns the search thread: keep `Handle`; on search-start/changed text send `Message::ChangeNeedle`, navigation sends `Message::Select`.
|
||||
Events arrive on the search thread via `event_cb` (forward to the apprt main loop). The renderer must set
|
||||
`terminal.flags.search_viewport_dirty = true` when it finds viewport/active dirty (as Zig), so the thread re-searches.
|
||||
Reference in new issue
Block a user