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,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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) });
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user