Files
ghostty-rs/crates/ghostty-core/tests/e2e_keys.rs
T
ddidderr 49d4ecefae feat(lib): port libghostty, Ghostty's embedded C API
Ports apprt/embedded.zig, main_c.zig and config/CApi.zig + c_get.zig as
crates/ghostty-lib (cdylib/staticlib/rlib `libghostty`), exporting every
function of the unchanged include/ghostty.h - the ABI Ghostty's Swift
macOS/iOS app links against. The embedded runtime implements the
runtime traits over the host's C callbacks, so the real core drives it.

- ghostty-core selects the renderer per target: Metal on Apple (surface
  NSView/UIView via RtSurface::platform_view), OpenGL elsewhere.
- ghostty_config_get follows Zig's c_get rules via a CValue type (143
  supported keys, checked against the Zig field types).
- The inspector C API is complete; its Metal backend replaces ImGui's
  Metal renderer and has not run.

Verification: generated C ABI probe (sizeof/alignof/offsetof of every
struct/union, 755 enum values, every function linked, nm exports, headers
byte-identical to upstream); a C host on Linux covering init, CLI
actions, config, app, keys, focus and color scheme; an opt-in
test-platform feature creating real surfaces on Linux with headless EGL
and a pty (13 tests). Apple targets are clippy-clean but not run; the
Swift app has not been built against it.

Test Plan:
- cargo test -p ghostty-lib (and --features test-platform)
- cargo clippy --target aarch64-apple-darwin -p ghostty-lib (cross env
  in docs/rewrite/notes/macos-M1.md)
2026-10-12 06:38:33 +02:00

259 lines
7.0 KiB
Rust

//! Keyboard input end to end: key events -> core Surface -> encoder -> pty ->
//! a real `cat` child that records the exact bytes it received.
// These drive the real surface over a headless OpenGL context; the Apple targets render
// with Metal.
#![cfg(not(target_vendor = "apple"))]
#![allow(clippy::unwrap_used, clippy::expect_used)]
mod common;
use common::*;
use ghostty_input::Key;
fn rec_args(setup: &str, args: &[&str]) -> Option<Harness> {
Harness::recorder(setup, args)
}
fn rec(setup: &str) -> Option<Harness> {
Harness::recorder(setup, &[])
}
#[test]
fn printable_text_enter_backspace_tab() {
let h = harness!(rec(""));
h.type_text("Hello, World! 123");
h.key(Key::Enter, none(), "");
h.key(Key::Backspace, none(), "");
h.key(Key::Tab, none(), "");
h.key(Key::Tab, shift(), "");
h.key(Key::Escape, none(), "");
h.key(Key::Space, none(), " ");
h.expect_input(b"Hello, World! 123\r\x7f\t\x1b[Z\x1b ");
}
#[test]
fn utf8_text_is_passed_through() {
let h = harness!(rec(""));
h.key(Key::KeyE, none(), "\u{e9}");
h.key(Key::Unidentified, none(), "\u{1f600}");
h.expect_input("\u{e9}\u{1f600}".as_bytes());
}
#[test]
fn ctrl_keys() {
let h = harness!(rec(""));
h.key(Key::KeyC, ctrl(), "c");
h.key(Key::KeyD, ctrl(), "d");
h.key(Key::KeyA, ctrl(), "a");
h.key(Key::KeyZ, ctrl(), "z");
// ctrl+[ is deliberately not in Ghostty's ctrl table (kitty behaviour): CSI u.
h.key(Key::BracketLeft, ctrl(), "[");
h.key(Key::Space, ctrl(), " ");
h.expect_input(b"\x03\x04\x01\x1a\x1b[91;5u\x00");
}
#[test]
fn alt_prefix() {
let h = harness!(rec(""));
h.key(Key::KeyX, alt(), "x");
h.key(Key::KeyB, alt(), "b");
h.key(Key::Enter, alt(), "");
h.key(Key::Backspace, alt(), "");
h.expect_input(b"\x1bx\x1bb\x1b\r\x1b\x7f");
}
#[test]
fn ctrl_alt() {
let h = harness!(rec(""));
h.key(
Key::KeyC,
ghostty_input::Mods {
ctrl: true,
alt: true,
..Default::default()
},
"c",
);
h.expect_input(b"\x1b\x03");
}
#[test]
fn arrows_normal_mode() {
let h = harness!(rec(""));
h.special(Key::ArrowUp, none());
h.special(Key::ArrowDown, none());
h.special(Key::ArrowRight, none());
h.special(Key::ArrowLeft, none());
h.special(Key::Home, none());
h.special(Key::End, none());
h.special(Key::PageUp, none());
h.special(Key::PageDown, none());
h.special(Key::Insert, none());
h.special(Key::Delete, none());
h.expect_input(b"\x1b[A\x1b[B\x1b[C\x1b[D\x1b[H\x1b[F\x1b[5~\x1b[6~\x1b[2~\x1b[3~");
}
#[test]
fn arrows_decckm_application_mode() {
let h = harness!(rec(r"\033[?1h"));
h.special(Key::ArrowUp, none());
h.special(Key::ArrowDown, none());
h.special(Key::ArrowRight, none());
h.special(Key::ArrowLeft, none());
h.special(Key::Home, none());
h.special(Key::End, none());
// Modified arrows always use CSI form.
h.special(Key::ArrowUp, shift());
h.special(Key::ArrowLeft, ctrl());
h.expect_input(b"\x1bOA\x1bOB\x1bOC\x1bOD\x1bOH\x1bOF\x1b[1;2A\x1b[1;5D");
}
#[test]
fn function_keys() {
let h = harness!(rec(""));
for k in [
Key::F1,
Key::F2,
Key::F3,
Key::F4,
Key::F5,
Key::F6,
Key::F7,
Key::F8,
Key::F9,
Key::F10,
Key::F11,
Key::F12,
] {
h.special(k, none());
}
h.special(Key::F1, shift());
h.special(Key::F5, ctrl());
h.expect_input(
b"\x1bOP\x1bOQ\x1bOR\x1bOS\x1b[15~\x1b[17~\x1b[18~\x1b[19~\x1b[20~\x1b[21~\x1b[23~\x1b[24~\x1b[1;2P\x1b[15;5~",
);
}
#[test]
fn keypad_normal_and_application() {
// numlock on, normal keypad mode: plain digits
let h = harness!(rec(""));
h.key(Key::Numpad1, none(), "1");
h.key(Key::NumpadAdd, none(), "+");
h.key(Key::NumpadEnter, none(), "");
h.expect_input(b"1+\r");
// DECKPAM alone is ignored (mode 1035 ignore_keypad_with_numlock defaults on).
let h = harness!(rec(r"\033="));
h.key(Key::Numpad1, none(), "1");
h.expect_input(b"1");
// DECKPAM + mode 1035 off: application keypad sequences
let h = harness!(rec(r"\033=\033[?1035l"));
h.key(Key::Numpad1, none(), "1");
h.key(Key::NumpadAdd, none(), "+");
h.key(Key::NumpadEnter, none(), "");
h.expect_input(b"\x1bOq\x1bOk\x1bOM");
}
#[test]
fn kitty_keyboard_disambiguate() {
let h = harness!(rec(r"\033[>1u"));
h.wait("kitty flags", |h| {
h.surface
.lock_terminal()
.screen()
.kitty_keyboard
.current()
.int()
== 1
});
h.key(Key::KeyC, ctrl(), "c");
h.key(Key::Escape, none(), "");
h.key(Key::KeyX, alt(), "x");
h.key(Key::KeyA, none(), "a");
h.key(Key::Enter, none(), "");
h.key(Key::Enter, shift(), "");
h.special(Key::ArrowUp, none());
h.special(Key::F1, none());
h.expect_input(b"\x1b[99;5u\x1b[27u\x1b[120;3ua\r\x1b[13;2u\x1b[A\x1b[P");
}
#[test]
fn kitty_keyboard_report_events_sends_release() {
// flags 1|2: disambiguate + report event types
let h = harness!(rec(r"\033[>3u"));
h.wait("kitty flags", |h| {
h.surface
.lock_terminal()
.screen()
.kitty_keyboard
.current()
.int()
== 3
});
h.key(Key::KeyA, none(), "a");
h.key(Key::KeyC, ctrl(), "c");
// press "a" is plain text, release "a" is reported
h.expect_input(b"a\x1b[97;1:3u\x1b[99;5u\x1b[99;5:3u");
}
#[test]
fn kitty_keyboard_pop_restores_legacy() {
let h = harness!(rec(r"\033[>1u\033[<1u"));
h.wait("kitty popped", |h| {
h.surface
.lock_terminal()
.screen()
.kitty_keyboard
.current()
.int()
== 0
});
h.key(Key::KeyC, ctrl(), "c");
h.expect_input(b"\x03");
}
#[test]
fn modify_other_keys_state_2() {
let h = harness!(rec(r"\033[>4;2m"));
h.wait("mok2", |h| {
h.surface.lock_terminal().flags.modify_other_keys_2
});
// Plain ctrl+letter stays legacy, other ctrl combos use CSI 27.
h.key(Key::KeyA, ctrl(), "a");
h.key(Key::Semicolon, ctrl(), ";");
h.key(Key::Digit1, ctrl(), "1");
h.key(Key::KeyA, none(), "a");
h.expect_input(b"\x01\x1b[27;5;59~1a");
}
#[test]
fn no_input_while_disable_keyboard_mode() {
// KAM (CSI 2 h) disables keyboard input.
let h = harness!(rec_args(r"\033[2h", &["--vt-kam-allowed=true"]));
h.wait("kam", |h| {
h.surface
.lock_terminal()
.modes
.get(ghostty_vt::modes::Mode::DisableKeyboard)
});
h.type_text("zz");
h.settle(100);
assert_eq!(h.recorded(), Vec::<u8>::new());
}
#[test]
fn key_release_without_press_is_ignored_in_legacy() {
let h = harness!(rec(""));
// Only a release event: nothing should be sent.
h.key_event(ghostty_input::KeyEvent {
action: ghostty_input::key::Action::Release,
key: Key::KeyA,
utf8: "a",
..Default::default()
});
h.type_text("b");
h.expect_input(b"b");
}