Files
ghostty-rs/docs/rewrite/notes/ghostty-inspector.md
T
ddidderr d1a5e55198 feat(inspector): port the terminal inspector
Ports ZIG/src/inspector (the Dear ImGui terminal inspector) as the
ghostty-inspector crate using dear-imgui-rs + dear-imgui-glow (imgui-rs
lacks a safe docking API): screen, modes, keyboard events, terminal IO
/ VT sequence log with filtering, cell inspector with picking, page
list, renderer and size/font info, with Ghostty's docking layout. The
crate denies unsafe except a single dock.rs module wrapping DockBuilder.

GTK: imgui_widget, inspector_widget and inspector_window classes with
their blueprints replace the former no-op; ctrl+shift+i toggles the
inspector window.

Core: activate/deactivate hooks, key event and mouse recording, and VT
stream recording in termio while an inspector is active, as in Zig;
overlay features reach the renderer through an InspectorHook on the
render state. The debug input replay hook gained inspector-screenshot/
click/scroll steps used to verify all tabs in the real app.

Deviations: font size scales with the display scale instead of Zig's
fixed 16*2; pop-out dock splits apply one frame later; DCS/APC
descriptions are approximated.

Test Plan:
- cargo test -p ghostty-inspector (15 incl. headless full-UI draw)
- cargo test -p ghostty-core --test e2e_inspector (4)
2026-10-12 04:39:14 +02:00

85 lines
7.6 KiB
Markdown

# ghostty-inspector (agent I1): terminal inspector
Ports `ZIG/src/inspector/*` (Inspector, widgets/{key,page,pagelist,renderer,screen,style,surface,terminal,termio}, units),
the GTK classes `imgui_widget`, `inspector_widget`, `inspector_window` (+ their `.blp`), and the core Surface hooks.
## Stack
`dear-imgui-rs` 0.19 (Dear ImGui 1.92.9b docking branch, builds fine with rustc 1.99) + `dear-imgui-glow` 0.19 (glow 0.18, the
same glow as the renderer). imgui-rs 0.12 was not used: it has no safe docking/DockBuilder and an old ImGui. The safe API of
`dear-imgui-rs` is strict (it panics on API misuse, e.g. table phases, dockspace ordering), which the headless test below exercises.
## Layout
* `crates/ghostty-inspector` (`unsafe_code = "deny"`)
* `inspector.rs`: `Inspector` (`mouse`, `gui`), `setup(ctx, scale)`, `Shared` (= `Mutex<Inspector>` newtype implementing
`ghostty_renderer::state::InspectorHook`).
* `widgets/*.rs`: 1:1 with the Zig widget files (`surface.rs` has the dockspace + Surface info + `SurfaceInfo` input struct).
* `keys.rs`: `tag_name` (Zig `@tagName` from `Debug`), `imgui_key` (Zig `Key.imguiKey`).
* `dock.rs`: the ONLY `unsafe` module (`#![allow(unsafe_code)]`, all SAFETY-commented): `DockBuilder` (node create/split/dock),
`SetNextWindowSize`, `SetNextItemAllowOverlap`, and a raw `Begin`/`End` scope (`begin_window`) so that widgets still run when
`Begin` returned false, like Zig's `Begin` + `defer End`.
* `ghostty-renderer::state`: `State.overlay_features` replaced by `State.inspector: Option<Arc<dyn InspectorHook>>` (Zig
`inspector: ?*Inspector`); `generic.rs` reads `overlay_features()` from it each frame. The trait breaks the crate cycle.
* `ghostty-termio`: `Output::process_output` goes into Zig's "slow mode" while an inspector is set (`record_pty_read` per byte, then
`process_byte`).
* `ghostty-core` (`surface/inspector.rs`): `activate_inspector` / `deactivate_inspector` (render-state pointer + renderer and IO
`Inspector(bool)` messages), `render_inspector(ui)` (locks terminal then inspector, fills `SurfaceInfo`, wakes the renderer),
key event recording in `keyboard.rs` (event created after key remapping, `binding` set in the consumed branch, `pty` copied in
`encode_key`, recorded + `queue_render` when done, only if the inspector is still active), mouse recording in
`mouse.rs` (cursor pos, `queue_render` on buttons). The `inspector` binding action already forwarded `RtAction::Inspector`.
* `ghostty-apprt` `CoreSurface`: new `render_inspector(&Ui)` next to `activate/deactivate_inspector`.
* `ghostty-apprt-gtk`: `class/{imgui_widget,inspector_widget,inspector_window}.rs`, `resources/ui/1.5/{imgui-widget,inspector-widget,
inspector-window}.blp` (verbatim from Zig), `Surface::control_inspector` (toggle/show/hide, weak ref to the window) and
`redraw_inspector`, `Window::toggle_inspector` (`win.toggle-inspector`, ctrl+shift+i). Unsafe only for `glow::Context::from_loader_function`
(epoxy loader) and the GL read-back of the debug screenshot, both `#[allow(unsafe_code)]` + SAFETY.
## GTK input/rendering (imgui_widget)
GLArea + `IMMulticontext` as in Zig: focus/key/click/motion/scroll/IM-commit controllers feed `Io` (modifier keys as LeftShift/Ctrl/Alt/Super
events, keyval -> `imgui_key`), 30 FPS tick callback, two ImGui frames per GL render (first one only `end_frame`), then clear + draw.
Zig clears with `0x28/0xFF` which is integer 0, so the background is black; kept.
## Debug / verification
`GHOSTTY_DEBUG_INPUT_SCRIPT` got `inspector-screenshot <png>`, `inspector-click <x> <y>`, `inspector-scroll <x> <y> <dy>`
(`debug_input.rs`). Inspector frames are rendered synchronously for these steps because the compositor does not send frame
callbacks to an off-screen window (without this the screenshot lags one event behind). Run:
`GSETTINGS_BACKEND=memory GHOSTTY_DEBUG_INPUT_SCRIPT=script.txt timeout 25 ./target/debug/ghostty -e sh` with
`key ctrl+shift+i`, `wait 2500`, `inspector-click 126 11`, `inspector-screenshot /abs/x.png`, ...
Verified in the real app on Wayland (screenshots viewed): all tabs (Terminal with screens/Misc/Layout/Mouse/Color/Modes tables, Surface,
Keyboard event table with pty encoding, Terminal IO VT log, Renderer overlays, ImGui demo), screen pop-out window with Info/Cell/PageList
docking (cell picking resolves the cell at (0,0)), detach of a header into a docked window, toggle open/close/reopen, closing a surface
and a tab while its inspector is open (no warnings, core deactivates in the surface's `dispose`).
## Tests
* ghostty-inspector: 15 unit tests (Zig has none for this package): label/pty/utf8 formatting, ring bound, hsv/key colour, text filter
(ImGuiTextFilter semantics), OSC/CSI/ESC/Execute encodings and event recording through a real `Stream`, features toggle, snake_case,
imgui key map, mouse button labels, plus `draws_frames_headless`: draws 8 frames of the whole inspector headlessly (all windows, pop-out
screen window, palette window, detached headers, selected VT event) to catch misuse of the strict imgui API.
* ghostty-core `tests/e2e_inspector.rs`: 4 tests (key events with pty data + binding, VT events from a real child, overlay features
reach the render state, activate/deactivate semantics).
* apprt-gtk `debug_input` parse test extended.
Skipped: none (Zig package has no tests).
## Deviations
* Font size is `16 * scale_factor` (Zig hardcodes `16 * 2` with a TODO). The imgui style is scaled by the change of the factor
(a `Style` cannot be re-created from Rust like Zig's `ImGuiStyle_ImGuiStyle`).
* `GhosttyImguiWidget` virtual methods (`setup`/`render`) are hooks installed with `ImguiWidget::set_hooks` (gtk-rs class-struct
boilerplate avoided); the widget hierarchy `InspectorWidget -> ImguiWidget -> AdwBin` is real.
* The inspector widget keeps an `Rc<dyn CoreSurface>` and the GTK Surface destroys its inspector window in `dispose` (Zig used a
GObject weak notify to call `deactivateInspector` while the core surface is alive).
* DockBuilder splits requested by the pop-out headers (`DetachableHeader`, `detachable_header`) are queued and applied after all windows
of the frame ended (`dock::flush_splits`): doing the split while the parent window is being built re-parents its dock node and trips
Dear ImGui's "Must call EndChild() and not End()" assertion. The popped-out window therefore docks one frame later.
* Popups: the VT filter popup uses a Rust port of `ImGuiTextFilter` (`TextFilter`, same Build/PassFilter semantics) because the safe wrapper's
type holds a raw pointer (not `Send`) and the inspector is shared with the IO thread.
* VT events: `Parser.Action.format` for DCS/APC is approximated (`parser.Parser.Action{ .dcs_hook = ... }`); the OSC metadata for
`color_operation.requests` / `kitty_color_protocol.list` print the Zig container type name; metadata keeps insertion order (Zig hash map order).
`vt_raw` only sees parser-driven actions (CSI/ESC/OSC/DCS/APC), exactly like Zig: printable text and C0 controls in the ground state
are dispatched straight to the handler, so they never appear in the log.
* The recorded cursor is a `CursorSnapshot` (displayed fields only) instead of a copy of the whole cursor struct.
* Added accessors in ghostty-terminal: `Page::string_used_bytes` / `string_capacity_bytes`, `PageList::viewport_pin_handle`.
* ImGui demo window is shown when `debug_assertions` (Zig: `builtin.mode == .Debug`); the workspace release profile keeps debug assertions,
so it is also shown there.
## Known gaps
* No unit tests can drive real GTK events; clicks/scroll are only covered by the debug replay.
* Fractional scale factors are rounded by GTK's integer `scale_factor`.