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)
259 lines
7.0 KiB
Rust
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");
|
|
}
|