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
5b9a0723a9
commit
75252a7651
35 files changed
+6911
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user