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)
This commit is contained in:
1 parent
b192fdb046
commit
d1a5e55198
25 files changed
+6693
No files matched your search
@@ -0,0 +1,197 @@
|
||||
//! The Inspector (ZIG/src/inspector/Inspector.zig).
|
||||
|
||||
use std::sync::{Mutex, MutexGuard, PoisonError};
|
||||
|
||||
use dear_imgui_rs::{ConfigFlags, Context, FontSource, StbTrueTypeFontData, Ui};
|
||||
use ghostty_renderer::overlay::Feature;
|
||||
use ghostty_renderer::state::InspectorHook;
|
||||
use ghostty_terminal::terminal::Terminal;
|
||||
|
||||
use crate::widgets;
|
||||
|
||||
/// Setup the ImGui state. This requires an ImGui context.
|
||||
///
|
||||
/// `scale_factor` is the content scale of the display; Zig hardcodes a 2x
|
||||
/// scale here (with a TODO) and sizes the font as `16 * 2`, this uses
|
||||
/// `16 * scale_factor` instead.
|
||||
pub fn setup(ctx: &mut Context, scale_factor: f64) {
|
||||
let io = ctx.io_mut();
|
||||
|
||||
// Enable docking, which we use heavily for the UI. Our colorspace is
|
||||
// sRGB.
|
||||
io.set_config_flags(io.config_flags() | ConfigFlags::DOCKING_ENABLE | ConfigFlags::IS_SRGB);
|
||||
|
||||
// Disable the ini file to save layout
|
||||
if let Err(err) = ctx.set_ini_filename::<std::path::PathBuf>(None) {
|
||||
log::warn!("failed to disable the imgui ini file err={err}");
|
||||
}
|
||||
if let Err(err) = ctx.set_log_filename::<std::path::PathBuf>(None) {
|
||||
log::warn!("failed to disable the imgui log file err={err}");
|
||||
}
|
||||
|
||||
// Use our own embedded font
|
||||
let font_size = (16.0 * scale_factor.max(1.0)) as f32;
|
||||
match StbTrueTypeFontData::from_slice(ghostty_font::embedded::REGULAR) {
|
||||
Ok(data) => {
|
||||
ctx.font_atlas()
|
||||
.add_font(&[FontSource::stb_truetype_with_size(data, font_size)]);
|
||||
}
|
||||
Err(err) => log::warn!("failed to load the embedded inspector font err={err}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// The Inspector is a development tool to debug the terminal. This is
|
||||
/// useful for terminal application developers as well as people potentially
|
||||
/// debugging issues in Ghostty itself.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Inspector {
|
||||
/// Mouse state that we track in addition to normal mouse states that
|
||||
/// Ghostty always knows about.
|
||||
pub mouse: widgets::surface::Mouse,
|
||||
|
||||
// ImGui state
|
||||
pub gui: widgets::surface::Inspector,
|
||||
}
|
||||
|
||||
impl Inspector {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Returns the renderer info panel. This is a convenience function
|
||||
/// to access and find this state to read and modify.
|
||||
pub fn renderer_info(&mut self) -> &mut widgets::renderer::Info {
|
||||
&mut self.gui.renderer_info
|
||||
}
|
||||
|
||||
/// Record a keyboard event.
|
||||
pub fn record_key_event(&mut self, ev: widgets::key::Event) {
|
||||
self.gui.key_stream.record(ev);
|
||||
}
|
||||
|
||||
/// Record data read from the pty.
|
||||
pub fn record_pty_read(&mut self, t: &Terminal, data: &[u8]) {
|
||||
self.gui.vt_stream.record_pty_read(t, data);
|
||||
}
|
||||
|
||||
/// Render the frame. The caller must hold the terminal lock and provide
|
||||
/// the surface information.
|
||||
pub fn render(&mut self, ui: &Ui, surface: &widgets::surface::SurfaceInfo, t: &mut Terminal) {
|
||||
// Draw the UI
|
||||
self.gui.draw(ui, surface, &self.mouse, t);
|
||||
}
|
||||
}
|
||||
|
||||
/// The inspector as shared between the surface, the IO thread (VT events)
|
||||
/// and the renderer (overlay features). Zig shares a pointer guarded by the
|
||||
/// renderer mutex; the lock order is: renderer state, then this.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Shared(Mutex<Inspector>);
|
||||
|
||||
impl Shared {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Lock the inspector (poisoning is ignored: the data is display-only).
|
||||
pub fn lock(&self) -> MutexGuard<'_, Inspector> {
|
||||
self.0.lock().unwrap_or_else(PoisonError::into_inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl InspectorHook for Shared {
|
||||
fn overlay_features(&self) -> Vec<Feature> {
|
||||
self.lock().gui.renderer_info.overlay_features()
|
||||
}
|
||||
|
||||
fn record_pty_read(&self, t: &Terminal, data: &[u8]) {
|
||||
self.lock().record_pty_read(t, data);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::widgets::surface::SurfaceInfo;
|
||||
use ghostty_input::{Action, Key, KeyEvent};
|
||||
use ghostty_renderer::size::{CellSize, Padding, ScreenSize, Size};
|
||||
use ghostty_terminal::terminal::Options;
|
||||
use ghostty_vt::stream::Stream;
|
||||
|
||||
fn surface_info() -> SurfaceInfo {
|
||||
SurfaceInfo {
|
||||
size: Size {
|
||||
screen: ScreenSize {
|
||||
width: 800,
|
||||
height: 600,
|
||||
},
|
||||
cell: CellSize {
|
||||
width: 10,
|
||||
height: 20,
|
||||
},
|
||||
padding: Padding::default(),
|
||||
},
|
||||
font_size_points: 12.0,
|
||||
font_size_pixels: 16.0,
|
||||
click_pressed: [false; 12],
|
||||
left_click_pin: None,
|
||||
left_click_xpos: 0.0,
|
||||
left_click_ypos: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw a few full frames headlessly (no renderer) with every window
|
||||
/// open and the detached/pop-out state enabled, to catch Dear ImGui
|
||||
/// misuse (the safe wrapper panics on invalid API use).
|
||||
#[test]
|
||||
fn draws_frames_headless() {
|
||||
let mut ctx = Context::create();
|
||||
setup(&mut ctx, 1.0);
|
||||
ctx.io_mut().set_display_size([1000.0, 700.0]);
|
||||
ctx.io_mut().set_delta_time(1.0 / 60.0);
|
||||
ctx.font_atlas()
|
||||
.try_claim_legacy_renderer()
|
||||
.expect("legacy font atlas")
|
||||
.build();
|
||||
|
||||
let mut t = Terminal::new(Options::new(80, 24)).expect("terminal");
|
||||
let mut insp = Inspector::new();
|
||||
|
||||
// Feed some pty data through both the inspector and the terminal.
|
||||
let data =
|
||||
b"hello\x07\x1b[31mred\x1b[0m\r\n\x1b]8;;http://x\x1b\\link\x1b]8;;\x1b\\\x1b]0;t\x07";
|
||||
insp.record_pty_read(&t, data);
|
||||
{
|
||||
let mut term_stream =
|
||||
Stream::new(ghostty_terminal::stream_readonly::Handler::new(&mut t));
|
||||
term_stream.next_slice(data).expect("process");
|
||||
}
|
||||
|
||||
insp.record_key_event(crate::KeyEvent::new(&KeyEvent {
|
||||
action: Action::Press,
|
||||
key: Key::KeyA,
|
||||
utf8: "a",
|
||||
..KeyEvent::default()
|
||||
}));
|
||||
|
||||
for frame in 0..8 {
|
||||
// Open the pop-out state after the layout has settled, like a
|
||||
// user clicking the buttons.
|
||||
if frame == 3 {
|
||||
insp.gui.terminal_info.misc_header.detached = true;
|
||||
insp.gui.terminal_info.layout_header.detached = true;
|
||||
}
|
||||
if frame == 2 {
|
||||
insp.gui.vt_stream.selected_event_seq = Some(1);
|
||||
}
|
||||
if frame == 4 {
|
||||
insp.gui.terminal_info.screens[0] = Some(crate::widgets::screen::Info::default());
|
||||
insp.gui.terminal_info.show_palette = true;
|
||||
}
|
||||
let ui = ctx.frame();
|
||||
insp.render(ui, &surface_info(), &mut t);
|
||||
let _ = ctx.render_legacy();
|
||||
}
|
||||
assert!(!insp.gui.vt_stream.events().is_empty());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user