fix(input): carry key and preedit text as bytes like Zig

Zig's KeyEvent.utf8 and the embedded API's key/preedit text are byte
slices passed through to key encoding and the pty; the port converted
them lossily to UTF-8.
- KeyEvent.utf8 is &[u8] end to end (ghostty-input encoder, core,
  ghostty-lib C API, libghostty-vt key events, GTK callers). Where Zig
  validates with Utf8View (alternates, modify-other-keys, ctrl CSI-u,
  associated text, binding unicode lookup) invalid text takes the same
  "no UTF-8" fallback.
- Invalid preedit UTF-8 behaves like Zig's `try Utf8View.init`: the
  prior preedit is cleared and marked dirty, the callback returns an
  error (logged by the C API) and no preedit is set.
- The inspector key widget shows invalid key text lossily, display only.

Test Plan:
- cargo test --workspace --no-fail-fast (3407 passed)
- cargo test -p ghostty-lib --features test-platform (C-API key text
  0xff 0xfe reaches the pty unchanged)
- just check-macos (both), just check-windows, just check-wasm
This commit is contained in:
ddidderr committed 2026-10-12 11:01:17 +02:00
1 parent 1327e63811
commit c8113345a5
19 files changed
+277 -144

No files matched your search

+3 -3
View File
@@ -216,7 +216,7 @@ struct OwnedKey {
mods: Mods,
consumed_mods: Mods,
composing: bool,
text: String,
text: Vec<u8>,
unshifted_codepoint: u32,
}
@@ -234,7 +234,7 @@ impl OwnedKey {
};
// SAFETY: per the function contract.
let text = unsafe { cstr(e.text) }
.map(|c| c.to_string_lossy().into_owned())
.map(|c| c.to_bytes().to_vec())
.unwrap_or_default();
// We want to get the physical unmapped key to process keybinds.
@@ -1636,7 +1636,7 @@ pub unsafe extern "C" fn ghostty_surface_preedit(
} else {
// SAFETY: `text` points to `len` readable bytes.
let bytes = unsafe { std::slice::from_raw_parts(text.cast::<u8>(), len) };
Some(String::from_utf8_lossy(bytes).into_owned())
Some(bytes.to_vec())
};
if let Err(err) = core.preedit_callback(preedit.as_deref()) {
log::error!("error in preedit callback err={err}");
+22
View File
@@ -519,6 +519,28 @@ fn selection_and_clipboard_round_trip() {
/// A child that prints the hex of the first two bytes it receives raw.
const HEX_TWO: &str = "stty raw -echo; dd bs=1 count=2 2>/dev/null | od -An -tx1 | tr -d ' \\n'; printf '\\nHEX-DONE\\n'; sleep 5";
#[test]
fn key_text_and_preedit_are_bytes_not_lossy_utf8() {
// Key text is a byte slice (Zig `[]const u8`): non-UTF-8 text reaches
// the pty unchanged.
let h = host!("");
let s = h.surface(HEX_TWO);
let raw = CString::new(&b"\xff\xfe"[..]).unwrap();
assert!(unsafe { ghostty_surface_key(s, key(1, KEY_A, 0, Some(&raw))) });
h.wait_screen(s, "fffe");
unsafe { ghostty_surface_free(s) };
// Invalid preedit bytes are rejected like Zig (logged error, no panic).
let s = h.surface("printf 'READY\\n'; sleep 30");
h.wait_screen(s, "READY");
let p = b"a\xffb";
unsafe {
ghostty_surface_preedit(s, p.as_ptr().cast(), p.len());
ghostty_surface_preedit(s, ptr::null(), 0);
ghostty_surface_free(s);
}
}
#[test]
fn clipboard_completion_and_binding_action_keep_raw_bytes() {
// Clipboard contents are bytes: 0xFF must reach the child as 0xFF.