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)
7.6 KiB
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 implementingghostty_renderer::state::InspectorHook).widgets/*.rs: 1:1 with the Zig widget files (surface.rshas the dockspace + Surface info +SurfaceInfoinput struct).keys.rs:tag_name(Zig@tagNamefromDebug),imgui_key(ZigKey.imguiKey).dock.rs: the ONLYunsafemodule (#![allow(unsafe_code)], all SAFETY-commented):DockBuilder(node create/split/dock),SetNextWindowSize,SetNextItemAllowOverlap, and a rawBegin/Endscope (begin_window) so that widgets still run whenBeginreturned false, like Zig'sBegin+defer End.
ghostty-renderer::state:State.overlay_featuresreplaced byState.inspector: Option<Arc<dyn InspectorHook>>(Ziginspector: ?*Inspector);generic.rsreadsoverlay_features()from it each frame. The trait breaks the crate cycle.ghostty-termio:Output::process_outputgoes into Zig's "slow mode" while an inspector is set (record_pty_readper byte, thenprocess_byte).ghostty-core(surface/inspector.rs):activate_inspector/deactivate_inspector(render-state pointer + renderer and IOInspector(bool)messages),render_inspector(ui)(locks terminal then inspector, fillsSurfaceInfo, wakes the renderer), key event recording inkeyboard.rs(event created after key remapping,bindingset in the consumed branch,ptycopied inencode_key, recorded +queue_renderwhen done, only if the inspector is still active), mouse recording inmouse.rs(cursor pos,queue_renderon buttons). Theinspectorbinding action already forwardedRtAction::Inspector.ghostty-apprtCoreSurface: newrender_inspector(&Ui)next toactivate/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) andredraw_inspector,Window::toggle_inspector(win.toggle-inspector, ctrl+shift+i). Unsafe only forglow::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, plusdraws_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_inputparse test extended. Skipped: none (Zig package has no tests).
Deviations
- Font size is
16 * scale_factor(Zig hardcodes16 * 2with a TODO). The imgui style is scaled by the change of the factor (aStylecannot be re-created from Rust like Zig'sImGuiStyle_ImGuiStyle). GhosttyImguiWidgetvirtual methods (setup/render) are hooks installed withImguiWidget::set_hooks(gtk-rs class-struct boilerplate avoided); the widget hierarchyInspectorWidget -> ImguiWidget -> AdwBinis real.- The inspector widget keeps an
Rc<dyn CoreSurface>and the GTK Surface destroys its inspector window indispose(Zig used a GObject weak notify to calldeactivateInspectorwhile 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 (notSend) and the inspector is shared with the IO thread. - VT events:
Parser.Action.formatfor DCS/APC is approximated (parser.Parser.Action{ .dcs_hook = ... }); the OSC metadata forcolor_operation.requests/kitty_color_protocol.listprint the Zig container type name; metadata keeps insertion order (Zig hash map order).vt_rawonly 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.