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:
ddidderr committed 2026-10-12 05:16:41 +02:00
1 parent 03e6e33686
commit 0915f7b935
35 files changed
+6911

No files matched your search

+135
View File
@@ -0,0 +1,135 @@
//! The C allocator interface (ZIG/src/lib/allocator.zig, `include/ghostty/vt/allocator.h`).
//!
//! Wrapper objects handed out through the C API (parsers, events, encoders)
//! and the SGR parameter copy are allocated with the caller's allocator when
//! one is given and with the Rust global allocator otherwise (Zig: libc
//! `c_allocator` / `wasm_allocator`). Interior Rust collections (e.g. the OSC
//! parser's buffer) always use the global allocator.
#![allow(unsafe_code)]
use std::{alloc::Layout, ffi::c_void, ptr::NonNull};
/// C: `GhosttyAllocatorVtable`
#[repr(C)]
pub struct AllocatorVtable {
pub alloc: unsafe extern "C" fn(
ctx: *mut c_void,
len: usize,
alignment: u8,
ret_addr: usize,
) -> *mut c_void,
pub resize: unsafe extern "C" fn(
ctx: *mut c_void,
memory: *mut c_void,
memory_len: usize,
alignment: u8,
new_len: usize,
ret_addr: usize,
) -> bool,
pub remap: unsafe extern "C" fn(
ctx: *mut c_void,
memory: *mut c_void,
memory_len: usize,
alignment: u8,
new_len: usize,
ret_addr: usize,
) -> *mut c_void,
pub free: unsafe extern "C" fn(
ctx: *mut c_void,
memory: *mut c_void,
memory_len: usize,
alignment: u8,
ret_addr: usize,
),
}
/// C: `GhosttyAllocator`
#[derive(Clone, Copy)]
#[repr(C)]
pub struct Allocator {
pub ctx: *mut c_void,
pub vtable: *const AllocatorVtable,
}
/// A possibly-absent C allocator (`None` = default allocator). Stored inside
/// every object so `free` releases memory with the allocator that created it.
#[derive(Clone, Copy)]
pub struct Alloc(Option<Allocator>);
impl Alloc {
/// From the `const GhosttyAllocator *` parameter of a `*_new` function.
///
/// # Safety
/// `ptr` must be null or point to a valid [`Allocator`] with a valid vtable.
pub unsafe fn from_c(ptr: *const Allocator) -> Alloc {
// SAFETY: caller guarantees validity when non-null; we copy the struct
// so the original need not outlive this call (the vtable/ctx must).
Alloc(unsafe { ptr.as_ref() }.copied())
}
/// Allocate `layout`; null on failure. Zero-sized layouts return a dangling
/// aligned pointer without calling the allocator.
pub fn alloc_raw(self, layout: Layout) -> *mut u8 {
if layout.size() == 0 {
return layout.align() as *mut u8;
}
match self.0 {
// SAFETY: non-zero size layout.
None => unsafe { std::alloc::alloc(layout) },
// SAFETY: `from_c` contract: vtable is valid.
Some(a) => unsafe {
((*a.vtable).alloc)(
a.ctx,
layout.size(),
layout.align().trailing_zeros() as u8,
0,
)
.cast()
},
}
}
/// Free memory returned by [`Alloc::alloc_raw`] with the same layout.
///
/// # Safety
/// `ptr` must have been allocated by this allocator with `layout`.
pub unsafe fn free_raw(self, ptr: *mut u8, layout: Layout) {
if layout.size() == 0 {
return;
}
match self.0 {
// SAFETY: per the function contract.
None => unsafe { std::alloc::dealloc(ptr, layout) },
// SAFETY: per the function contract and `from_c`.
Some(a) => unsafe {
((*a.vtable).free)(
a.ctx,
ptr.cast(),
layout.size(),
layout.align().trailing_zeros() as u8,
0,
);
},
}
}
/// Move `value` into freshly allocated memory (Zig `alloc.create`).
pub fn create<T>(self, value: T) -> Option<NonNull<T>> {
let p = NonNull::new(self.alloc_raw(Layout::new::<T>()))?.cast::<T>();
// SAFETY: freshly allocated, correctly sized and aligned.
unsafe { p.as_ptr().write(value) };
Some(p)
}
/// Drop and free an object made by [`Alloc::create`] (Zig `alloc.destroy`).
///
/// # Safety
/// `p` must come from `create::<T>` on this allocator and not be used again.
pub unsafe fn destroy<T>(self, p: NonNull<T>) {
// SAFETY: per the function contract.
unsafe {
std::ptr::drop_in_place(p.as_ptr());
self.free_raw(p.as_ptr().cast(), Layout::new::<T>());
}
}
}
+56
View File
@@ -0,0 +1,56 @@
//! `ghostty_color_rgb_get` (ZIG/src/terminal/c/color.zig).
#![allow(unsafe_code)]
/// C: `GhosttyColorRgb`
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(C)]
pub struct GhosttyColorRgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl From<ghostty_vt::color::Rgb> for GhosttyColorRgb {
fn from(c: ghostty_vt::color::Rgb) -> Self {
Self {
r: c.r,
g: c.g,
b: c.b,
}
}
}
/// Extract the RGB components of a color.
///
/// # Safety
/// `r`, `g` and `b` must be valid for writing a `u8` (Zig: non-optional
/// pointers).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_color_rgb_get(
color: GhosttyColorRgb,
r: *mut u8,
g: *mut u8,
b: *mut u8,
) {
// SAFETY: the caller guarantees all three pointers are valid for writes.
unsafe {
*r = color.r;
*g = color.g;
*b = color.b;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rgb_get() {
let (mut r, mut g, mut b) = (0u8, 0u8, 0u8);
// SAFETY: pointers to live locals.
unsafe {
ghostty_color_rgb_get(GhosttyColorRgb { r: 1, g: 2, b: 3 }, &mut r, &mut g, &mut b);
}
assert_eq!((r, g, b), (1, 2, 3));
}
}
+303
View File
@@ -0,0 +1,303 @@
//! Key encoder C API (ZIG/src/terminal/c/key_encode.zig, `include/ghostty/vt/key/encoder.h`).
#![allow(unsafe_code)]
use std::{
ffi::{c_int, c_void},
io::{self, Write},
ptr::NonNull,
};
use ghostty_input::{
OptionAsAlt,
key_encode::{Options, encode},
};
use ghostty_vt::kitty::key::Flags as KittyFlags;
use crate::{
alloc::{Alloc, Allocator},
key_event::KeyEventWrapper,
result::GhosttyResult,
};
/// C: `GhosttyKeyEncoder` (opaque). Mirrors Zig's `KeyEncoderWrapper`.
pub struct KeyEncoderWrapper {
alloc: Alloc,
opts: Options,
}
/// C: `GhosttyKeyEncoderOption`
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum KeyEncoderOption {
CursorKeyApplication = 0,
KeypadKeyApplication = 1,
IgnoreKeypadWithNumlock = 2,
AltEscPrefix = 3,
ModifyOtherKeysState2 = 4,
KittyFlags = 5,
MacosOptionAsAlt = 6,
}
impl KeyEncoderOption {
fn from_c(v: c_int) -> Option<Self> {
Some(match v {
0 => Self::CursorKeyApplication,
1 => Self::KeypadKeyApplication,
2 => Self::IgnoreKeypadWithNumlock,
3 => Self::AltEscPrefix,
4 => Self::ModifyOtherKeysState2,
5 => Self::KittyFlags,
6 => Self::MacosOptionAsAlt,
_ => return None,
})
}
}
/// C: `GhosttyOptionAsAlt` (values of [`OptionAsAlt`]).
fn option_as_alt_from_c(v: c_int) -> Option<OptionAsAlt> {
Some(match v {
0 => OptionAsAlt::False,
1 => OptionAsAlt::True,
2 => OptionAsAlt::Left,
3 => OptionAsAlt::Right,
_ => return None,
})
}
/// # Safety
/// `allocator` must be null or valid; `encoder` must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_encoder_new(
allocator: *const Allocator,
encoder: *mut *mut KeyEncoderWrapper,
) -> GhosttyResult {
// SAFETY: forwarded caller contract.
let alloc = unsafe { Alloc::from_c(allocator) };
let Some(p) = alloc.create(KeyEncoderWrapper {
alloc,
opts: Options::default(),
}) else {
return GhosttyResult::OutOfMemory;
};
// SAFETY: caller guarantees `encoder` is valid for writes.
unsafe { *encoder = p.as_ptr() };
GhosttyResult::Success
}
/// # Safety
/// `encoder` must be null or a live encoder from `ghostty_key_encoder_new`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_encoder_free(encoder: *mut KeyEncoderWrapper) {
let Some(p) = NonNull::new(encoder) else {
return;
};
// SAFETY: `p` came from `Alloc::create` in `ghostty_key_encoder_new`.
unsafe {
let alloc = p.as_ref().alloc;
alloc.destroy(p);
}
}
/// # Safety
/// `encoder` must be null or live; `value` must be null or point to the type
/// documented for `option` (`bool`, `uint8_t` or `GhosttyOptionAsAlt`).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_encoder_setopt(
encoder: *mut KeyEncoderWrapper,
option: c_int,
value: *const c_void,
) {
let Some(option) = KeyEncoderOption::from_c(option) else {
log::warn!("setopt invalid option value={option}");
return;
};
if value.is_null() {
return;
}
// SAFETY: caller guarantees a live encoder (or null).
let Some(enc) = (unsafe { encoder.as_mut() }) else {
return;
};
let opts = &mut enc.opts;
// SAFETY (all reads): `value` is non-null and points to the option's type.
unsafe {
let flag = || value.cast::<u8>().read_unaligned() != 0;
match option {
KeyEncoderOption::CursorKeyApplication => opts.cursor_key_application = flag(),
KeyEncoderOption::KeypadKeyApplication => opts.keypad_key_application = flag(),
KeyEncoderOption::IgnoreKeypadWithNumlock => {
opts.ignore_keypad_with_numlock = flag();
}
KeyEncoderOption::AltEscPrefix => opts.alt_esc_prefix = flag(),
KeyEncoderOption::ModifyOtherKeysState2 => opts.modify_other_keys_state_2 = flag(),
KeyEncoderOption::KittyFlags => {
// Zig: truncate to the 5 flag bits.
opts.kitty_flags = KittyFlags::from_int(value.cast::<u8>().read_unaligned() & 0x1f);
}
KeyEncoderOption::MacosOptionAsAlt => {
let raw = value.cast::<c_int>().read_unaligned();
match option_as_alt_from_c(raw) {
Some(v) => opts.macos_option_as_alt = v,
None => log::warn!("setopt invalid OptionAsAlt value={raw}"),
}
}
}
}
}
/// Counts bytes without storing them (Zig `Writer.Discarding`).
struct Counter(usize);
impl Write for Counter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0 += buf.len();
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
/// # Safety
/// `encoder`/`event` must be live; `out_buf` must be null or point to
/// `out_buf_size` writable bytes; `out_len` must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_encoder_encode(
encoder: *mut KeyEncoderWrapper,
event: *mut KeyEventWrapper,
out_buf: *mut u8,
out_buf_size: usize,
out_len: *mut usize,
) -> GhosttyResult {
// SAFETY: caller guarantees live handles.
let (Some(enc), Some(ev)) = (unsafe { encoder.as_ref() }, unsafe { event.as_ref() }) else {
return GhosttyResult::InvalidValue;
};
// SAFETY: the event's UTF-8 bytes must be alive per `set_utf8`'s contract.
let ev = unsafe { ev.as_event() };
let buf: &mut [u8] = if out_buf.is_null() {
&mut []
} else {
// SAFETY: caller guarantees `out_buf_size` writable bytes.
unsafe { std::slice::from_raw_parts_mut(out_buf, out_buf_size) }
};
let capacity = buf.len();
let mut cursor: &mut [u8] = buf;
let result = encode(&mut cursor, &ev, &enc.opts);
let written = capacity - cursor.len();
let (code, n) = if result.is_ok() {
(GhosttyResult::Success, written)
} else {
// Not enough space: count how much we would have needed.
let mut counter = Counter(0);
let _ = encode(&mut counter, &ev, &enc.opts);
(GhosttyResult::OutOfMemory, counter.0)
};
if !out_len.is_null() {
// SAFETY: non-null and valid per the contract.
unsafe { *out_len = n };
}
code
}
#[cfg(test)]
mod tests {
use ghostty_input::Key;
use super::*;
use crate::key_event::{
ghostty_key_event_free,
ghostty_key_event_new,
ghostty_key_event_set_action,
ghostty_key_event_set_key,
ghostty_key_event_set_mods,
};
fn new() -> *mut KeyEncoderWrapper {
let mut e = std::ptr::null_mut();
// SAFETY: valid out pointer, default allocator.
assert_eq!(
unsafe { ghostty_key_encoder_new(std::ptr::null(), &mut e) },
GhosttyResult::Success
);
e
}
#[test]
fn setopt_bool_kitty_and_option_as_alt() {
let e = new();
// SAFETY: live encoder; values point to the right types.
unsafe {
let t = true;
ghostty_key_encoder_setopt(e, 0, (&raw const t).cast());
assert!((*e).opts.cursor_key_application);
let f = false;
ghostty_key_encoder_setopt(e, 0, (&raw const f).cast());
assert!(!(*e).opts.cursor_key_application);
ghostty_key_encoder_setopt(e, 1, (&raw const t).cast());
assert!((*e).opts.keypad_key_application);
let flags = 0b0000_0011u8;
ghostty_key_encoder_setopt(e, 5, (&raw const flags).cast());
assert!((*e).opts.kitty_flags.disambiguate && (*e).opts.kitty_flags.report_events);
assert!(!(*e).opts.kitty_flags.report_alternates);
let left: c_int = 2;
ghostty_key_encoder_setopt(e, 6, (&raw const left).cast());
assert_eq!((*e).opts.macos_option_as_alt, OptionAsAlt::Left);
let bad: c_int = 77;
ghostty_key_encoder_setopt(e, 6, (&raw const bad).cast());
assert_eq!((*e).opts.macos_option_as_alt, OptionAsAlt::Left);
// Invalid option / null value are ignored.
ghostty_key_encoder_setopt(e, 99, (&raw const t).cast());
ghostty_key_encoder_setopt(e, 0, std::ptr::null());
ghostty_key_encoder_free(e);
}
}
#[test]
fn encode_kitty_ctrl_release_with_ctrl_mod_set() {
let enc = new();
let mut ev = std::ptr::null_mut();
// SAFETY: live handles, valid buffers.
unsafe {
let all = 0x1fu8;
ghostty_key_encoder_setopt(enc, 5, (&raw const all).cast());
assert_eq!(
ghostty_key_event_new(std::ptr::null(), &mut ev),
GhosttyResult::Success
);
ghostty_key_event_set_action(ev, 0);
ghostty_key_event_set_key(ev, Key::ControlLeft as c_int);
ghostty_key_event_set_mods(ev, 0b10);
let mut required = 0usize;
assert_eq!(
ghostty_key_encoder_encode(enc, ev, std::ptr::null_mut(), 0, &mut required),
GhosttyResult::OutOfMemory
);
let mut buf = [0u8; 128];
let mut written = 0usize;
assert_eq!(
ghostty_key_encoder_encode(enc, ev, buf.as_mut_ptr(), buf.len(), &mut written),
GhosttyResult::Success
);
assert_eq!(required, written);
assert_eq!(&buf[..written], b"\x1b[57442;5:3u");
// Exactly-too-small buffer reports the needed size.
let mut small = [0u8; 3];
assert_eq!(
ghostty_key_encoder_encode(enc, ev, small.as_mut_ptr(), small.len(), &mut written),
GhosttyResult::OutOfMemory
);
assert_eq!(written, required);
ghostty_key_event_free(ev);
ghostty_key_encoder_free(enc);
}
}
}
+346
View File
@@ -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);
}
}
+26
View File
@@ -0,0 +1,26 @@
//! C ABI of libghostty-vt (ZIG/src/lib_vt.zig, ZIG/src/terminal/c/*.zig).
//!
//! The exported symbols and struct/enum layouts match
//! `include/ghostty/vt.h` (copied unchanged from upstream) exactly. Each
//! Zig file under `terminal/c/` maps to a module of the same name; the
//! Zig `@export` list in `lib_vt.zig` is expressed here as
//! `#[unsafe(no_mangle)]` on each `extern "C"` function.
//!
//! Wasm: on `wasm32` the allocation helpers (`ghostty_wasm_alloc_*`,
//! `ghostty_wasm_free_*`, `ghostty_wasm_{alloc,free}_sgr_attribute`) are
//! exported as well; on other targets they exist as ordinary Rust functions
//! but are not exported (like upstream).
#![cfg_attr(test, allow(clippy::unwrap_used))]
pub mod alloc;
pub mod color;
pub mod key_encode;
pub mod key_event;
pub mod osc;
pub mod paste;
pub mod result;
pub mod sgr;
pub mod wasm;
pub use result::GhosttyResult;
+241
View File
@@ -0,0 +1,241 @@
//! OSC parser C API (ZIG/src/terminal/c/osc.zig, `include/ghostty/vt/osc.h`).
#![allow(unsafe_code)]
use std::{
ffi::{c_int, c_void},
ptr::NonNull,
};
use ghostty_vt::osc::{Command, Parser};
use crate::{
alloc::{Alloc, Allocator},
result::GhosttyResult,
};
/// C: `GhosttyOscParser` (opaque).
pub struct OscParser {
alloc: Alloc,
parser: Parser,
/// Backing store for the last command returned by `ghostty_osc_end`.
command: OscCommand,
}
/// C: `GhosttyOscCommand` (opaque).
///
/// Zig hands out a pointer to the parser's own command; the Rust `Command`
/// is returned by value, so it is parked here (with a NUL-terminated copy of
/// string payloads, as Zig's sentinel slices are) and stays valid until the
/// next `ghostty_osc_end`/`ghostty_osc_reset`/`ghostty_osc_free`.
pub struct OscCommand {
command: Command,
/// NUL-terminated window title (only for `ChangeWindowTitle`).
title_z: Vec<u8>,
}
/// C: `GhosttyOscCommandData`
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum OscCommandData {
Invalid = 0,
ChangeWindowTitleStr = 1,
}
impl OscCommandData {
fn from_c(v: c_int) -> Option<Self> {
match v {
0 => Some(Self::Invalid),
1 => Some(Self::ChangeWindowTitleStr),
_ => None,
}
}
}
/// # Safety
/// `allocator` must be null or valid; `result` must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_osc_new(
allocator: *const Allocator,
result: *mut *mut OscParser,
) -> GhosttyResult {
// SAFETY: forwarded caller contract.
let alloc = unsafe { Alloc::from_c(allocator) };
// C-built parsers have an allocator (Zig: `.init(alloc)`), so commands
// that need unbounded buffers (OSC 52, ...) are accepted.
let Some(p) = alloc.create(OscParser {
alloc,
parser: Parser::init(true),
command: OscCommand {
command: Command::Invalid,
title_z: Vec::new(),
},
}) else {
return GhosttyResult::OutOfMemory;
};
// SAFETY: caller guarantees `result` is valid for writes.
unsafe { *result = p.as_ptr() };
GhosttyResult::Success
}
/// # Safety
/// `parser` must be null or a live parser from `ghostty_osc_new`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_osc_free(parser: *mut OscParser) {
let Some(p) = NonNull::new(parser) else {
return;
};
// SAFETY: `p` came from `Alloc::create` in `ghostty_osc_new`.
unsafe {
let alloc = p.as_ref().alloc;
alloc.destroy(p);
}
}
/// # Safety
/// `parser` must be null or a live parser.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_osc_reset(parser: *mut OscParser) {
// SAFETY: caller guarantees a live parser (or null).
if let Some(p) = unsafe { parser.as_mut() } {
p.parser.reset();
}
}
/// # Safety
/// `parser` must be null or a live parser.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_osc_next(parser: *mut OscParser, byte: u8) {
// SAFETY: caller guarantees a live parser (or null).
if let Some(p) = unsafe { parser.as_mut() } {
p.parser.next(byte);
}
}
/// # Safety
/// `parser` must be null or a live parser. The returned command is owned by
/// the parser.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_osc_end(
parser: *mut OscParser,
terminator: u8,
) -> *mut OscCommand {
// SAFETY: caller guarantees a live parser (or null).
let Some(p) = (unsafe { parser.as_mut() }) else {
return std::ptr::null_mut();
};
let cmd = p.parser.end(Some(terminator)).unwrap_or(Command::Invalid);
p.command.title_z.clear();
if let Command::ChangeWindowTitle(t) = &cmd {
p.command.title_z.extend_from_slice(t);
p.command.title_z.push(0);
}
p.command.command = cmd;
&raw mut p.command
}
/// Returns the `GhosttyOscCommandType` (an `int` enum).
///
/// # Safety
/// `command` must be null or a command returned by `ghostty_osc_end`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_osc_command_type(command: *mut OscCommand) -> c_int {
// SAFETY: caller guarantees validity (or null).
match unsafe { command.as_ref() } {
Some(c) => c.command.key() as c_int,
None => 0, // invalid
}
}
/// # Safety
/// `command` must be null or valid; `out` must point to storage of the type
/// documented for `data` (for the title: a `const char *`).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_osc_command_data(
command: *mut OscCommand,
data: c_int,
out: *mut c_void,
) -> bool {
let Some(data) = OscCommandData::from_c(data) else {
log::warn!("commandData invalid data value={data}");
return false;
};
// SAFETY: caller guarantees validity (or null).
let Some(command) = (unsafe { command.as_ref() }) else {
return false;
};
match data {
OscCommandData::Invalid => false,
OscCommandData::ChangeWindowTitleStr => match &command.command {
Command::ChangeWindowTitle(_) if !out.is_null() => {
// SAFETY: caller guarantees `out` is a `const char **`.
unsafe {
out.cast::<*const u8>()
.write_unaligned(command.title_z.as_ptr());
}
true
}
_ => false,
},
}
}
#[cfg(test)]
mod tests {
use std::ffi::CStr;
use super::*;
fn new() -> *mut OscParser {
let mut p = std::ptr::null_mut();
// SAFETY: valid out pointer, default allocator.
assert_eq!(
unsafe { ghostty_osc_new(std::ptr::null(), &mut p) },
GhosttyResult::Success
);
p
}
#[test]
fn alloc() {
let p = new();
// SAFETY: `p` is live.
unsafe { ghostty_osc_free(p) };
}
#[test]
fn command_type_null() {
// SAFETY: null is allowed.
assert_eq!(unsafe { ghostty_osc_command_type(std::ptr::null_mut()) }, 0);
}
#[test]
fn change_window_title() {
let p = new();
// SAFETY: `p` is live; `title` is a valid out slot.
unsafe {
ghostty_osc_next(p, b'0');
ghostty_osc_next(p, b';');
ghostty_osc_next(p, b'a');
let cmd = ghostty_osc_end(p, 0);
assert_eq!(ghostty_osc_command_type(cmd), 1);
let mut title: *const u8 = std::ptr::null();
assert!(ghostty_osc_command_data(cmd, 1, (&raw mut title).cast()));
assert_eq!(CStr::from_ptr(title.cast()).to_str().unwrap(), "a");
assert!(!ghostty_osc_command_data(cmd, 0, std::ptr::null_mut()));
assert!(!ghostty_osc_command_data(cmd, 99, std::ptr::null_mut()));
ghostty_osc_free(p);
}
}
#[test]
fn invalid_command_is_type_zero() {
let p = new();
// SAFETY: `p` is live.
unsafe {
ghostty_osc_next(p, b'x');
let cmd = ghostty_osc_end(p, 0);
assert_eq!(ghostty_osc_command_type(cmd), 0);
ghostty_osc_free(p);
}
}
}
+53
View File
@@ -0,0 +1,53 @@
//! `ghostty_paste_is_safe` (ZIG/src/terminal/c/paste.zig).
#![allow(unsafe_code)]
/// Whether pasting `data` is safe (no newline, no bracketed-paste end).
///
/// # Safety
/// `data` must be null or point to `len` readable bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_paste_is_safe(data: *const u8, len: usize) -> bool {
let slice: &[u8] = if data.is_null() {
&[]
} else {
// SAFETY: caller guarantees `data` points to `len` readable bytes.
unsafe { std::slice::from_raw_parts(data, len) }
};
ghostty_input::paste::is_safe(slice)
}
#[cfg(test)]
mod tests {
use super::*;
fn safe(s: &[u8]) -> bool {
// SAFETY: valid slice.
unsafe { ghostty_paste_is_safe(s.as_ptr(), s.len()) }
}
#[test]
fn is_safe_with_safe_data() {
assert!(safe(b"hello world"));
}
#[test]
fn is_safe_with_newline() {
assert!(!safe(b"hello\nworld"));
}
#[test]
fn is_safe_with_bracketed_paste_end() {
assert!(!safe(b"hello\x1b[201~world"));
}
#[test]
fn is_safe_with_empty_data() {
assert!(safe(b""));
}
#[test]
fn is_safe_with_null_empty_data() {
// SAFETY: null with len 0 is explicitly allowed.
assert!(unsafe { ghostty_paste_is_safe(std::ptr::null(), 0) });
}
}
+10
View File
@@ -0,0 +1,10 @@
//! `GhosttyResult` (ZIG/src/terminal/c/result.zig).
/// C: `GhosttyResult`
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum GhosttyResult {
Success = 0,
OutOfMemory = -1,
InvalidValue = -2,
}
+544
View File
@@ -0,0 +1,544 @@
//! SGR parser C API (ZIG/src/terminal/c/sgr.zig, `include/ghostty/vt/sgr.h`).
#![allow(unsafe_code)]
use std::{alloc::Layout, ffi::c_int, ptr::NonNull};
use ghostty_vt::{
parser::{MAX_PARAMS, SepList},
sgr::{Attribute, Parser},
};
use crate::{
alloc::{Alloc, Allocator},
color::GhosttyColorRgb,
result::GhosttyResult,
};
/// C: `GhosttySgrParser` (opaque). Mirrors Zig's `ParserWrapper`: the SGR
/// parser state plus the allocator that owns the parameter copy.
pub struct SgrParser {
alloc: Alloc,
/// Owned copy of the parameters (allocated with `alloc`), or null.
params: *mut u16,
params_len: usize,
params_sep: SepList,
idx: usize,
}
impl SgrParser {
fn params_layout(len: usize) -> Layout {
Layout::array::<u16>(len).unwrap_or(Layout::new::<u16>())
}
fn free_params(&mut self) {
if !self.params.is_null() {
// SAFETY: allocated by `set_params` with this allocator/layout.
unsafe {
self.alloc
.free_raw(self.params.cast(), Self::params_layout(self.params_len));
}
self.params = std::ptr::null_mut();
self.params_len = 0;
}
}
fn params(&self) -> &[u16] {
if self.params.is_null() {
&[]
} else {
// SAFETY: `params` points to `params_len` initialized u16s that
// live until `free_params`.
unsafe { std::slice::from_raw_parts(self.params, self.params_len) }
}
}
}
/// C: `GhosttySgrAttributeTag`
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum SgrAttributeTag {
Unset = 0,
Unknown = 1,
Bold = 2,
ResetBold = 3,
Italic = 4,
ResetItalic = 5,
Faint = 6,
Underline = 7,
UnderlineColor = 8,
UnderlineColor256 = 9,
ResetUnderlineColor = 10,
Overline = 11,
ResetOverline = 12,
Blink = 13,
ResetBlink = 14,
Inverse = 15,
ResetInverse = 16,
Invisible = 17,
ResetInvisible = 18,
Strikethrough = 19,
ResetStrikethrough = 20,
DirectColorFg = 21,
DirectColorBg = 22,
Bg8 = 23,
Fg8 = 24,
ResetFg = 25,
ResetBg = 26,
BrightBg8 = 27,
BrightFg8 = 28,
Bg256 = 29,
Fg256 = 30,
}
/// C: `GhosttySgrUnknown`
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct SgrUnknown {
pub full_ptr: *const u16,
pub full_len: usize,
pub partial_ptr: *const u16,
pub partial_len: usize,
}
/// C: `GhosttySgrAttributeValue`
#[derive(Clone, Copy)]
#[repr(C)]
pub union SgrAttributeValue {
pub unknown: SgrUnknown,
/// A `GhosttySgrUnderline` (`int`).
pub underline: c_int,
pub underline_color: GhosttyColorRgb,
pub underline_color_256: u8,
pub direct_color_fg: GhosttyColorRgb,
pub direct_color_bg: GhosttyColorRgb,
pub bg_8: u8,
pub fg_8: u8,
pub bright_bg_8: u8,
pub bright_fg_8: u8,
pub bg_256: u8,
pub fg_256: u8,
pub _padding: [u64; 8],
}
/// C: `GhosttySgrAttribute`
#[derive(Clone, Copy)]
#[repr(C)]
pub struct SgrAttribute {
/// A [`SgrAttributeTag`].
pub tag: c_int,
pub value: SgrAttributeValue,
}
impl SgrAttribute {
fn from_attr(attr: &Attribute<'_>) -> SgrAttribute {
use SgrAttributeTag as T;
let mut value = SgrAttributeValue { _padding: [0; 8] };
let tag = match *attr {
Attribute::Unset => T::Unset,
Attribute::Unknown(u) => {
value.unknown = SgrUnknown {
full_ptr: u.full.as_ptr(),
full_len: u.full.len(),
partial_ptr: u.partial.as_ptr(),
partial_len: u.partial.len(),
};
T::Unknown
}
Attribute::Bold => T::Bold,
Attribute::ResetBold => T::ResetBold,
Attribute::Italic => T::Italic,
Attribute::ResetItalic => T::ResetItalic,
Attribute::Faint => T::Faint,
Attribute::Underline(u) => {
value.underline = c_int::from(u as u8);
T::Underline
}
Attribute::UnderlineColor(c) => {
value.underline_color = c.into();
T::UnderlineColor
}
Attribute::UnderlineColor256(v) => {
value.underline_color_256 = v;
T::UnderlineColor256
}
Attribute::ResetUnderlineColor => T::ResetUnderlineColor,
Attribute::Overline => T::Overline,
Attribute::ResetOverline => T::ResetOverline,
Attribute::Blink => T::Blink,
Attribute::ResetBlink => T::ResetBlink,
Attribute::Inverse => T::Inverse,
Attribute::ResetInverse => T::ResetInverse,
Attribute::Invisible => T::Invisible,
Attribute::ResetInvisible => T::ResetInvisible,
Attribute::Strikethrough => T::Strikethrough,
Attribute::ResetStrikethrough => T::ResetStrikethrough,
Attribute::DirectColorFg(c) => {
value.direct_color_fg = c.into();
T::DirectColorFg
}
Attribute::DirectColorBg(c) => {
value.direct_color_bg = c.into();
T::DirectColorBg
}
Attribute::Bg8(n) => {
value.bg_8 = n.0;
T::Bg8
}
Attribute::Fg8(n) => {
value.fg_8 = n.0;
T::Fg8
}
Attribute::ResetFg => T::ResetFg,
Attribute::ResetBg => T::ResetBg,
Attribute::Bg8Bright(n) => {
value.bright_bg_8 = n.0;
T::BrightBg8
}
Attribute::Fg8Bright(n) => {
value.bright_fg_8 = n.0;
T::BrightFg8
}
Attribute::Bg256(v) => {
value.bg_256 = v;
T::Bg256
}
Attribute::Fg256(v) => {
value.fg_256 = v;
T::Fg256
}
};
SgrAttribute {
tag: tag as c_int,
value,
}
}
}
/// # Safety
/// `allocator` must be null or valid; `result` must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_new(
allocator: *const Allocator,
result: *mut *mut SgrParser,
) -> GhosttyResult {
// SAFETY: forwarded caller contract.
let alloc = unsafe { Alloc::from_c(allocator) };
let Some(p) = alloc.create(SgrParser {
alloc,
params: std::ptr::null_mut(),
params_len: 0,
params_sep: SepList::init_empty(),
idx: 0,
}) else {
return GhosttyResult::OutOfMemory;
};
// SAFETY: caller guarantees `result` is valid for writes.
unsafe { *result = p.as_ptr() };
GhosttyResult::Success
}
/// # Safety
/// `parser` must be null or a live parser from `ghostty_sgr_new`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_free(parser: *mut SgrParser) {
let Some(mut p) = NonNull::new(parser) else {
return;
};
// SAFETY: `p` came from `Alloc::create` in `ghostty_sgr_new`.
unsafe {
let alloc = p.as_ref().alloc;
p.as_mut().free_params();
alloc.destroy(p);
}
}
/// # Safety
/// `parser` must be null or a live parser.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_reset(parser: *mut SgrParser) {
// SAFETY: caller guarantees a live parser (or null).
if let Some(p) = unsafe { parser.as_mut() } {
p.idx = 0;
}
}
/// # Safety
/// `parser` must be null or live; `params` must point to `len` `u16`s;
/// `separators`, if non-null, must point to `len` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_set_params(
parser: *mut SgrParser,
params: *const u16,
separators: *const u8,
len: usize,
) -> GhosttyResult {
// SAFETY: caller guarantees a live parser (or null).
let Some(p) = (unsafe { parser.as_mut() }) else {
return GhosttyResult::InvalidValue;
};
if params.is_null() && len > 0 {
return GhosttyResult::InvalidValue;
}
// Copy the new parameters (Zig: `alloc.dupe`).
let layout = SgrParser::params_layout(len);
let new = p.alloc.alloc_raw(layout).cast::<u16>();
if new.is_null() {
return GhosttyResult::OutOfMemory;
}
if len > 0 {
// SAFETY: `params` has `len` readable elements; `new` has room for `len`.
unsafe { std::ptr::copy_nonoverlapping(params, new, len) };
}
p.free_params();
p.params = new;
p.params_len = len;
// If we have separators, set that state too.
p.params_sep = SepList::init_empty();
if !separators.is_null() {
if len > MAX_PARAMS {
log::warn!(
"ghostty_sgr_set_params: separators length {len} exceeds max supported length {MAX_PARAMS}"
);
return GhosttyResult::InvalidValue;
}
// SAFETY: caller guarantees `len` readable separator bytes.
let seps = unsafe { std::slice::from_raw_parts(separators, len) };
for (i, sep) in seps.iter().enumerate() {
if *sep == b':' {
p.params_sep.set(i);
}
}
}
// Reset our parsing state.
p.idx = 0;
GhosttyResult::Success
}
/// # Safety
/// `parser` must be null or live; `result` must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_next(
parser: *mut SgrParser,
result: *mut SgrAttribute,
) -> bool {
// SAFETY: caller guarantees a live parser (or null).
let Some(p) = (unsafe { parser.as_mut() }) else {
return false;
};
let mut sp = Parser {
params: p.params(),
params_sep: p.params_sep,
idx: p.idx,
};
let attr = sp.next();
let idx = sp.idx;
// `attr` borrows `p.params`; convert before touching `p` mutably.
let Some(attr) = attr else {
p.idx = idx;
return false;
};
let c = SgrAttribute::from_attr(&attr);
p.idx = idx;
// SAFETY: caller guarantees `result` is valid for writes.
unsafe { result.write(c) };
true
}
/// # Safety
/// `ptr`, if non-null, must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_unknown_full(
unknown: SgrUnknown,
ptr: *mut *const u16,
) -> usize {
if !ptr.is_null() {
// SAFETY: non-null and valid per the contract.
unsafe { *ptr = unknown.full_ptr };
}
unknown.full_len
}
/// # Safety
/// `ptr`, if non-null, must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_unknown_partial(
unknown: SgrUnknown,
ptr: *mut *const u16,
) -> usize {
if !ptr.is_null() {
// SAFETY: non-null and valid per the contract.
unsafe { *ptr = unknown.partial_ptr };
}
unknown.partial_len
}
/// Returns the `GhosttySgrAttributeTag` of `attr`.
#[unsafe(no_mangle)]
pub extern "C" fn ghostty_sgr_attribute_tag(attr: SgrAttribute) -> c_int {
attr.tag
}
/// # Safety
/// `attr` must point to a valid attribute; the result points into it.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_attribute_value(
attr: *mut SgrAttribute,
) -> *mut SgrAttributeValue {
// SAFETY: caller guarantees `attr` is valid; no reference is created.
unsafe { &raw mut (*attr).value }
}
#[cfg(test)]
mod tests {
use super::*;
fn new() -> *mut SgrParser {
let mut p = std::ptr::null_mut();
// SAFETY: valid out pointer, default allocator.
assert_eq!(
unsafe { ghostty_sgr_new(std::ptr::null(), &mut p) },
GhosttyResult::Success
);
p
}
fn zeroed() -> SgrAttribute {
SgrAttribute {
tag: 0,
value: SgrAttributeValue { _padding: [0; 8] },
}
}
#[test]
fn alloc() {
let p = new();
// SAFETY: live parser.
unsafe { ghostty_sgr_free(p) };
}
#[test]
fn simple_params_no_seps() {
let p = new();
let mut attr = zeroed();
// SAFETY: live parser; valid buffers.
unsafe {
let params = [1u16];
// Set it twice on purpose to make sure we don't leak.
for _ in 0..2 {
assert_eq!(
ghostty_sgr_set_params(p, params.as_ptr(), std::ptr::null(), 1),
GhosttyResult::Success
);
}
assert!(ghostty_sgr_next(p, &mut attr));
assert_eq!(attr.tag, SgrAttributeTag::Bold as c_int);
assert!(!ghostty_sgr_next(p, &mut attr));
ghostty_sgr_free(p);
}
}
#[test]
fn colon_separated_underline_and_colors() {
let p = new();
let params: [u16; 17] = [
4, 3, 38, 2, 51, 51, 51, 48, 2, 170, 170, 170, 58, 2, 255, 97, 136,
];
let mut seps = [b';'; 17];
seps[0] = b':';
let mut attr = zeroed();
// SAFETY: live parser; valid buffers.
unsafe {
assert_eq!(
ghostty_sgr_set_params(p, params.as_ptr(), seps.as_ptr(), 17),
GhosttyResult::Success
);
assert!(ghostty_sgr_next(p, &mut attr));
assert_eq!(attr.tag, SgrAttributeTag::Underline as c_int);
assert_eq!(attr.value.underline, 3);
assert!(ghostty_sgr_next(p, &mut attr));
assert_eq!(attr.tag, SgrAttributeTag::DirectColorFg as c_int);
assert_eq!(
attr.value.direct_color_fg,
GhosttyColorRgb {
r: 51,
g: 51,
b: 51
}
);
assert!(ghostty_sgr_next(p, &mut attr));
assert_eq!(attr.tag, SgrAttributeTag::DirectColorBg as c_int);
assert!(ghostty_sgr_next(p, &mut attr));
assert_eq!(attr.tag, SgrAttributeTag::UnderlineColor as c_int);
assert_eq!(
attr.value.underline_color,
GhosttyColorRgb {
r: 255,
g: 97,
b: 136
}
);
assert!(!ghostty_sgr_next(p, &mut attr));
ghostty_sgr_reset(p);
assert!(ghostty_sgr_next(p, &mut attr));
ghostty_sgr_free(p);
}
}
#[test]
fn empty_params_is_unset_and_too_many_seps_is_invalid() {
let p = new();
let mut attr = zeroed();
// SAFETY: live parser; valid buffers.
unsafe {
assert_eq!(
ghostty_sgr_set_params(p, [].as_ptr(), std::ptr::null(), 0),
GhosttyResult::Success
);
assert!(ghostty_sgr_next(p, &mut attr));
assert_eq!(attr.tag, SgrAttributeTag::Unset as c_int);
assert!(!ghostty_sgr_next(p, &mut attr));
let params = [0u16; 25];
let seps = [b';'; 25];
assert_eq!(
ghostty_sgr_set_params(p, params.as_ptr(), seps.as_ptr(), 25),
GhosttyResult::InvalidValue
);
assert_eq!(
ghostty_sgr_set_params(std::ptr::null_mut(), params.as_ptr(), std::ptr::null(), 1),
GhosttyResult::InvalidValue
);
ghostty_sgr_free(p);
}
}
#[test]
fn unknown_attribute_exposes_params() {
let p = new();
let params = [9999u16];
let mut attr = zeroed();
// SAFETY: live parser; valid buffers.
unsafe {
ghostty_sgr_set_params(p, params.as_ptr(), std::ptr::null(), 1);
assert!(ghostty_sgr_next(p, &mut attr));
assert_eq!(attr.tag, SgrAttributeTag::Unknown as c_int);
let mut ptr: *const u16 = std::ptr::null();
let n = ghostty_sgr_unknown_full(attr.value.unknown, &mut ptr);
assert_eq!(std::slice::from_raw_parts(ptr, n), &[9999]);
let n = ghostty_sgr_unknown_partial(attr.value.unknown, std::ptr::null_mut());
assert!(n <= 1);
assert_eq!(
ghostty_sgr_attribute_tag(attr),
SgrAttributeTag::Unknown as c_int
);
assert_eq!(
ghostty_sgr_attribute_value(&mut attr).cast::<u8>(),
(&raw mut attr).cast::<u8>().add(8)
);
ghostty_sgr_free(p);
}
}
}
+149
View File
@@ -0,0 +1,149 @@
//! Wasm allocation helpers (ZIG/src/lib/allocator/convenience.zig and
//! `wasm_alloc_attribute` in ZIG/src/terminal/c/sgr.zig,
//! `include/ghostty/vt/wasm.h`).
//!
//! JavaScript hosts cannot easily create the pointers the C API wants, so
//! upstream exports these helpers on wasm. They always use the default
//! allocator. They are exported (`no_mangle`) only when compiling for wasm.
#![allow(unsafe_code)]
use std::{alloc::Layout, ffi::c_void};
use crate::sgr::SgrAttribute;
fn alloc_layout(layout: Layout) -> *mut u8 {
if layout.size() == 0 {
return layout.align() as *mut u8;
}
// SAFETY: non-zero size.
unsafe { std::alloc::alloc(layout) }
}
/// # Safety
/// `ptr` must be null or come from `alloc_layout` with the same `layout`.
unsafe fn free_layout(ptr: *mut u8, layout: Layout) {
if !ptr.is_null() && layout.size() != 0 {
// SAFETY: per the contract.
unsafe { std::alloc::dealloc(ptr, layout) };
}
}
/// Allocate one `void *` (e.g. an out-parameter for `*_new`).
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_opaque() -> *mut *mut c_void {
alloc_layout(Layout::new::<*mut c_void>()).cast()
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_opaque`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_opaque(ptr: *mut *mut c_void) {
// SAFETY: per the contract.
unsafe { free_layout(ptr.cast(), Layout::new::<*mut c_void>()) };
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_u8_array(len: usize) -> *mut u8 {
Layout::array::<u8>(len).map_or(std::ptr::null_mut(), alloc_layout)
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_u8_array(len)`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_u8_array(ptr: *mut u8, len: usize) {
if let Ok(l) = Layout::array::<u8>(len) {
// SAFETY: per the contract.
unsafe { free_layout(ptr, l) };
}
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_u16_array(len: usize) -> *mut u16 {
Layout::array::<u16>(len).map_or(std::ptr::null_mut(), |l| alloc_layout(l).cast())
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_u16_array(len)`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_u16_array(ptr: *mut u16, len: usize) {
if let Ok(l) = Layout::array::<u16>(len) {
// SAFETY: per the contract.
unsafe { free_layout(ptr.cast(), l) };
}
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_u8() -> *mut u8 {
alloc_layout(Layout::new::<u8>())
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_u8`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_u8(ptr: *mut u8) {
// SAFETY: per the contract.
unsafe { free_layout(ptr, Layout::new::<u8>()) };
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_usize() -> *mut usize {
alloc_layout(Layout::new::<usize>()).cast()
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_usize`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_usize(ptr: *mut usize) {
// SAFETY: per the contract.
unsafe { free_layout(ptr.cast(), Layout::new::<usize>()) };
}
/// Allocate a `GhosttySgrAttribute` (zero-initialized).
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_sgr_attribute() -> *mut SgrAttribute {
let p = alloc_layout(Layout::new::<SgrAttribute>()).cast::<SgrAttribute>();
if !p.is_null() {
// SAFETY: freshly allocated, correctly sized; all-zero is a valid value.
unsafe { p.write_bytes(0, 1) };
}
p
}
/// # Safety
/// `attr` must be null or from `ghostty_wasm_alloc_sgr_attribute`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_sgr_attribute(attr: *mut SgrAttribute) {
// SAFETY: per the contract.
unsafe { free_layout(attr.cast(), Layout::new::<SgrAttribute>()) };
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alloc_and_free_round_trip() {
// SAFETY: pointers come from the matching allocators.
unsafe {
let p = ghostty_wasm_alloc_u8_array(16);
assert!(!p.is_null());
p.write_bytes(7, 16);
ghostty_wasm_free_u8_array(p, 16);
let z = ghostty_wasm_alloc_u8_array(0);
ghostty_wasm_free_u8_array(z, 0);
let u = ghostty_wasm_alloc_u16_array(5);
ghostty_wasm_free_u16_array(u, 5);
let o = ghostty_wasm_alloc_opaque();
ghostty_wasm_free_opaque(o);
let b = ghostty_wasm_alloc_u8();
ghostty_wasm_free_u8(b);
let s = ghostty_wasm_alloc_usize();
ghostty_wasm_free_usize(s);
let a = ghostty_wasm_alloc_sgr_attribute();
assert_eq!((*a).tag, 0);
ghostty_wasm_free_sgr_attribute(a);
}
}
}