Files
ghostty-rs/crates/ghostty-inspector/src/keys.rs
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

186 lines
5.7 KiB
Rust

//! Helpers to name things like Zig's `@tagName`.
use dear_imgui_rs::Key as ImKey;
/// The `snake_case` form of the `Debug` output of a field-less enum value,
/// which is what Zig's `@tagName` returns for the equivalent enum.
pub fn tag_name(v: &dyn std::fmt::Debug) -> String {
let dbg = format!("{v:?}");
// Only take the variant name (drop payloads for data-carrying enums).
let name = dbg
.split(|c: char| !c.is_alphanumeric() && c != '_')
.next()
.unwrap_or("");
snake_case(name)
}
/// Convert `CamelCase` to `snake_case` (`SgrPixels` -> `sgr_pixels`,
/// `X10` -> `x10`).
pub fn snake_case(name: &str) -> String {
let mut out = String::with_capacity(name.len() + 4);
let mut prev_lower = false;
for ch in name.chars() {
if ch.is_uppercase() {
if prev_lower {
out.push('_');
}
out.extend(ch.to_lowercase());
prev_lower = false;
} else {
out.push(ch);
prev_lower = ch.is_lowercase();
}
}
out
}
/// Returns the Dear ImGui key for a Ghostty key (Zig: `Key.imguiKey`), or
/// `None` for keys that have no ImGui equivalent.
pub fn imgui_key(key: ghostty_input::Key) -> Option<ImKey> {
use dear_imgui_rs::Key as I;
use ghostty_input::Key as K;
Some(match key {
K::KeyA => I::A,
K::KeyB => I::B,
K::KeyC => I::C,
K::KeyD => I::D,
K::KeyE => I::E,
K::KeyF => I::F,
K::KeyG => I::G,
K::KeyH => I::H,
K::KeyI => I::I,
K::KeyJ => I::J,
K::KeyK => I::K,
K::KeyL => I::L,
K::KeyM => I::M,
K::KeyN => I::N,
K::KeyO => I::O,
K::KeyP => I::P,
K::KeyQ => I::Q,
K::KeyR => I::R,
K::KeyS => I::S,
K::KeyT => I::T,
K::KeyU => I::U,
K::KeyV => I::V,
K::KeyW => I::W,
K::KeyX => I::X,
K::KeyY => I::Y,
K::KeyZ => I::Z,
K::Digit0 => I::Key0,
K::Numpad0 => I::Keypad0,
K::Digit1 => I::Key1,
K::Numpad1 => I::Keypad1,
K::Digit2 => I::Key2,
K::Numpad2 => I::Keypad2,
K::Digit3 => I::Key3,
K::Numpad3 => I::Keypad3,
K::Digit4 => I::Key4,
K::Numpad4 => I::Keypad4,
K::Digit5 => I::Key5,
K::Numpad5 => I::Keypad5,
K::Digit6 => I::Key6,
K::Numpad6 => I::Keypad6,
K::Digit7 => I::Key7,
K::Numpad7 => I::Keypad7,
K::Digit8 => I::Key8,
K::Numpad8 => I::Keypad8,
K::Digit9 => I::Key9,
K::Numpad9 => I::Keypad9,
K::F1 => I::F1,
K::F2 => I::F2,
K::F3 => I::F3,
K::F4 => I::F4,
K::F5 => I::F5,
K::F6 => I::F6,
K::F7 => I::F7,
K::F8 => I::F8,
K::F9 => I::F9,
K::F10 => I::F10,
K::F11 => I::F11,
K::F12 => I::F12,
K::Semicolon => I::Semicolon,
K::Space => I::Space,
K::Quote => I::Apostrophe,
K::Comma => I::Comma,
K::Backquote => I::GraveAccent,
K::Period => I::Period,
K::Slash => I::Slash,
K::Minus => I::Minus,
K::Equal => I::Equal,
K::BracketLeft => I::LeftBracket,
K::BracketRight => I::RightBracket,
K::Backslash => I::Backslash,
K::ArrowUp => I::UpArrow,
K::ArrowDown => I::DownArrow,
K::ArrowLeft => I::LeftArrow,
K::ArrowRight => I::RightArrow,
K::Home => I::Home,
K::End => I::End,
K::Insert => I::Insert,
K::Delete => I::Delete,
K::CapsLock => I::CapsLock,
K::ScrollLock => I::ScrollLock,
K::NumLock => I::NumLock,
K::PageUp => I::PageUp,
K::PageDown => I::PageDown,
K::Escape => I::Escape,
K::Enter => I::Enter,
K::Tab => I::Tab,
K::Backspace => I::Backspace,
K::PrintScreen => I::PrintScreen,
K::Pause => I::Pause,
K::ContextMenu => I::Menu,
K::NumpadDecimal => I::KeypadDecimal,
K::NumpadDivide => I::KeypadDivide,
K::NumpadMultiply => I::KeypadMultiply,
K::NumpadSubtract => I::KeypadSubtract,
K::NumpadAdd => I::KeypadAdd,
K::NumpadEnter => I::KeypadEnter,
K::NumpadEqual => I::KeypadEqual,
K::NumpadLeft => I::LeftArrow,
K::NumpadRight => I::RightArrow,
K::NumpadUp => I::UpArrow,
K::NumpadDown => I::DownArrow,
K::NumpadPageUp => I::PageUp,
K::NumpadPageDown => I::PageUp,
K::NumpadHome => I::Home,
K::NumpadEnd => I::End,
K::NumpadInsert => I::Insert,
K::NumpadDelete => I::Delete,
K::NumpadBegin => I::Tab,
K::ShiftLeft => I::LeftShift,
K::ControlLeft => I::LeftCtrl,
K::AltLeft => I::LeftAlt,
K::MetaLeft => I::LeftSuper,
K::ShiftRight => I::RightShift,
K::ControlRight => I::RightCtrl,
K::AltRight => I::RightAlt,
K::MetaRight => I::RightSuper,
_ => return None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snake_case_names() {
assert_eq!(snake_case("SgrPixels"), "sgr_pixels");
assert_eq!(snake_case("X10"), "x10");
assert_eq!(snake_case("Utf8"), "utf8");
assert_eq!(snake_case("Active"), "active");
assert_eq!(tag_name(&Some(3)), "some");
}
#[test]
fn imgui_key_mapping() {
use ghostty_input::Key;
assert_eq!(imgui_key(Key::KeyA), Some(ImKey::A));
assert_eq!(imgui_key(Key::Digit5), Some(ImKey::Key5));
assert_eq!(imgui_key(Key::ArrowUp), Some(ImKey::UpArrow));
assert_eq!(imgui_key(Key::F13), None);
assert_eq!(imgui_key(Key::NumpadPageDown), Some(ImKey::PageUp));
}
}