feat(vt-capi): port the libghostty-vt C API
Ports ZIG/src/lib_vt.zig and terminal/c/* as crates/ghostty-vt-capi (cdylib + staticlib + rlib, soname libghostty-vt.so) implementing the unchanged include/ghostty/vt.h + vt/*.h ABI: OSC parser, SGR parser, key events and key encoding, paste safety, colors, result codes - all 38 native symbols, plus the ghostty_wasm_* helpers on wasm32. Verification: - tests/abi.rs generates a C probe from the headers, compiled with cc, that checks sizeof/alignof/offsetof of every struct/union, every enumerator and #define against Rust, and links every declared symbol; nm -D exports equal the declared functions. - Ghostty's example/c-vt* programs compile unchanged against the Rust library (shared and static) and produce the expected output. - The wasm32 build runs Ghostty's wasm-key-encode/wasm-sgr example scripts unmodified under node (upstream's wasm-sgr tag table is stale vs sgr.h; a regenerated copy checks full output). Test Plan: - cargo test -p ghostty-vt-capi (22 unit + 2 ABI + 5 C + 3 wasm tests)
This commit is contained in:
1 parent
03e6e33686
commit
0915f7b935
35 files changed
+6911
No files matched your search
@@ -0,0 +1,346 @@
|
||||
//! Key event C API (ZIG/src/terminal/c/key_event.zig, `include/ghostty/vt/key/event.h`).
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::{ffi::c_int, ptr::NonNull};
|
||||
|
||||
use ghostty_input::{Action, Key, KeyEvent, Mods};
|
||||
|
||||
use crate::{
|
||||
alloc::{Alloc, Allocator},
|
||||
result::GhosttyResult,
|
||||
};
|
||||
|
||||
/// C: `GhosttyKeyEvent` (opaque). Mirrors Zig's `KeyEventWrapper`.
|
||||
///
|
||||
/// The UTF-8 text is not owned by the wrapper: the caller is responsible for
|
||||
/// keeping the bytes passed to `ghostty_key_event_set_utf8` alive.
|
||||
pub struct KeyEventWrapper {
|
||||
alloc: Alloc,
|
||||
action: Action,
|
||||
key: Key,
|
||||
mods: Mods,
|
||||
consumed_mods: Mods,
|
||||
composing: bool,
|
||||
utf8: *const u8,
|
||||
utf8_len: usize,
|
||||
unshifted_codepoint: u32,
|
||||
}
|
||||
|
||||
impl KeyEventWrapper {
|
||||
/// Borrow as the Rust [`KeyEvent`]. Zig's `utf8` is a byte slice, ours is
|
||||
/// `&str`: invalid UTF-8 is truncated to its longest valid prefix.
|
||||
///
|
||||
/// # Safety
|
||||
/// The caller-provided UTF-8 bytes must still be alive.
|
||||
pub(crate) unsafe fn as_event(&self) -> KeyEvent<'_> {
|
||||
let bytes: &[u8] = if self.utf8.is_null() {
|
||||
&[]
|
||||
} else {
|
||||
// SAFETY: per the contract of `ghostty_key_event_set_utf8`.
|
||||
unsafe { std::slice::from_raw_parts(self.utf8, self.utf8_len) }
|
||||
};
|
||||
let text = match std::str::from_utf8(bytes) {
|
||||
Ok(s) => s,
|
||||
Err(e) => std::str::from_utf8(&bytes[..e.valid_up_to()]).unwrap_or(""),
|
||||
};
|
||||
KeyEvent {
|
||||
action: self.action,
|
||||
key: self.key,
|
||||
mods: self.mods,
|
||||
consumed_mods: self.consumed_mods,
|
||||
composing: self.composing,
|
||||
utf8: text,
|
||||
unshifted_codepoint: self.unshifted_codepoint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// C: `GhosttyKeyAction`
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum KeyAction {
|
||||
Release = 0,
|
||||
Press = 1,
|
||||
Repeat = 2,
|
||||
}
|
||||
|
||||
fn action_from_c(v: c_int) -> Option<Action> {
|
||||
match v {
|
||||
0 => Some(Action::Release),
|
||||
1 => Some(Action::Press),
|
||||
2 => Some(Action::Repeat),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn key_from_c(v: c_int) -> Option<Key> {
|
||||
usize::try_from(v)
|
||||
.ok()
|
||||
.and_then(|i| Key::ALL.get(i).copied())
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `allocator` must be null or valid; `event` must be valid for writes.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_new(
|
||||
allocator: *const Allocator,
|
||||
event: *mut *mut KeyEventWrapper,
|
||||
) -> GhosttyResult {
|
||||
// SAFETY: forwarded caller contract.
|
||||
let alloc = unsafe { Alloc::from_c(allocator) };
|
||||
let d = KeyEvent::default();
|
||||
let Some(p) = alloc.create(KeyEventWrapper {
|
||||
alloc,
|
||||
action: d.action,
|
||||
key: d.key,
|
||||
mods: d.mods,
|
||||
consumed_mods: d.consumed_mods,
|
||||
composing: d.composing,
|
||||
utf8: std::ptr::null(),
|
||||
utf8_len: 0,
|
||||
unshifted_codepoint: d.unshifted_codepoint,
|
||||
}) else {
|
||||
return GhosttyResult::OutOfMemory;
|
||||
};
|
||||
// SAFETY: caller guarantees `event` is valid for writes.
|
||||
unsafe { *event = p.as_ptr() };
|
||||
GhosttyResult::Success
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or a live event from `ghostty_key_event_new`.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_free(event: *mut KeyEventWrapper) {
|
||||
let Some(p) = NonNull::new(event) else {
|
||||
return;
|
||||
};
|
||||
// SAFETY: `p` came from `Alloc::create` in `ghostty_key_event_new`.
|
||||
unsafe {
|
||||
let alloc = p.as_ref().alloc;
|
||||
alloc.destroy(p);
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! with_event {
|
||||
($event:expr, $e:ident => $body:expr, $default:expr) => {
|
||||
// SAFETY: caller guarantees a live event (or null).
|
||||
match unsafe { $event.as_mut() } {
|
||||
Some($e) => $body,
|
||||
None => $default,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or live.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_set_action(event: *mut KeyEventWrapper, action: c_int) {
|
||||
let Some(action) = action_from_c(action) else {
|
||||
log::warn!("set_action invalid action value={action}");
|
||||
return;
|
||||
};
|
||||
with_event!(event, e => e.action = action, ());
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or live.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_get_action(event: *mut KeyEventWrapper) -> c_int {
|
||||
with_event!(event, e => e.action as c_int, KeyAction::Press as c_int)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or live.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_set_key(event: *mut KeyEventWrapper, key: c_int) {
|
||||
let Some(key) = key_from_c(key) else {
|
||||
log::warn!("set_key invalid key value={key}");
|
||||
return;
|
||||
};
|
||||
with_event!(event, e => e.key = key, ());
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or live.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_get_key(event: *mut KeyEventWrapper) -> c_int {
|
||||
with_event!(event, e => e.key as c_int, 0)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or live.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_set_mods(event: *mut KeyEventWrapper, mods: u16) {
|
||||
with_event!(event, e => e.mods = Mods::from_int(mods), ());
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or live.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_get_mods(event: *mut KeyEventWrapper) -> u16 {
|
||||
with_event!(event, e => e.mods.int(), 0)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or live.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_set_consumed_mods(
|
||||
event: *mut KeyEventWrapper,
|
||||
consumed_mods: u16,
|
||||
) {
|
||||
with_event!(event, e => e.consumed_mods = Mods::from_int(consumed_mods), ());
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or live.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_get_consumed_mods(event: *mut KeyEventWrapper) -> u16 {
|
||||
with_event!(event, e => e.consumed_mods.int(), 0)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or live.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_set_composing(
|
||||
event: *mut KeyEventWrapper,
|
||||
composing: bool,
|
||||
) {
|
||||
with_event!(event, e => e.composing = composing, ());
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or live.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_get_composing(event: *mut KeyEventWrapper) -> bool {
|
||||
with_event!(event, e => e.composing, false)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or live; `utf8` must be null or point to `len`
|
||||
/// bytes that stay valid while the event is used.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_set_utf8(
|
||||
event: *mut KeyEventWrapper,
|
||||
utf8: *const u8,
|
||||
len: usize,
|
||||
) {
|
||||
with_event!(event, e => {
|
||||
if utf8.is_null() {
|
||||
e.utf8 = std::ptr::null();
|
||||
e.utf8_len = 0;
|
||||
} else {
|
||||
e.utf8 = utf8;
|
||||
e.utf8_len = len;
|
||||
}
|
||||
}, ());
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or live; `len`, if non-null, must be valid for writes.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_get_utf8(
|
||||
event: *mut KeyEventWrapper,
|
||||
len: *mut usize,
|
||||
) -> *const u8 {
|
||||
with_event!(event, e => {
|
||||
if !len.is_null() {
|
||||
// SAFETY: non-null and valid per the contract.
|
||||
unsafe { *len = e.utf8_len };
|
||||
}
|
||||
if e.utf8_len == 0 { std::ptr::null() } else { e.utf8 }
|
||||
}, std::ptr::null())
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or live.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_set_unshifted_codepoint(
|
||||
event: *mut KeyEventWrapper,
|
||||
codepoint: u32,
|
||||
) {
|
||||
// Zig: `@truncate` to u21.
|
||||
with_event!(event, e => e.unshifted_codepoint = codepoint & 0x1f_ffff, ());
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `event` must be null or live.
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn ghostty_key_event_get_unshifted_codepoint(
|
||||
event: *mut KeyEventWrapper,
|
||||
) -> u32 {
|
||||
with_event!(event, e => e.unshifted_codepoint, 0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn new() -> *mut KeyEventWrapper {
|
||||
let mut e = std::ptr::null_mut();
|
||||
// SAFETY: valid out pointer, default allocator.
|
||||
assert_eq!(
|
||||
unsafe { ghostty_key_event_new(std::ptr::null(), &mut e) },
|
||||
GhosttyResult::Success
|
||||
);
|
||||
e
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alloc() {
|
||||
let e = new();
|
||||
// SAFETY: live event.
|
||||
unsafe { ghostty_key_event_free(e) };
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_and_get_round_trip() {
|
||||
let e = new();
|
||||
let text = b"test";
|
||||
// SAFETY: live event; `text` outlives its use.
|
||||
unsafe {
|
||||
ghostty_key_event_set_action(e, KeyAction::Repeat as c_int);
|
||||
ghostty_key_event_set_key(e, Key::KeyZ as c_int);
|
||||
ghostty_key_event_set_mods(e, 0b1100); // alt | super
|
||||
ghostty_key_event_set_consumed_mods(e, 0b0100);
|
||||
ghostty_key_event_set_composing(e, true);
|
||||
ghostty_key_event_set_utf8(e, text.as_ptr(), text.len());
|
||||
ghostty_key_event_set_unshifted_codepoint(e, 'z' as u32);
|
||||
|
||||
assert_eq!(ghostty_key_event_get_action(e), KeyAction::Repeat as c_int);
|
||||
assert_eq!(ghostty_key_event_get_key(e), Key::KeyZ as c_int);
|
||||
assert_eq!(ghostty_key_event_get_mods(e), 0b1100);
|
||||
assert_eq!(ghostty_key_event_get_consumed_mods(e), 0b0100);
|
||||
assert!(ghostty_key_event_get_composing(e));
|
||||
let mut len = 0usize;
|
||||
let p = ghostty_key_event_get_utf8(e, &mut len);
|
||||
assert_eq!(std::slice::from_raw_parts(p, len), b"test");
|
||||
assert_eq!(ghostty_key_event_get_unshifted_codepoint(e), 'z' as u32);
|
||||
|
||||
ghostty_key_event_set_utf8(e, std::ptr::null(), 0);
|
||||
assert!(ghostty_key_event_get_utf8(e, std::ptr::null_mut()).is_null());
|
||||
ghostty_key_event_free(e);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_enum_values_are_ignored() {
|
||||
let e = new();
|
||||
// SAFETY: live event.
|
||||
unsafe {
|
||||
ghostty_key_event_set_action(e, 99);
|
||||
assert_eq!(ghostty_key_event_get_action(e), KeyAction::Press as c_int);
|
||||
ghostty_key_event_set_key(e, -1);
|
||||
ghostty_key_event_set_key(e, 100_000);
|
||||
assert_eq!(ghostty_key_event_get_key(e), 0);
|
||||
ghostty_key_event_free(e);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_numbering_matches_header() {
|
||||
assert_eq!(Key::ALL.len(), 176);
|
||||
assert_eq!(key_from_c(1), Some(Key::Backquote));
|
||||
assert_eq!(key_from_c(175), Some(Key::Paste));
|
||||
assert_eq!(key_from_c(176), None);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user