diff --git a/crates/ghostty-vt-capi/Cargo.toml b/crates/ghostty-vt-capi/Cargo.toml new file mode 100644 index 0000000..977dbf8 --- /dev/null +++ b/crates/ghostty-vt-capi/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "ghostty-vt-capi" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "C ABI of libghostty-vt (include/ghostty/vt.h), ported from Ghostty's lib_vt.zig" + +# The crate name cannot contain a hyphen, so Cargo produces +# `libghostty_vt_capi.{so,a}`. `just vt-lib` (and the integration tests) +# install them as `libghostty-vt.{so,a}`, the names Ghostty ships; the soname +# is set to `libghostty-vt.so` by build.rs. +[lib] +name = "ghostty_vt_capi" +crate-type = ["cdylib", "staticlib", "rlib"] + +[dependencies] +ghostty-vt.workspace = true +ghostty-input.workspace = true +log = "0.4.34" + +# The workspace forbids `unsafe`; this crate is the C ABI boundary, so it +# denies it instead and allows it (with SAFETY comments) in the FFI modules. +# Everything else is copied from `[workspace.lints]`. +[lints.rust] +unsafe_code = "deny" + +[lints.clippy] +pedantic = { level = "warn", priority = -1 } +todo = "warn" +unwrap_used = "warn" +cast_possible_truncation = "allow" +cast_possible_wrap = "allow" +cast_sign_loss = "allow" +cast_precision_loss = "allow" +cast_lossless = "allow" +module_name_repetitions = "allow" +too_many_lines = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +similar_names = "allow" +struct_excessive_bools = "allow" +fn_params_excessive_bools = "allow" +match_same_arms = "allow" +unreadable_literal = "allow" +many_single_char_names = "allow" +doc_markdown = "allow" +must_use_candidate = "allow" +return_self_not_must_use = "allow" +used_underscore_binding = "allow" +large_stack_arrays = "allow" +inline_always = "allow" +# The exported functions are `unsafe extern "C"` mirroring the header contracts. +missing_safety_doc = "allow" +# `&mut x` coerced to `*mut T` is the idiomatic way to pass out-params in FFI tests. +borrow_as_ptr = "allow" +# Allocations are made with the layout of the pointee, so the cast is aligned. +cast_ptr_alignment = "allow" diff --git a/crates/ghostty-vt-capi/build.rs b/crates/ghostty-vt-capi/build.rs new file mode 100644 index 0000000..b7b0e5f --- /dev/null +++ b/crates/ghostty-vt-capi/build.rs @@ -0,0 +1,14 @@ +fn main() { + // Ship the shared library under Ghostty's name (`libghostty-vt.so`). + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + match target_os.as_str() { + "linux" | "android" | "freebsd" => { + println!("cargo:rustc-cdylib-link-arg=-Wl,-soname,libghostty-vt.so"); + } + "macos" | "ios" => { + println!("cargo:rustc-cdylib-link-arg=-Wl,-install_name,@rpath/libghostty-vt.dylib"); + } + _ => {} + } + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/crates/ghostty-vt-capi/examples/c/c-vt-key-encode.c b/crates/ghostty-vt-capi/examples/c/c-vt-key-encode.c new file mode 100644 index 0000000..82444f9 --- /dev/null +++ b/crates/ghostty-vt-capi/examples/c/c-vt-key-encode.c @@ -0,0 +1,59 @@ +#include +#include +#include +#include +#include + +int main() { + GhosttyKeyEncoder encoder; + GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder); + assert(result == GHOSTTY_SUCCESS); + + // Set kitty flags with all features enabled + ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS, &(uint8_t){GHOSTTY_KITTY_KEY_ALL}); + + // Create key event + GhosttyKeyEvent event; + result = ghostty_key_event_new(NULL, &event); + assert(result == GHOSTTY_SUCCESS); + ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_RELEASE); + ghostty_key_event_set_key(event, GHOSTTY_KEY_CONTROL_LEFT); + ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL); + printf("Encoding event: left ctrl release with all Kitty flags enabled\n"); + + // Optionally, encode with null buffer to get required size. You can + // skip this step and provide a sufficiently large buffer directly. + // If there isn't enoug hspace, the function will return an out of memory + // error. + size_t required = 0; + result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required); + assert(result == GHOSTTY_OUT_OF_MEMORY); + printf("Required buffer size: %zu bytes\n", required); + + // Encode the key event. We don't use our required size above because + // that was just an example; we know 128 bytes is enough. + char buf[128]; + size_t written = 0; + result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + assert(result == GHOSTTY_SUCCESS); + printf("Encoded %zu bytes\n", written); + + // Print the encoded sequence (hex and string) + printf("Hex: "); + for (size_t i = 0; i < written; i++) printf("%02x ", (unsigned char)buf[i]); + printf("\n"); + + printf("String: "); + for (size_t i = 0; i < written; i++) { + if (buf[i] == 0x1b) { + printf("\\x1b"); + } else { + printf("%c", buf[i]); + } + } + printf("\n"); + + ghostty_key_event_free(event); + ghostty_key_encoder_free(encoder); + return 0; +} diff --git a/crates/ghostty-vt-capi/examples/c/c-vt-paste.c b/crates/ghostty-vt-capi/examples/c/c-vt-paste.c new file mode 100644 index 0000000..153861c --- /dev/null +++ b/crates/ghostty-vt-capi/examples/c/c-vt-paste.c @@ -0,0 +1,31 @@ +#include +#include +#include + +int main() { + // Test safe paste data + const char *safe_data = "hello world"; + if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) { + printf("'%s' is safe to paste\n", safe_data); + } + + // Test unsafe paste data with newline + const char *unsafe_newline = "rm -rf /\n"; + if (!ghostty_paste_is_safe(unsafe_newline, strlen(unsafe_newline))) { + printf("'%s' is UNSAFE - contains newline\n", unsafe_newline); + } + + // Test unsafe paste data with bracketed paste end sequence + const char *unsafe_escape = "evil\x1b[201~code"; + if (!ghostty_paste_is_safe(unsafe_escape, strlen(unsafe_escape))) { + printf("Data with escape sequence is UNSAFE\n"); + } + + // Test empty data + const char *empty_data = ""; + if (ghostty_paste_is_safe(empty_data, 0)) { + printf("Empty data is safe\n"); + } + + return 0; +} diff --git a/crates/ghostty-vt-capi/examples/c/c-vt-sgr.c b/crates/ghostty-vt-capi/examples/c/c-vt-sgr.c new file mode 100644 index 0000000..21a5297 --- /dev/null +++ b/crates/ghostty-vt-capi/examples/c/c-vt-sgr.c @@ -0,0 +1,131 @@ +#include +#include +#include + +int main() { + // Create parser + GhosttySgrParser parser; + GhosttyResult result = ghostty_sgr_new(NULL, &parser); + assert(result == GHOSTTY_SUCCESS); + + // Parse a complex SGR sequence from Kakoune + // This corresponds to the escape sequence: + // ESC[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136m + // + // Breaking down the sequence: + // - 4:3 = curly underline (colon-separated sub-parameters) + // - 38;2;51;51;51 = foreground RGB color (51, 51, 51) - dark gray + // - 48;2;170;170;170 = background RGB color (170, 170, 170) - light gray + // - 58;2;255;97;136 = underline RGB color (255, 97, 136) - pink + uint16_t params[] = {4, 3, 38, 2, 51, 51, 51, 48, 2, 170, 170, 170, 58, 2, 255, 97, 136}; + + // Separator array: ':' at position 0 (between 4 and 3), ';' elsewhere + char separators[] = ";;;;;;;;;;;;;;;;"; + separators[0] = ':'; + + result = ghostty_sgr_set_params(parser, params, separators, sizeof(params) / sizeof(params[0])); + assert(result == GHOSTTY_SUCCESS); + + printf("Parsing Kakoune SGR sequence:\n"); + printf("ESC[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136m\n\n"); + + // Iterate through attributes + GhosttySgrAttribute attr; + int count = 0; + while (ghostty_sgr_next(parser, &attr)) { + count++; + printf("Attribute %d: ", count); + + switch (attr.tag) { + case GHOSTTY_SGR_ATTR_UNDERLINE: + printf("Underline style = "); + switch (attr.value.underline) { + case GHOSTTY_SGR_UNDERLINE_NONE: + printf("none\n"); + break; + case GHOSTTY_SGR_UNDERLINE_SINGLE: + printf("single\n"); + break; + case GHOSTTY_SGR_UNDERLINE_DOUBLE: + printf("double\n"); + break; + case GHOSTTY_SGR_UNDERLINE_CURLY: + printf("curly\n"); + break; + case GHOSTTY_SGR_UNDERLINE_DOTTED: + printf("dotted\n"); + break; + case GHOSTTY_SGR_UNDERLINE_DASHED: + printf("dashed\n"); + break; + default: + printf("unknown (%d)\n", attr.value.underline); + break; + } + break; + + case GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG: + printf("Foreground RGB = (%d, %d, %d)\n", + attr.value.direct_color_fg.r, + attr.value.direct_color_fg.g, + attr.value.direct_color_fg.b); + break; + + case GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG: + printf("Background RGB = (%d, %d, %d)\n", + attr.value.direct_color_bg.r, + attr.value.direct_color_bg.g, + attr.value.direct_color_bg.b); + break; + + case GHOSTTY_SGR_ATTR_UNDERLINE_COLOR: + printf("Underline color RGB = (%d, %d, %d)\n", + attr.value.underline_color.r, + attr.value.underline_color.g, + attr.value.underline_color.b); + break; + + case GHOSTTY_SGR_ATTR_FG_8: + printf("Foreground 8-color = %d\n", attr.value.fg_8); + break; + + case GHOSTTY_SGR_ATTR_BG_8: + printf("Background 8-color = %d\n", attr.value.bg_8); + break; + + case GHOSTTY_SGR_ATTR_FG_256: + printf("Foreground 256-color = %d\n", attr.value.fg_256); + break; + + case GHOSTTY_SGR_ATTR_BG_256: + printf("Background 256-color = %d\n", attr.value.bg_256); + break; + + case GHOSTTY_SGR_ATTR_BOLD: + printf("Bold\n"); + break; + + case GHOSTTY_SGR_ATTR_ITALIC: + printf("Italic\n"); + break; + + case GHOSTTY_SGR_ATTR_UNSET: + printf("Reset all attributes\n"); + break; + + case GHOSTTY_SGR_ATTR_UNKNOWN: + printf("Unknown attribute\n"); + break; + + default: + printf("Other attribute (tag=%d)\n", attr.tag); + break; + } + } + + printf("\nTotal attributes parsed: %d\n", count); + + // Cleanup + ghostty_sgr_free(parser); + return 0; +} diff --git a/crates/ghostty-vt-capi/examples/c/c-vt.c b/crates/ghostty-vt-capi/examples/c/c-vt.c new file mode 100644 index 0000000..b1297d7 --- /dev/null +++ b/crates/ghostty-vt-capi/examples/c/c-vt.c @@ -0,0 +1,36 @@ +#include +#include +#include +#include + +int main() { + GhosttyOscParser parser; + if (ghostty_osc_new(NULL, &parser) != GHOSTTY_SUCCESS) { + return 1; + } + + // Setup change window title command to change the title to "hello" + ghostty_osc_next(parser, '0'); + ghostty_osc_next(parser, ';'); + const char *title = "hello"; + for (size_t i = 0; i < strlen(title); i++) { + ghostty_osc_next(parser, title[i]); + } + + // End parsing and get command + GhosttyOscCommand command = ghostty_osc_end(parser, 0); + + // Get and print command type + GhosttyOscCommandType type = ghostty_osc_command_type(command); + printf("Command type: %d\n", type); + + // Extract and print the title + if (ghostty_osc_command_data(command, GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR, &title)) { + printf("Extracted title: %s\n", title); + } else { + printf("Failed to extract title\n"); + } + + ghostty_osc_free(parser); + return 0; +} diff --git a/crates/ghostty-vt-capi/examples/wasm/key-encode.html b/crates/ghostty-vt-capi/examples/wasm/key-encode.html new file mode 100644 index 0000000..9f4d8be --- /dev/null +++ b/crates/ghostty-vt-capi/examples/wasm/key-encode.html @@ -0,0 +1,687 @@ + + + + + + Ghostty VT Key Encoder - WebAssembly Example + + + +

Ghostty VT Key Encoder - WebAssembly Example

+

This example demonstrates encoding key events into terminal escape sequences using the Ghostty VT WebAssembly module.

+ +
+ ⚠️ Warning: + This is an example of the libghostty-vt WebAssembly API. The JavaScript + keyboard event mapping to the libghostty-vt API may not be perfect + and may result in encoding inaccuracies for certain keys or layouts. + Do not use this as a key encoding reference. +
+ +
Loading WebAssembly module...
+ +
+

Key Action

+
+ + + +
+
+ +
+

Kitty Keyboard Protocol Flags

+
+ + + + + +
+
+ + + +
Waiting for key events...
+ +

Note: This example must be served via HTTP (not opened directly as a file). See the README for instructions.

+ + + + diff --git a/crates/ghostty-vt-capi/examples/wasm/sgr.html b/crates/ghostty-vt-capi/examples/wasm/sgr.html new file mode 100644 index 0000000..e62b26c --- /dev/null +++ b/crates/ghostty-vt-capi/examples/wasm/sgr.html @@ -0,0 +1,457 @@ + + + + + + Ghostty VT SGR Parser - WebAssembly Example + + + +

Ghostty VT SGR Parser - WebAssembly Example

+

This example demonstrates parsing terminal SGR (Select Graphic Rendition) sequences using the Ghostty VT WebAssembly module.

+ +
Loading WebAssembly module...
+ +
+

SGR Sequence

+ + +

The parser runs live as you type.

+
+ +
Waiting for input...
+ +

Note: This example must be served via HTTP (not opened directly as a file). See the README for instructions.

+ + + + diff --git a/crates/ghostty-vt-capi/include/ghostty/vt.h b/crates/ghostty-vt-capi/include/ghostty/vt.h new file mode 100644 index 0000000..4f8fef8 --- /dev/null +++ b/crates/ghostty-vt-capi/include/ghostty/vt.h @@ -0,0 +1,87 @@ +/** + * @file vt.h + * + * libghostty-vt - Virtual terminal emulator library + * + * This library provides functionality for parsing and handling terminal + * escape sequences as well as maintaining terminal state such as styles, + * cursor position, screen, scrollback, and more. + * + * WARNING: This is an incomplete, work-in-progress API. It is not yet + * stable and is definitely going to change. + */ + +/** + * @mainpage libghostty-vt - Virtual Terminal Emulator Library + * + * libghostty-vt is a C library which implements a modern terminal emulator, + * extracted from the [Ghostty](https://ghostty.org) terminal emulator. + * + * libghostty-vt contains the logic for handling the core parts of a terminal + * emulator: parsing terminal escape sequences, maintaining terminal state, + * encoding input events, etc. It can handle scrollback, line wrapping, + * reflow on resize, and more. + * + * @warning This library is currently in development and the API is not yet stable. + * Breaking changes are expected in future versions. Use with caution in production code. + * + * @section groups_sec API Reference + * + * The API is organized into the following groups: + * - @ref key "Key Encoding" - Encode key events into terminal sequences + * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences + * - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences + * - @ref paste "Paste Utilities" - Validate paste data safety + * - @ref allocator "Memory Management" - Memory management and custom allocators + * - @ref wasm "WebAssembly Utilities" - WebAssembly convenience functions + * + * @section examples_sec Examples + * + * Complete working examples: + * - @ref c-vt/src/main.c - OSC parser example + * - @ref c-vt-key-encode/src/main.c - Key encoding example + * - @ref c-vt-paste/src/main.c - Paste safety check example + * - @ref c-vt-sgr/src/main.c - SGR parser example + * + */ + +/** @example c-vt/src/main.c + * This example demonstrates how to use the OSC parser to parse an OSC sequence, + * extract command information, and retrieve command-specific data like window titles. + */ + +/** @example c-vt-key-encode/src/main.c + * This example demonstrates how to use the key encoder to convert key events + * into terminal escape sequences using the Kitty keyboard protocol. + */ + +/** @example c-vt-paste/src/main.c + * This example demonstrates how to use the paste utilities to check if + * paste data is safe before sending it to the terminal. + */ + +/** @example c-vt-sgr/src/main.c + * This example demonstrates how to use the SGR parser to parse terminal + * styling sequences and extract text attributes like colors and underline styles. + */ + +#ifndef GHOSTTY_VT_H +#define GHOSTTY_VT_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_H */ diff --git a/crates/ghostty-vt-capi/include/ghostty/vt/allocator.h b/crates/ghostty-vt-capi/include/ghostty/vt/allocator.h new file mode 100644 index 0000000..4cebe91 --- /dev/null +++ b/crates/ghostty-vt-capi/include/ghostty/vt/allocator.h @@ -0,0 +1,196 @@ +/** + * @file allocator.h + * + * Memory management interface for libghostty-vt. + */ + +#ifndef GHOSTTY_VT_ALLOCATOR_H +#define GHOSTTY_VT_ALLOCATOR_H + +#include +#include +#include + +/** @defgroup allocator Memory Management + * + * libghostty-vt does require memory allocation for various operations, + * but is resilient to allocation failures and will gracefully handle + * out-of-memory situations by returning error codes. + * + * The exact memory management semantics are documented in the relevant + * functions and data structures. + * + * libghostty-vt uses explicit memory allocation via an allocator + * interface provided by GhosttyAllocator. The interface is based on the + * [Zig](https://ziglang.org) allocator interface, since this has been + * shown to be a flexible and powerful interface in practice and enables + * a wide variety of allocation strategies. + * + * **For the common case, you can pass NULL as the allocator for any + * function that accepts one,** and libghostty will use a default allocator. + * The default allocator will be libc malloc/free if libc is linked. + * Otherwise, a custom allocator is used (currently Zig's SMP allocator) + * that doesn't require any external dependencies. + * + * ## Basic Usage + * + * For simple use cases, you can ignore this interface entirely by passing NULL + * as the allocator parameter to functions that accept one. This will use the + * default allocator (typically libc malloc/free, if libc is linked, but + * we provide our own default allocator if libc isn't linked). + * + * To use a custom allocator: + * 1. Implement the GhosttyAllocatorVtable function pointers + * 2. Create a GhosttyAllocator struct with your vtable and context + * 3. Pass the allocator to functions that accept one + * + * @{ + */ + +/** + * Function table for custom memory allocator operations. + * + * This vtable defines the interface for a custom memory allocator. All + * function pointers must be valid and non-NULL. + * + * @ingroup allocator + * + * If you're not going to use a custom allocator, you can ignore all of + * this. All functions that take an allocator pointer allow NULL to use a + * default allocator. + * + * The interface is based on the Zig allocator interface. I'll say up front + * that it is easy to look at this interface and think "wow, this is really + * overcomplicated". The reason for this complexity is well thought out by + * the Zig folks, and it enables a diverse set of allocation strategies + * as shown by the Zig ecosystem. As a consolation, please note that many + * of the arguments are only needed for advanced use cases and can be + * safely ignored in simple implementations. For example, if you look at + * the Zig implementation of the libc allocator in `lib/std/heap.zig` + * (search for CAllocator), you'll see it is very simple. + * + * We chose to align with the Zig allocator interface because: + * + * 1. It is a proven interface that serves a wide variety of use cases + * in the real world via the Zig ecosystem. It's shown to work. + * + * 2. Our core implementation itself is Zig, and this lets us very + * cheaply and easily convert between C and Zig allocators. + * + * NOTE(mitchellh): In the future, we can have default implementations of + * resize/remap and allow those to be null. + */ +typedef struct { + /** + * Return a pointer to `len` bytes with specified `alignment`, or return + * `NULL` indicating the allocation failed. + * + * @param ctx The allocator context + * @param len Number of bytes to allocate + * @param alignment Required alignment for the allocation. Guaranteed to + * be a power of two between 1 and 16 inclusive. + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return Pointer to allocated memory, or NULL if allocation failed + */ + void* (*alloc)(void *ctx, size_t len, uint8_t alignment, uintptr_t ret_addr); + + /** + * Attempt to expand or shrink memory in place. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * `new_len` must be greater than zero. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to resize + * @param memory_len Current size of the memory block + * @param alignment Alignment (must match original allocation) + * @param new_len New requested size + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return true if resize was successful in-place, false if relocation would be required + */ + bool (*resize)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); + + /** + * Attempt to expand or shrink memory, allowing relocation. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * A non-`NULL` return value indicates the resize was successful. The + * allocation may have same address, or may have been relocated. In either + * case, the allocation now has size of `new_len`. A `NULL` return value + * indicates that the resize would be equivalent to allocating new memory, + * copying the bytes from the old memory, and then freeing the old memory. + * In such case, it is more efficient for the caller to perform the copy. + * + * `new_len` must be greater than zero. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to remap + * @param memory_len Current size of the memory block + * @param alignment Alignment (must match original allocation) + * @param new_len New requested size + * @param ret_addr First return address of the allocation call stack (0 if not provided) + * @return Pointer to resized memory (may be relocated), or NULL if manual copy is needed + */ + void* (*remap)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, size_t new_len, uintptr_t ret_addr); + + /** + * Free and invalidate a region of memory. + * + * `memory_len` must equal the length requested from the most recent + * successful call to `alloc`, `resize`, or `remap`. `alignment` must + * equal the same value that was passed as the `alignment` parameter to + * the original `alloc` call. + * + * @param ctx The allocator context + * @param memory Pointer to the memory block to free + * @param memory_len Size of the memory block + * @param alignment Alignment (must match original allocation) + * @param ret_addr First return address of the allocation call stack (0 if not provided) + */ + void (*free)(void *ctx, void *memory, size_t memory_len, uint8_t alignment, uintptr_t ret_addr); +} GhosttyAllocatorVtable; + +/** + * Custom memory allocator. + * + * For functions that take an allocator pointer, a NULL pointer indicates + * that the default allocator should be used. The default allocator will + * be libc malloc/free if we're linking to libc. If libc isn't linked, + * a custom allocator is used (currently Zig's SMP allocator). + * + * @ingroup allocator + * + * Usage example: + * @code + * GhosttyAllocator allocator = { + * .vtable = &my_allocator_vtable, + * .ctx = my_allocator_state + * }; + * @endcode + */ +typedef struct GhosttyAllocator { + /** + * Opaque context pointer passed to all vtable functions. + * This allows the allocator implementation to maintain state + * or reference external resources needed for memory management. + */ + void *ctx; + + /** + * Pointer to the allocator's vtable containing function pointers + * for memory operations (alloc, resize, remap, free). + */ + const GhosttyAllocatorVtable *vtable; +} GhosttyAllocator; + +/** @} */ + +#endif /* GHOSTTY_VT_ALLOCATOR_H */ diff --git a/crates/ghostty-vt-capi/include/ghostty/vt/color.h b/crates/ghostty-vt-capi/include/ghostty/vt/color.h new file mode 100644 index 0000000..0d57b8d --- /dev/null +++ b/crates/ghostty-vt-capi/include/ghostty/vt/color.h @@ -0,0 +1,96 @@ +/** + * @file color.h + * + * Color types and utilities. + */ + +#ifndef GHOSTTY_VT_COLOR_H +#define GHOSTTY_VT_COLOR_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * RGB color value. + * + * @ingroup sgr + */ +typedef struct { + uint8_t r; /**< Red component (0-255) */ + uint8_t g; /**< Green component (0-255) */ + uint8_t b; /**< Blue component (0-255) */ +} GhosttyColorRgb; + +/** + * Palette color index (0-255). + * + * @ingroup sgr + */ +typedef uint8_t GhosttyColorPaletteIndex; + +/** @addtogroup sgr + * @{ + */ + +/** Black color (0) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BLACK 0 +/** Red color (1) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_RED 1 +/** Green color (2) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_GREEN 2 +/** Yellow color (3) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_YELLOW 3 +/** Blue color (4) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BLUE 4 +/** Magenta color (5) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_MAGENTA 5 +/** Cyan color (6) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_CYAN 6 +/** White color (7) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_WHITE 7 +/** Bright black color (8) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_BLACK 8 +/** Bright red color (9) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_RED 9 +/** Bright green color (10) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_GREEN 10 +/** Bright yellow color (11) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW 11 +/** Bright blue color (12) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_BLUE 12 +/** Bright magenta color (13) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA 13 +/** Bright cyan color (14) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_CYAN 14 +/** Bright white color (15) @ingroup sgr */ +#define GHOSTTY_COLOR_NAMED_BRIGHT_WHITE 15 + +/** @} */ + +/** + * Get the RGB color components. + * + * This function extracts the individual red, green, and blue components + * from a GhosttyColorRgb value. Primarily useful in WebAssembly environments + * where accessing struct fields directly is difficult. + * + * @param color The RGB color value + * @param r Pointer to store the red component (0-255) + * @param g Pointer to store the green component (0-255) + * @param b Pointer to store the blue component (0-255) + * + * @ingroup sgr + */ +void ghostty_color_rgb_get(GhosttyColorRgb color, + uint8_t* r, + uint8_t* g, + uint8_t* b); + +#ifdef __cplusplus +} +#endif + +#endif /* GHOSTTY_VT_COLOR_H */ diff --git a/crates/ghostty-vt-capi/include/ghostty/vt/key.h b/crates/ghostty-vt-capi/include/ghostty/vt/key.h new file mode 100644 index 0000000..772b5d4 --- /dev/null +++ b/crates/ghostty-vt-capi/include/ghostty/vt/key.h @@ -0,0 +1,80 @@ +/** + * @file key.h + * + * Key encoding module - encode key events into terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_KEY_H +#define GHOSTTY_VT_KEY_H + +/** @defgroup key Key Encoding + * + * Utilities for encoding key events into terminal escape sequences, + * supporting both legacy encoding as well as Kitty Keyboard Protocol. + * + * ## Basic Usage + * + * 1. Create an encoder instance with ghostty_key_encoder_new() + * 2. Configure encoder options with ghostty_key_encoder_setopt(). + * 3. For each key event: + * - Create a key event with ghostty_key_event_new() + * - Set event properties (action, key, modifiers, etc.) + * - Encode with ghostty_key_encoder_encode() + * - Free the event with ghostty_key_event_free() + * - Note: You can also reuse the same key event multiple times by + * changing its properties. + * 4. Free the encoder with ghostty_key_encoder_free() when done + * + * ## Example + * + * @code{.c} + * #include + * #include + * #include + * + * int main() { + * // Create encoder + * GhosttyKeyEncoder encoder; + * GhosttyResult result = ghostty_key_encoder_new(NULL, &encoder); + * assert(result == GHOSTTY_SUCCESS); + * + * // Enable Kitty keyboard protocol with all features + * ghostty_key_encoder_setopt(encoder, GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS, + * &(uint8_t){GHOSTTY_KITTY_KEY_ALL}); + * + * // Create and configure key event for Ctrl+C press + * GhosttyKeyEvent event; + * result = ghostty_key_event_new(NULL, &event); + * assert(result == GHOSTTY_SUCCESS); + * ghostty_key_event_set_action(event, GHOSTTY_KEY_ACTION_PRESS); + * ghostty_key_event_set_key(event, GHOSTTY_KEY_C); + * ghostty_key_event_set_mods(event, GHOSTTY_MODS_CTRL); + * + * // Encode the key event + * char buf[128]; + * size_t written = 0; + * result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * assert(result == GHOSTTY_SUCCESS); + * + * // Use the encoded sequence (e.g., write to terminal) + * fwrite(buf, 1, written, stdout); + * + * // Cleanup + * ghostty_key_event_free(event); + * ghostty_key_encoder_free(encoder); + * return 0; + * } + * @endcode + * + * For a complete working example, see example/c-vt-key-encode in the + * repository. + * + * @{ + */ + +#include +#include + +/** @} */ + +#endif /* GHOSTTY_VT_KEY_H */ diff --git a/crates/ghostty-vt-capi/include/ghostty/vt/key/encoder.h b/crates/ghostty-vt-capi/include/ghostty/vt/key/encoder.h new file mode 100644 index 0000000..766a294 --- /dev/null +++ b/crates/ghostty-vt-capi/include/ghostty/vt/key/encoder.h @@ -0,0 +1,221 @@ +/** + * @file encoder.h + * + * Key event encoding to terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_KEY_ENCODER_H +#define GHOSTTY_VT_KEY_ENCODER_H + +#include +#include +#include +#include +#include + +/** + * Opaque handle to a key encoder instance. + * + * This handle represents a key encoder that converts key events into terminal + * escape sequences. + * + * @ingroup key + */ +typedef struct GhosttyKeyEncoder *GhosttyKeyEncoder; + +/** + * Kitty keyboard protocol flags. + * + * Bitflags representing the various modes of the Kitty keyboard protocol. + * These can be combined using bitwise OR operations. Valid values all + * start with `GHOSTTY_KITTY_KEY_`. + * + * @ingroup key + */ +typedef uint8_t GhosttyKittyKeyFlags; + +/** Kitty keyboard protocol disabled (all flags off) */ +#define GHOSTTY_KITTY_KEY_DISABLED 0 + +/** Disambiguate escape codes */ +#define GHOSTTY_KITTY_KEY_DISAMBIGUATE (1 << 0) + +/** Report key press and release events */ +#define GHOSTTY_KITTY_KEY_REPORT_EVENTS (1 << 1) + +/** Report alternate key codes */ +#define GHOSTTY_KITTY_KEY_REPORT_ALTERNATES (1 << 2) + +/** Report all key events including those normally handled by the terminal */ +#define GHOSTTY_KITTY_KEY_REPORT_ALL (1 << 3) + +/** Report associated text with key events */ +#define GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED (1 << 4) + +/** All Kitty keyboard protocol flags enabled */ +#define GHOSTTY_KITTY_KEY_ALL (GHOSTTY_KITTY_KEY_DISAMBIGUATE | GHOSTTY_KITTY_KEY_REPORT_EVENTS | GHOSTTY_KITTY_KEY_REPORT_ALTERNATES | GHOSTTY_KITTY_KEY_REPORT_ALL | GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED) + +/** + * macOS option key behavior. + * + * Determines whether the "option" key on macOS is treated as "alt" or not. + * See the Ghostty `macos-option-as-alt` configuration option for more details. + * + * @ingroup key + */ +typedef enum { + /** Option key is not treated as alt */ + GHOSTTY_OPTION_AS_ALT_FALSE = 0, + /** Option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_TRUE = 1, + /** Only left option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_LEFT = 2, + /** Only right option key is treated as alt */ + GHOSTTY_OPTION_AS_ALT_RIGHT = 3, +} GhosttyOptionAsAlt; + +/** + * Key encoder option identifiers. + * + * These values are used with ghostty_key_encoder_setopt() to configure + * the behavior of the key encoder. + * + * @ingroup key + */ +typedef enum { + /** Terminal DEC mode 1: cursor key application mode (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_CURSOR_KEY_APPLICATION = 0, + + /** Terminal DEC mode 66: keypad key application mode (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_KEYPAD_KEY_APPLICATION = 1, + + /** Terminal DEC mode 1035: ignore keypad with numlock (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_IGNORE_KEYPAD_WITH_NUMLOCK = 2, + + /** Terminal DEC mode 1036: alt sends escape prefix (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_ALT_ESC_PREFIX = 3, + + /** xterm modifyOtherKeys mode 2 (value: bool) */ + GHOSTTY_KEY_ENCODER_OPT_MODIFY_OTHER_KEYS_STATE_2 = 4, + + /** Kitty keyboard protocol flags (value: GhosttyKittyKeyFlags bitmask) */ + GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS = 5, + + /** macOS option-as-alt setting (value: GhosttyOptionAsAlt) */ + GHOSTTY_KEY_ENCODER_OPT_MACOS_OPTION_AS_ALT = 6, +} GhosttyKeyEncoderOption; + +/** + * Create a new key encoder instance. + * + * Creates a new key encoder with default options. The encoder can be configured + * using ghostty_key_encoder_setopt() and must be freed using + * ghostty_key_encoder_free() when no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param encoder Pointer to store the created encoder handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup key + */ +GhosttyResult ghostty_key_encoder_new(const GhosttyAllocator *allocator, GhosttyKeyEncoder *encoder); + +/** + * Free a key encoder instance. + * + * Releases all resources associated with the key encoder. After this call, + * the encoder handle becomes invalid and must not be used. + * + * @param encoder The encoder handle to free (may be NULL) + * + * @ingroup key + */ +void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); + +/** + * Set an option on the key encoder. + * + * Configures the behavior of the key encoder. Options control various aspects + * of encoding such as terminal modes (cursor key application mode, keypad mode), + * protocol selection (Kitty keyboard protocol flags), and platform-specific + * behaviors (macOS option-as-alt). + * + * A null pointer value does nothing. It does not reset the value to the + * default. The setopt call will do nothing. + * + * @param encoder The encoder handle, must not be NULL + * @param option The option to set + * @param value Pointer to the value to set (type depends on the option) + * + * @ingroup key + */ +void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value); + +/** + * Encode a key event into a terminal escape sequence. + * + * Converts a key event into the appropriate terminal escape sequence based on + * the encoder's current options. The sequence is written to the provided buffer. + * + * Not all key events produce output. For example, unmodified modifier keys + * typically don't generate escape sequences. Check the out_len parameter to + * determine if any data was written. + * + * If the output buffer is too small, this function returns GHOSTTY_OUT_OF_MEMORY + * and out_len will contain the required buffer size. The caller can then + * allocate a larger buffer and call the function again. + * + * @param encoder The encoder handle, must not be NULL + * @param event The key event to encode, must not be NULL + * @param out_buf Buffer to write the encoded sequence to + * @param out_buf_size Size of the output buffer in bytes + * @param out_len Pointer to store the number of bytes written (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer too small, or other error code + * + * ## Example: Calculate required buffer size + * + * @code{.c} + * // Query the required size with a NULL buffer (always returns OUT_OF_MEMORY) + * size_t required = 0; + * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, NULL, 0, &required); + * assert(result == GHOSTTY_OUT_OF_MEMORY); + * + * // Allocate buffer of required size + * char *buf = malloc(required); + * + * // Encode with properly sized buffer + * size_t written = 0; + * result = ghostty_key_encoder_encode(encoder, event, buf, required, &written); + * assert(result == GHOSTTY_SUCCESS); + * + * // Use the encoded sequence... + * + * free(buf); + * @endcode + * + * ## Example: Direct encoding with static buffer + * + * @code{.c} + * // Most escape sequences are short, so a static buffer often suffices + * char buf[128]; + * size_t written = 0; + * GhosttyResult result = ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * + * if (result == GHOSTTY_SUCCESS) { + * // Write the encoded sequence to the terminal + * write(pty_fd, buf, written); + * } else if (result == GHOSTTY_OUT_OF_MEMORY) { + * // Buffer too small, written contains required size + * char *dynamic_buf = malloc(written); + * result = ghostty_key_encoder_encode(encoder, event, dynamic_buf, written, &written); + * assert(result == GHOSTTY_SUCCESS); + * write(pty_fd, dynamic_buf, written); + * free(dynamic_buf); + * } + * @endcode + * + * @ingroup key + */ +GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len); + +#endif /* GHOSTTY_VT_KEY_ENCODER_H */ diff --git a/crates/ghostty-vt-capi/include/ghostty/vt/key/event.h b/crates/ghostty-vt-capi/include/ghostty/vt/key/event.h new file mode 100644 index 0000000..dbd2e9f --- /dev/null +++ b/crates/ghostty-vt-capi/include/ghostty/vt/key/event.h @@ -0,0 +1,474 @@ +/** + * @file event.h + * + * Key event representation and manipulation. + */ + +#ifndef GHOSTTY_VT_KEY_EVENT_H +#define GHOSTTY_VT_KEY_EVENT_H + +#include +#include +#include +#include +#include + +/** + * Opaque handle to a key event. + * + * This handle represents a keyboard input event containing information about + * the physical key pressed, modifiers, and generated text. + * + * @ingroup key + */ +typedef struct GhosttyKeyEvent *GhosttyKeyEvent; + +/** + * Keyboard input event types. + * + * @ingroup key + */ +typedef enum { + /** Key was released */ + GHOSTTY_KEY_ACTION_RELEASE = 0, + /** Key was pressed */ + GHOSTTY_KEY_ACTION_PRESS = 1, + /** Key is being repeated (held down) */ + GHOSTTY_KEY_ACTION_REPEAT = 2, +} GhosttyKeyAction; + +/** + * Keyboard modifier keys bitmask. + * + * A bitmask representing all keyboard modifiers. This tracks which modifier keys + * are pressed and, where supported by the platform, which side (left or right) + * of each modifier is active. + * + * Use the GHOSTTY_MODS_* constants to test and set individual modifiers. + * + * Modifier side bits are only meaningful when the corresponding modifier bit is set. + * Not all platforms support distinguishing between left and right modifier + * keys and Ghostty is built to expect that some platforms may not provide this + * information. + * + * @ingroup key + */ +typedef uint16_t GhosttyMods; + +/** Shift key is pressed */ +#define GHOSTTY_MODS_SHIFT (1 << 0) +/** Control key is pressed */ +#define GHOSTTY_MODS_CTRL (1 << 1) +/** Alt/Option key is pressed */ +#define GHOSTTY_MODS_ALT (1 << 2) +/** Super/Command/Windows key is pressed */ +#define GHOSTTY_MODS_SUPER (1 << 3) +/** Caps Lock is active */ +#define GHOSTTY_MODS_CAPS_LOCK (1 << 4) +/** Num Lock is active */ +#define GHOSTTY_MODS_NUM_LOCK (1 << 5) + +/** + * Right shift is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_SHIFT is set. + */ +#define GHOSTTY_MODS_SHIFT_SIDE (1 << 6) +/** + * Right ctrl is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_CTRL is set. + */ +#define GHOSTTY_MODS_CTRL_SIDE (1 << 7) +/** + * Right alt is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_ALT is set. + */ +#define GHOSTTY_MODS_ALT_SIDE (1 << 8) +/** + * Right super is pressed (0 = left, 1 = right). + * Only meaningful when GHOSTTY_MODS_SUPER is set. + */ +#define GHOSTTY_MODS_SUPER_SIDE (1 << 9) + +/** + * Physical key codes. + * + * The set of key codes that Ghostty is aware of. These represent physical keys + * on the keyboard and are layout-independent. For example, the "a" key on a US + * keyboard is the same as the "ф" key on a Russian keyboard, but both will + * report the same key_a value. + * + * Layout-dependent strings are provided separately as UTF-8 text and are produced + * by the platform. These values are based on the W3C UI Events KeyboardEvent code + * standard. See: https://www.w3.org/TR/uievents-code + * + * @ingroup key + */ +typedef enum { + GHOSTTY_KEY_UNIDENTIFIED = 0, + + // Writing System Keys (W3C § 3.1.1) + GHOSTTY_KEY_BACKQUOTE, + GHOSTTY_KEY_BACKSLASH, + GHOSTTY_KEY_BRACKET_LEFT, + GHOSTTY_KEY_BRACKET_RIGHT, + GHOSTTY_KEY_COMMA, + GHOSTTY_KEY_DIGIT_0, + GHOSTTY_KEY_DIGIT_1, + GHOSTTY_KEY_DIGIT_2, + GHOSTTY_KEY_DIGIT_3, + GHOSTTY_KEY_DIGIT_4, + GHOSTTY_KEY_DIGIT_5, + GHOSTTY_KEY_DIGIT_6, + GHOSTTY_KEY_DIGIT_7, + GHOSTTY_KEY_DIGIT_8, + GHOSTTY_KEY_DIGIT_9, + GHOSTTY_KEY_EQUAL, + GHOSTTY_KEY_INTL_BACKSLASH, + GHOSTTY_KEY_INTL_RO, + GHOSTTY_KEY_INTL_YEN, + GHOSTTY_KEY_A, + GHOSTTY_KEY_B, + GHOSTTY_KEY_C, + GHOSTTY_KEY_D, + GHOSTTY_KEY_E, + GHOSTTY_KEY_F, + GHOSTTY_KEY_G, + GHOSTTY_KEY_H, + GHOSTTY_KEY_I, + GHOSTTY_KEY_J, + GHOSTTY_KEY_K, + GHOSTTY_KEY_L, + GHOSTTY_KEY_M, + GHOSTTY_KEY_N, + GHOSTTY_KEY_O, + GHOSTTY_KEY_P, + GHOSTTY_KEY_Q, + GHOSTTY_KEY_R, + GHOSTTY_KEY_S, + GHOSTTY_KEY_T, + GHOSTTY_KEY_U, + GHOSTTY_KEY_V, + GHOSTTY_KEY_W, + GHOSTTY_KEY_X, + GHOSTTY_KEY_Y, + GHOSTTY_KEY_Z, + GHOSTTY_KEY_MINUS, + GHOSTTY_KEY_PERIOD, + GHOSTTY_KEY_QUOTE, + GHOSTTY_KEY_SEMICOLON, + GHOSTTY_KEY_SLASH, + + // Functional Keys (W3C § 3.1.2) + GHOSTTY_KEY_ALT_LEFT, + GHOSTTY_KEY_ALT_RIGHT, + GHOSTTY_KEY_BACKSPACE, + GHOSTTY_KEY_CAPS_LOCK, + GHOSTTY_KEY_CONTEXT_MENU, + GHOSTTY_KEY_CONTROL_LEFT, + GHOSTTY_KEY_CONTROL_RIGHT, + GHOSTTY_KEY_ENTER, + GHOSTTY_KEY_META_LEFT, + GHOSTTY_KEY_META_RIGHT, + GHOSTTY_KEY_SHIFT_LEFT, + GHOSTTY_KEY_SHIFT_RIGHT, + GHOSTTY_KEY_SPACE, + GHOSTTY_KEY_TAB, + GHOSTTY_KEY_CONVERT, + GHOSTTY_KEY_KANA_MODE, + GHOSTTY_KEY_NON_CONVERT, + + // Control Pad Section (W3C § 3.2) + GHOSTTY_KEY_DELETE, + GHOSTTY_KEY_END, + GHOSTTY_KEY_HELP, + GHOSTTY_KEY_HOME, + GHOSTTY_KEY_INSERT, + GHOSTTY_KEY_PAGE_DOWN, + GHOSTTY_KEY_PAGE_UP, + + // Arrow Pad Section (W3C § 3.3) + GHOSTTY_KEY_ARROW_DOWN, + GHOSTTY_KEY_ARROW_LEFT, + GHOSTTY_KEY_ARROW_RIGHT, + GHOSTTY_KEY_ARROW_UP, + + // Numpad Section (W3C § 3.4) + GHOSTTY_KEY_NUM_LOCK, + GHOSTTY_KEY_NUMPAD_0, + GHOSTTY_KEY_NUMPAD_1, + GHOSTTY_KEY_NUMPAD_2, + GHOSTTY_KEY_NUMPAD_3, + GHOSTTY_KEY_NUMPAD_4, + GHOSTTY_KEY_NUMPAD_5, + GHOSTTY_KEY_NUMPAD_6, + GHOSTTY_KEY_NUMPAD_7, + GHOSTTY_KEY_NUMPAD_8, + GHOSTTY_KEY_NUMPAD_9, + GHOSTTY_KEY_NUMPAD_ADD, + GHOSTTY_KEY_NUMPAD_BACKSPACE, + GHOSTTY_KEY_NUMPAD_CLEAR, + GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY, + GHOSTTY_KEY_NUMPAD_COMMA, + GHOSTTY_KEY_NUMPAD_DECIMAL, + GHOSTTY_KEY_NUMPAD_DIVIDE, + GHOSTTY_KEY_NUMPAD_ENTER, + GHOSTTY_KEY_NUMPAD_EQUAL, + GHOSTTY_KEY_NUMPAD_MEMORY_ADD, + GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR, + GHOSTTY_KEY_NUMPAD_MEMORY_RECALL, + GHOSTTY_KEY_NUMPAD_MEMORY_STORE, + GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT, + GHOSTTY_KEY_NUMPAD_MULTIPLY, + GHOSTTY_KEY_NUMPAD_PAREN_LEFT, + GHOSTTY_KEY_NUMPAD_PAREN_RIGHT, + GHOSTTY_KEY_NUMPAD_SUBTRACT, + GHOSTTY_KEY_NUMPAD_SEPARATOR, + GHOSTTY_KEY_NUMPAD_UP, + GHOSTTY_KEY_NUMPAD_DOWN, + GHOSTTY_KEY_NUMPAD_RIGHT, + GHOSTTY_KEY_NUMPAD_LEFT, + GHOSTTY_KEY_NUMPAD_BEGIN, + GHOSTTY_KEY_NUMPAD_HOME, + GHOSTTY_KEY_NUMPAD_END, + GHOSTTY_KEY_NUMPAD_INSERT, + GHOSTTY_KEY_NUMPAD_DELETE, + GHOSTTY_KEY_NUMPAD_PAGE_UP, + GHOSTTY_KEY_NUMPAD_PAGE_DOWN, + + // Function Section (W3C § 3.5) + GHOSTTY_KEY_ESCAPE, + GHOSTTY_KEY_F1, + GHOSTTY_KEY_F2, + GHOSTTY_KEY_F3, + GHOSTTY_KEY_F4, + GHOSTTY_KEY_F5, + GHOSTTY_KEY_F6, + GHOSTTY_KEY_F7, + GHOSTTY_KEY_F8, + GHOSTTY_KEY_F9, + GHOSTTY_KEY_F10, + GHOSTTY_KEY_F11, + GHOSTTY_KEY_F12, + GHOSTTY_KEY_F13, + GHOSTTY_KEY_F14, + GHOSTTY_KEY_F15, + GHOSTTY_KEY_F16, + GHOSTTY_KEY_F17, + GHOSTTY_KEY_F18, + GHOSTTY_KEY_F19, + GHOSTTY_KEY_F20, + GHOSTTY_KEY_F21, + GHOSTTY_KEY_F22, + GHOSTTY_KEY_F23, + GHOSTTY_KEY_F24, + GHOSTTY_KEY_F25, + GHOSTTY_KEY_FN, + GHOSTTY_KEY_FN_LOCK, + GHOSTTY_KEY_PRINT_SCREEN, + GHOSTTY_KEY_SCROLL_LOCK, + GHOSTTY_KEY_PAUSE, + + // Media Keys (W3C § 3.6) + GHOSTTY_KEY_BROWSER_BACK, + GHOSTTY_KEY_BROWSER_FAVORITES, + GHOSTTY_KEY_BROWSER_FORWARD, + GHOSTTY_KEY_BROWSER_HOME, + GHOSTTY_KEY_BROWSER_REFRESH, + GHOSTTY_KEY_BROWSER_SEARCH, + GHOSTTY_KEY_BROWSER_STOP, + GHOSTTY_KEY_EJECT, + GHOSTTY_KEY_LAUNCH_APP_1, + GHOSTTY_KEY_LAUNCH_APP_2, + GHOSTTY_KEY_LAUNCH_MAIL, + GHOSTTY_KEY_MEDIA_PLAY_PAUSE, + GHOSTTY_KEY_MEDIA_SELECT, + GHOSTTY_KEY_MEDIA_STOP, + GHOSTTY_KEY_MEDIA_TRACK_NEXT, + GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS, + GHOSTTY_KEY_POWER, + GHOSTTY_KEY_SLEEP, + GHOSTTY_KEY_AUDIO_VOLUME_DOWN, + GHOSTTY_KEY_AUDIO_VOLUME_MUTE, + GHOSTTY_KEY_AUDIO_VOLUME_UP, + GHOSTTY_KEY_WAKE_UP, + + // Legacy, Non-standard, and Special Keys (W3C § 3.7) + GHOSTTY_KEY_COPY, + GHOSTTY_KEY_CUT, + GHOSTTY_KEY_PASTE, +} GhosttyKey; + +/** + * Create a new key event instance. + * + * Creates a new key event with default values. The event must be freed using + * ghostty_key_event_free() when no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param event Pointer to store the created key event handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup key + */ +GhosttyResult ghostty_key_event_new(const GhosttyAllocator *allocator, GhosttyKeyEvent *event); + +/** + * Free a key event instance. + * + * Releases all resources associated with the key event. After this call, + * the event handle becomes invalid and must not be used. + * + * @param event The key event handle to free (may be NULL) + * + * @ingroup key + */ +void ghostty_key_event_free(GhosttyKeyEvent event); + +/** + * Set the key action (press, release, repeat). + * + * @param event The key event handle, must not be NULL + * @param action The action to set + * + * @ingroup key + */ +void ghostty_key_event_set_action(GhosttyKeyEvent event, GhosttyKeyAction action); + +/** + * Get the key action (press, release, repeat). + * + * @param event The key event handle, must not be NULL + * @return The key action + * + * @ingroup key + */ +GhosttyKeyAction ghostty_key_event_get_action(GhosttyKeyEvent event); + +/** + * Set the physical key code. + * + * @param event The key event handle, must not be NULL + * @param key The physical key code to set + * + * @ingroup key + */ +void ghostty_key_event_set_key(GhosttyKeyEvent event, GhosttyKey key); + +/** + * Get the physical key code. + * + * @param event The key event handle, must not be NULL + * @return The physical key code + * + * @ingroup key + */ +GhosttyKey ghostty_key_event_get_key(GhosttyKeyEvent event); + +/** + * Set the modifier keys bitmask. + * + * @param event The key event handle, must not be NULL + * @param mods The modifier keys bitmask to set + * + * @ingroup key + */ +void ghostty_key_event_set_mods(GhosttyKeyEvent event, GhosttyMods mods); + +/** + * Get the modifier keys bitmask. + * + * @param event The key event handle, must not be NULL + * @return The modifier keys bitmask + * + * @ingroup key + */ +GhosttyMods ghostty_key_event_get_mods(GhosttyKeyEvent event); + +/** + * Set the consumed modifiers bitmask. + * + * @param event The key event handle, must not be NULL + * @param consumed_mods The consumed modifiers bitmask to set + * + * @ingroup key + */ +void ghostty_key_event_set_consumed_mods(GhosttyKeyEvent event, GhosttyMods consumed_mods); + +/** + * Get the consumed modifiers bitmask. + * + * @param event The key event handle, must not be NULL + * @return The consumed modifiers bitmask + * + * @ingroup key + */ +GhosttyMods ghostty_key_event_get_consumed_mods(GhosttyKeyEvent event); + +/** + * Set whether the key event is part of a composition sequence. + * + * @param event The key event handle, must not be NULL + * @param composing Whether the key event is part of a composition sequence + * + * @ingroup key + */ +void ghostty_key_event_set_composing(GhosttyKeyEvent event, bool composing); + +/** + * Get whether the key event is part of a composition sequence. + * + * @param event The key event handle, must not be NULL + * @return Whether the key event is part of a composition sequence + * + * @ingroup key + */ +bool ghostty_key_event_get_composing(GhosttyKeyEvent event); + +/** + * Set the UTF-8 text generated by the key event. + * + * The key event does NOT take ownership of the text pointer. The caller + * must ensure the string remains valid for the lifetime needed by the event. + * + * @param event The key event handle, must not be NULL + * @param utf8 The UTF-8 text to set (or NULL for empty) + * @param len Length of the UTF-8 text in bytes + * + * @ingroup key + */ +void ghostty_key_event_set_utf8(GhosttyKeyEvent event, const char *utf8, size_t len); + +/** + * Get the UTF-8 text generated by the key event. + * + * The returned pointer is valid until the event is freed or the UTF-8 text is modified. + * + * @param event The key event handle, must not be NULL + * @param len Pointer to store the length of the UTF-8 text in bytes (may be NULL) + * @return The UTF-8 text (or NULL for empty) + * + * @ingroup key + */ +const char *ghostty_key_event_get_utf8(GhosttyKeyEvent event, size_t *len); + +/** + * Set the unshifted Unicode codepoint. + * + * @param event The key event handle, must not be NULL + * @param codepoint The unshifted Unicode codepoint to set + * + * @ingroup key + */ +void ghostty_key_event_set_unshifted_codepoint(GhosttyKeyEvent event, uint32_t codepoint); + +/** + * Get the unshifted Unicode codepoint. + * + * @param event The key event handle, must not be NULL + * @return The unshifted Unicode codepoint + * + * @ingroup key + */ +uint32_t ghostty_key_event_get_unshifted_codepoint(GhosttyKeyEvent event); + +#endif /* GHOSTTY_VT_KEY_EVENT_H */ diff --git a/crates/ghostty-vt-capi/include/ghostty/vt/osc.h b/crates/ghostty-vt-capi/include/ghostty/vt/osc.h new file mode 100644 index 0000000..f53077a --- /dev/null +++ b/crates/ghostty-vt-capi/include/ghostty/vt/osc.h @@ -0,0 +1,233 @@ +/** + * @file osc.h + * + * OSC (Operating System Command) sequence parser and command handling. + */ + +#ifndef GHOSTTY_VT_OSC_H +#define GHOSTTY_VT_OSC_H + +#include +#include +#include +#include +#include + +/** + * Opaque handle to an OSC parser instance. + * + * This handle represents an OSC (Operating System Command) parser that can + * be used to parse the contents of OSC sequences. + * + * @ingroup osc + */ +typedef struct GhosttyOscParser *GhosttyOscParser; + +/** + * Opaque handle to a single OSC command. + * + * This handle represents a parsed OSC (Operating System Command) command. + * The command can be queried for its type and associated data. + * + * @ingroup osc + */ +typedef struct GhosttyOscCommand *GhosttyOscCommand; + +/** @defgroup osc OSC Parser + * + * OSC (Operating System Command) sequence parser and command handling. + * + * The parser operates in a streaming fashion, processing input byte-by-byte + * to handle OSC sequences that may arrive in fragments across multiple reads. + * This interface makes it easy to integrate into most environments and avoids + * over-allocating buffers. + * + * ## Basic Usage + * + * 1. Create a parser instance with ghostty_osc_new() + * 2. Feed bytes to the parser using ghostty_osc_next() + * 3. Finalize parsing with ghostty_osc_end() to get the command + * 4. Query command type and extract data using ghostty_osc_command_type() + * and ghostty_osc_command_data() + * 5. Free the parser with ghostty_osc_free() when done + * + * @{ + */ + +/** + * OSC command types. + * + * @ingroup osc + */ +typedef enum { + GHOSTTY_OSC_COMMAND_INVALID = 0, + GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE = 1, + GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_ICON = 2, + GHOSTTY_OSC_COMMAND_SEMANTIC_PROMPT = 3, + GHOSTTY_OSC_COMMAND_CLIPBOARD_CONTENTS = 4, + GHOSTTY_OSC_COMMAND_REPORT_PWD = 5, + GHOSTTY_OSC_COMMAND_MOUSE_SHAPE = 6, + GHOSTTY_OSC_COMMAND_COLOR_OPERATION = 7, + GHOSTTY_OSC_COMMAND_KITTY_COLOR_PROTOCOL = 8, + GHOSTTY_OSC_COMMAND_SHOW_DESKTOP_NOTIFICATION = 9, + GHOSTTY_OSC_COMMAND_HYPERLINK_START = 10, + GHOSTTY_OSC_COMMAND_HYPERLINK_END = 11, + GHOSTTY_OSC_COMMAND_CONEMU_SLEEP = 12, + GHOSTTY_OSC_COMMAND_CONEMU_SHOW_MESSAGE_BOX = 13, + GHOSTTY_OSC_COMMAND_CONEMU_CHANGE_TAB_TITLE = 14, + GHOSTTY_OSC_COMMAND_CONEMU_PROGRESS_REPORT = 15, + GHOSTTY_OSC_COMMAND_CONEMU_WAIT_INPUT = 16, + GHOSTTY_OSC_COMMAND_CONEMU_GUIMACRO = 17, + GHOSTTY_OSC_COMMAND_CONEMU_RUN_PROCESS = 18, + GHOSTTY_OSC_COMMAND_CONEMU_OUTPUT_ENVIRONMENT_VARIABLE = 19, + GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20, + GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21, + GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22, +} GhosttyOscCommandType; + +/** + * OSC command data types. + * + * These values specify what type of data to extract from an OSC command + * using `ghostty_osc_command_data`. + * + * @ingroup osc + */ +typedef enum { + /** Invalid data type. Never results in any data extraction. */ + GHOSTTY_OSC_DATA_INVALID = 0, + + /** + * Window title string data. + * + * Valid for: GHOSTTY_OSC_COMMAND_CHANGE_WINDOW_TITLE + * + * Output type: const char ** (pointer to null-terminated string) + * + * Lifetime: Valid until the next call to any ghostty_osc_* function with + * the same parser instance. Memory is owned by the parser. + */ + GHOSTTY_OSC_DATA_CHANGE_WINDOW_TITLE_STR = 1, +} GhosttyOscCommandData; + +/** + * Create a new OSC parser instance. + * + * Creates a new OSC (Operating System Command) parser using the provided + * allocator. The parser must be freed using ghostty_vt_osc_free() when + * no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or NULL to use the default allocator + * @param parser Pointer to store the created parser handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup osc + */ +GhosttyResult ghostty_osc_new(const GhosttyAllocator *allocator, GhosttyOscParser *parser); + +/** + * Free an OSC parser instance. + * + * Releases all resources associated with the OSC parser. After this call, + * the parser handle becomes invalid and must not be used. + * + * @param parser The parser handle to free (may be NULL) + * + * @ingroup osc + */ +void ghostty_osc_free(GhosttyOscParser parser); + +/** + * Reset an OSC parser instance to its initial state. + * + * Resets the parser state, clearing any partially parsed OSC sequences + * and returning the parser to its initial state. This is useful for + * reusing a parser instance or recovering from parse errors. + * + * @param parser The parser handle to reset, must not be null. + * + * @ingroup osc + */ +void ghostty_osc_reset(GhosttyOscParser parser); + +/** + * Parse the next byte in an OSC sequence. + * + * Processes a single byte as part of an OSC sequence. The parser maintains + * internal state to track the progress through the sequence. Call this + * function for each byte in the sequence data. + * + * When finished pumping the parser with bytes, call ghostty_osc_end + * to get the final result. + * + * @param parser The parser handle, must not be null. + * @param byte The next byte to parse + * + * @ingroup osc + */ +void ghostty_osc_next(GhosttyOscParser parser, uint8_t byte); + +/** + * Finalize OSC parsing and retrieve the parsed command. + * + * Call this function after feeding all bytes of an OSC sequence to the parser + * using ghostty_osc_next() with the exception of the terminating character + * (ESC or ST). This function finalizes the parsing process and returns the + * parsed OSC command. + * + * The return value is never NULL. Invalid commands will return a command + * with type GHOSTTY_OSC_COMMAND_INVALID. + * + * The terminator parameter specifies the byte that terminated the OSC sequence + * (typically 0x07 for BEL or 0x5C for ST after ESC). This information is + * preserved in the parsed command so that responses can use the same terminator + * format for better compatibility with the calling program. For commands that + * do not require a response, this parameter is ignored and the resulting + * command will not retain the terminator information. + * + * The returned command handle is valid until the next call to any + * `ghostty_osc_*` function with the same parser instance with the exception + * of command introspection functions such as `ghostty_osc_command_type`. + * + * @param parser The parser handle, must not be null. + * @param terminator The terminating byte of the OSC sequence (0x07 for BEL, 0x5C for ST) + * @return Handle to the parsed OSC command + * + * @ingroup osc + */ +GhosttyOscCommand ghostty_osc_end(GhosttyOscParser parser, uint8_t terminator); + +/** + * Get the type of an OSC command. + * + * Returns the type identifier for the given OSC command. This can be used + * to determine what kind of command was parsed and what data might be + * available from it. + * + * @param command The OSC command handle to query (may be NULL) + * @return The command type, or GHOSTTY_OSC_COMMAND_INVALID if command is NULL + * + * @ingroup osc + */ +GhosttyOscCommandType ghostty_osc_command_type(GhosttyOscCommand command); + +/** + * Extract data from an OSC command. + * + * Extracts typed data from the given OSC command based on the specified + * data type. The output pointer must be of the appropriate type for the + * requested data kind. Valid command types, output types, and memory + * safety information are documented in the `GhosttyOscCommandData` enum. + * + * @param command The OSC command handle to query (may be NULL) + * @param data The type of data to extract + * @param out Pointer to store the extracted data (type depends on data parameter) + * @return true if data extraction was successful, false otherwise + * + * @ingroup osc + */ +bool ghostty_osc_command_data(GhosttyOscCommand command, GhosttyOscCommandData data, void *out); + +/** @} */ + +#endif /* GHOSTTY_VT_OSC_H */ diff --git a/crates/ghostty-vt-capi/include/ghostty/vt/paste.h b/crates/ghostty-vt-capi/include/ghostty/vt/paste.h new file mode 100644 index 0000000..d90f303 --- /dev/null +++ b/crates/ghostty-vt-capi/include/ghostty/vt/paste.h @@ -0,0 +1,75 @@ +/** + * @file paste.h + * + * Paste utilities - validate and encode paste data for terminal input. + */ + +#ifndef GHOSTTY_VT_PASTE_H +#define GHOSTTY_VT_PASTE_H + +/** @defgroup paste Paste Utilities + * + * Utilities for validating paste data safety. + * + * ## Basic Usage + * + * Use ghostty_paste_is_safe() to check if paste data contains potentially + * dangerous sequences before sending it to the terminal. + * + * ## Example + * + * @code{.c} + * #include + * #include + * #include + * + * int main() { + * const char* safe_data = "hello world"; + * const char* unsafe_data = "rm -rf /\n"; + * + * if (ghostty_paste_is_safe(safe_data, strlen(safe_data))) { + * printf("Safe to paste\n"); + * } + * + * if (!ghostty_paste_is_safe(unsafe_data, strlen(unsafe_data))) { + * printf("Unsafe! Contains newline\n"); + * } + * + * return 0; + * } + * @endcode + * + * @{ + */ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Check if paste data is safe to paste into the terminal. + * + * Data is considered unsafe if it contains: + * - Newlines (`\n`) which can inject commands + * - The bracketed paste end sequence (`\x1b[201~`) which can be used + * to exit bracketed paste mode and inject commands + * + * This check is conservative and considers data unsafe regardless of + * current terminal state. + * + * @param data The paste data to check (must not be NULL) + * @param len The length of the data in bytes + * @return true if the data is safe to paste, false otherwise + */ +bool ghostty_paste_is_safe(const char* data, size_t len); + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_PASTE_H */ diff --git a/crates/ghostty-vt-capi/include/ghostty/vt/result.h b/crates/ghostty-vt-capi/include/ghostty/vt/result.h new file mode 100644 index 0000000..65938ee --- /dev/null +++ b/crates/ghostty-vt-capi/include/ghostty/vt/result.h @@ -0,0 +1,22 @@ +/** + * @file result.h + * + * Result codes for libghostty-vt operations. + */ + +#ifndef GHOSTTY_VT_RESULT_H +#define GHOSTTY_VT_RESULT_H + +/** + * Result codes for libghostty-vt operations. + */ +typedef enum { + /** Operation completed successfully */ + GHOSTTY_SUCCESS = 0, + /** Operation failed due to failed allocation */ + GHOSTTY_OUT_OF_MEMORY = -1, + /** Operation failed due to invalid value */ + GHOSTTY_INVALID_VALUE = -2, +} GhosttyResult; + +#endif /* GHOSTTY_VT_RESULT_H */ diff --git a/crates/ghostty-vt-capi/include/ghostty/vt/sgr.h b/crates/ghostty-vt-capi/include/ghostty/vt/sgr.h new file mode 100644 index 0000000..5aaa368 --- /dev/null +++ b/crates/ghostty-vt-capi/include/ghostty/vt/sgr.h @@ -0,0 +1,393 @@ +/** + * @file sgr.h + * + * SGR (Select Graphic Rendition) attribute parsing and handling. + */ + +#ifndef GHOSTTY_VT_SGR_H +#define GHOSTTY_VT_SGR_H + +/** @defgroup sgr SGR Parser + * + * SGR (Select Graphic Rendition) attribute parser. + * + * SGR sequences are the syntax used to set styling attributes such as + * bold, italic, underline, and colors for text in terminal emulators. + * For example, you may be familiar with sequences like `ESC[1;31m`. The + * `1;31` is the SGR attribute list. + * + * The parser processes SGR parameters from CSI sequences (e.g., `ESC[1;31m`) + * and returns individual text attributes like bold, italic, colors, etc. + * It supports both semicolon (`;`) and colon (`:`) separators, possibly mixed, + * and handles various color formats including 8-color, 16-color, 256-color, + * X11 named colors, and RGB in multiple formats. + * + * ## Basic Usage + * + * 1. Create a parser instance with ghostty_sgr_new() + * 2. Set SGR parameters with ghostty_sgr_set_params() + * 3. Iterate through attributes using ghostty_sgr_next() + * 4. Free the parser with ghostty_sgr_free() when done + * + * ## Example + * + * @code{.c} + * #include + * #include + * #include + * + * int main() { + * // Create parser + * GhosttySgrParser parser; + * GhosttyResult result = ghostty_sgr_new(NULL, &parser); + * assert(result == GHOSTTY_SUCCESS); + * + * // Parse "bold, red foreground" sequence: ESC[1;31m + * uint16_t params[] = {1, 31}; + * result = ghostty_sgr_set_params(parser, params, NULL, 2); + * assert(result == GHOSTTY_SUCCESS); + * + * // Iterate through attributes + * GhosttySgrAttribute attr; + * while (ghostty_sgr_next(parser, &attr)) { + * switch (attr.tag) { + * case GHOSTTY_SGR_ATTR_BOLD: + * printf("Bold enabled\n"); + * break; + * case GHOSTTY_SGR_ATTR_FG_8: + * printf("Foreground color: %d\n", attr.value.fg_8); + * break; + * default: + * break; + * } + * } + * + * // Cleanup + * ghostty_sgr_free(parser); + * return 0; + * } + * @endcode + * + * @{ + */ + +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Opaque handle to an SGR parser instance. + * + * This handle represents an SGR (Select Graphic Rendition) parser that can + * be used to parse SGR sequences and extract individual text attributes. + * + * @ingroup sgr + */ +typedef struct GhosttySgrParser* GhosttySgrParser; + +/** + * SGR attribute tags. + * + * These values identify the type of an SGR attribute in a tagged union. + * Use the tag to determine which field in the attribute value union to access. + * + * @ingroup sgr + */ +typedef enum { + GHOSTTY_SGR_ATTR_UNSET = 0, + GHOSTTY_SGR_ATTR_UNKNOWN = 1, + GHOSTTY_SGR_ATTR_BOLD = 2, + GHOSTTY_SGR_ATTR_RESET_BOLD = 3, + GHOSTTY_SGR_ATTR_ITALIC = 4, + GHOSTTY_SGR_ATTR_RESET_ITALIC = 5, + GHOSTTY_SGR_ATTR_FAINT = 6, + GHOSTTY_SGR_ATTR_UNDERLINE = 7, + GHOSTTY_SGR_ATTR_UNDERLINE_COLOR = 8, + GHOSTTY_SGR_ATTR_UNDERLINE_COLOR_256 = 9, + GHOSTTY_SGR_ATTR_RESET_UNDERLINE_COLOR = 10, + GHOSTTY_SGR_ATTR_OVERLINE = 11, + GHOSTTY_SGR_ATTR_RESET_OVERLINE = 12, + GHOSTTY_SGR_ATTR_BLINK = 13, + GHOSTTY_SGR_ATTR_RESET_BLINK = 14, + GHOSTTY_SGR_ATTR_INVERSE = 15, + GHOSTTY_SGR_ATTR_RESET_INVERSE = 16, + GHOSTTY_SGR_ATTR_INVISIBLE = 17, + GHOSTTY_SGR_ATTR_RESET_INVISIBLE = 18, + GHOSTTY_SGR_ATTR_STRIKETHROUGH = 19, + GHOSTTY_SGR_ATTR_RESET_STRIKETHROUGH = 20, + GHOSTTY_SGR_ATTR_DIRECT_COLOR_FG = 21, + GHOSTTY_SGR_ATTR_DIRECT_COLOR_BG = 22, + GHOSTTY_SGR_ATTR_BG_8 = 23, + GHOSTTY_SGR_ATTR_FG_8 = 24, + GHOSTTY_SGR_ATTR_RESET_FG = 25, + GHOSTTY_SGR_ATTR_RESET_BG = 26, + GHOSTTY_SGR_ATTR_BRIGHT_BG_8 = 27, + GHOSTTY_SGR_ATTR_BRIGHT_FG_8 = 28, + GHOSTTY_SGR_ATTR_BG_256 = 29, + GHOSTTY_SGR_ATTR_FG_256 = 30, +} GhosttySgrAttributeTag; + +/** + * Underline style types. + * + * @ingroup sgr + */ +typedef enum { + GHOSTTY_SGR_UNDERLINE_NONE = 0, + GHOSTTY_SGR_UNDERLINE_SINGLE = 1, + GHOSTTY_SGR_UNDERLINE_DOUBLE = 2, + GHOSTTY_SGR_UNDERLINE_CURLY = 3, + GHOSTTY_SGR_UNDERLINE_DOTTED = 4, + GHOSTTY_SGR_UNDERLINE_DASHED = 5, +} GhosttySgrUnderline; + +/** + * Unknown SGR attribute data. + * + * Contains the full parameter list and the partial list where parsing + * encountered an unknown or invalid sequence. + * + * @ingroup sgr + */ +typedef struct { + const uint16_t* full_ptr; + size_t full_len; + const uint16_t* partial_ptr; + size_t partial_len; +} GhosttySgrUnknown; + +/** + * SGR attribute value union. + * + * This union contains all possible attribute values. Use the tag field + * to determine which union member is active. Attributes without associated + * data (like bold, italic) don't use the union value. + * + * @ingroup sgr + */ +typedef union { + GhosttySgrUnknown unknown; + GhosttySgrUnderline underline; + GhosttyColorRgb underline_color; + GhosttyColorPaletteIndex underline_color_256; + GhosttyColorRgb direct_color_fg; + GhosttyColorRgb direct_color_bg; + GhosttyColorPaletteIndex bg_8; + GhosttyColorPaletteIndex fg_8; + GhosttyColorPaletteIndex bright_bg_8; + GhosttyColorPaletteIndex bright_fg_8; + GhosttyColorPaletteIndex bg_256; + GhosttyColorPaletteIndex fg_256; + uint64_t _padding[8]; +} GhosttySgrAttributeValue; + +/** + * SGR attribute (tagged union). + * + * A complete SGR attribute with both its type tag and associated value. + * Always check the tag field to determine which value union member is valid. + * + * Attributes without associated data (e.g., GHOSTTY_SGR_ATTR_BOLD) can be + * identified by tag alone; the value union is not used for these and + * the memory in the value field is undefined. + * + * @ingroup sgr + */ +typedef struct { + GhosttySgrAttributeTag tag; + GhosttySgrAttributeValue value; +} GhosttySgrAttribute; + +/** + * Create a new SGR parser instance. + * + * Creates a new SGR (Select Graphic Rendition) parser using the provided + * allocator. The parser must be freed using ghostty_sgr_free() when + * no longer needed. + * + * @param allocator Pointer to the allocator to use for memory management, or + * NULL to use the default allocator + * @param parser Pointer to store the created parser handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup sgr + */ +GhosttyResult ghostty_sgr_new(const GhosttyAllocator* allocator, + GhosttySgrParser* parser); + +/** + * Free an SGR parser instance. + * + * Releases all resources associated with the SGR parser. After this call, + * the parser handle becomes invalid and must not be used. This includes + * any attributes previously returned by ghostty_sgr_next(). + * + * @param parser The parser handle to free (may be NULL) + * + * @ingroup sgr + */ +void ghostty_sgr_free(GhosttySgrParser parser); + +/** + * Reset an SGR parser instance to the beginning of the parameter list. + * + * Resets the parser's iteration state without clearing the parameters. + * After calling this, ghostty_sgr_next() will start from the beginning + * of the parameter list again. + * + * @param parser The parser handle to reset, must not be NULL + * + * @ingroup sgr + */ +void ghostty_sgr_reset(GhosttySgrParser parser); + +/** + * Set SGR parameters for parsing. + * + * Sets the SGR parameter list to parse. Parameters are the numeric values + * from a CSI SGR sequence (e.g., for `ESC[1;31m`, params would be {1, 31}). + * + * The separators array optionally specifies the separator type for each + * parameter position. Each byte should be either ';' for semicolon or ':' + * for colon. This is needed for certain color formats that use colon + * separators (e.g., `ESC[4:3m` for curly underline). Any invalid separator + * values are treated as semicolons. The separators array must have the same + * length as the params array, if it is not NULL. + * + * If separators is NULL, all parameters are assumed to be semicolon-separated. + * + * This function makes an internal copy of the parameter and separator data, + * so the caller can safely free or modify the input arrays after this call. + * + * After calling this function, the parser is automatically reset and ready + * to iterate from the beginning. + * + * @param parser The parser handle, must not be NULL + * @param params Array of SGR parameter values + * @param separators Optional array of separator characters (';' or ':'), or + * NULL + * @param len Number of parameters (and separators if provided) + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup sgr + */ +GhosttyResult ghostty_sgr_set_params(GhosttySgrParser parser, + const uint16_t* params, + const char* separators, + size_t len); + +/** + * Get the next SGR attribute. + * + * Parses and returns the next attribute from the parameter list. + * Call this function repeatedly until it returns false to process + * all attributes in the sequence. + * + * @param parser The parser handle, must not be NULL + * @param attr Pointer to store the next attribute + * @return true if an attribute was returned, false if no more attributes + * + * @ingroup sgr + */ +bool ghostty_sgr_next(GhosttySgrParser parser, GhosttySgrAttribute* attr); + +/** + * Get the full parameter list from an unknown SGR attribute. + * + * This function retrieves the full parameter list that was provided to the + * parser when an unknown attribute was encountered. Primarily useful in + * WebAssembly environments where accessing struct fields directly is difficult. + * + * @param unknown The unknown attribute data + * @param ptr Pointer to store the pointer to the parameter array (may be NULL) + * @return The length of the full parameter array + * + * @ingroup sgr + */ +size_t ghostty_sgr_unknown_full(GhosttySgrUnknown unknown, + const uint16_t** ptr); + +/** + * Get the partial parameter list from an unknown SGR attribute. + * + * This function retrieves the partial parameter list where parsing stopped + * when an unknown attribute was encountered. Primarily useful in WebAssembly + * environments where accessing struct fields directly is difficult. + * + * @param unknown The unknown attribute data + * @param ptr Pointer to store the pointer to the parameter array (may be NULL) + * @return The length of the partial parameter array + * + * @ingroup sgr + */ +size_t ghostty_sgr_unknown_partial(GhosttySgrUnknown unknown, + const uint16_t** ptr); + +/** + * Get the tag from an SGR attribute. + * + * This function extracts the tag that identifies which type of attribute + * this is. Primarily useful in WebAssembly environments where accessing + * struct fields directly is difficult. + * + * @param attr The SGR attribute + * @return The attribute tag + * + * @ingroup sgr + */ +GhosttySgrAttributeTag ghostty_sgr_attribute_tag(GhosttySgrAttribute attr); + +/** + * Get the value from an SGR attribute. + * + * This function returns a pointer to the value union from an SGR attribute. Use + * the tag to determine which field of the union is valid. Primarily useful in + * WebAssembly environments where accessing struct fields directly is difficult. + * + * @param attr Pointer to the SGR attribute + * @return Pointer to the attribute value union + * + * @ingroup sgr + */ +GhosttySgrAttributeValue* ghostty_sgr_attribute_value( + GhosttySgrAttribute* attr); + +#ifdef __wasm__ +/** + * Allocate memory for an SGR attribute (WebAssembly only). + * + * This is a convenience function for WebAssembly environments to allocate + * memory for an SGR attribute structure that can be passed to ghostty_sgr_next. + * + * @return Pointer to the allocated attribute structure + * + * @ingroup wasm + */ +GhosttySgrAttribute* ghostty_wasm_alloc_sgr_attribute(void); + +/** + * Free memory for an SGR attribute (WebAssembly only). + * + * Frees memory allocated by ghostty_wasm_alloc_sgr_attribute. + * + * @param attr Pointer to the attribute structure to free + * + * @ingroup wasm + */ +void ghostty_wasm_free_sgr_attribute(GhosttySgrAttribute* attr); +#endif + +#ifdef __cplusplus +} +#endif + +/** @} */ + +#endif /* GHOSTTY_VT_SGR_H */ diff --git a/crates/ghostty-vt-capi/include/ghostty/vt/wasm.h b/crates/ghostty-vt-capi/include/ghostty/vt/wasm.h new file mode 100644 index 0000000..37a8263 --- /dev/null +++ b/crates/ghostty-vt-capi/include/ghostty/vt/wasm.h @@ -0,0 +1,159 @@ +/** + * @file wasm.h + * + * WebAssembly utility functions for libghostty-vt. + */ + +#ifndef GHOSTTY_VT_WASM_H +#define GHOSTTY_VT_WASM_H + +#ifdef __wasm__ + +#include +#include + +/** @defgroup wasm WebAssembly Utilities + * + * Convenience functions for allocating various types in WebAssembly builds. + * **These are only available the libghostty-vt wasm module.** + * + * Ghostty relies on pointers to various types for ABI compatibility, and + * creating those pointers in Wasm can be tedious. These functions provide + * a purely additive set of utilities that simplify memory management in + * Wasm environments without changing the core C library API. + * + * @note These functions always use the default allocator. If you need + * custom allocation strategies, you should allocate types manually using + * your custom allocator. This is a very rare use case in the WebAssembly + * world so these are optimized for simplicity. + * + * ## Example Usage + * + * Here's a simple example of using the Wasm utilities with the key encoder: + * + * @code + * const { exports } = wasmInstance; + * const view = new DataView(wasmMemory.buffer); + * + * // Create key encoder + * const encoderPtr = exports.ghostty_wasm_alloc_opaque(); + * exports.ghostty_key_encoder_new(null, encoderPtr); + * const encoder = view.getUint32(encoder, true); + * + * // Configure encoder with Kitty protocol flags + * const flagsPtr = exports.ghostty_wasm_alloc_u8(); + * view.setUint8(flagsPtr, 0x1F); + * exports.ghostty_key_encoder_setopt(encoder, 5, flagsPtr); + * + * // Allocate output buffer and size pointer + * const bufferSize = 32; + * const bufPtr = exports.ghostty_wasm_alloc_u8_array(bufferSize); + * const writtenPtr = exports.ghostty_wasm_alloc_usize(); + * + * // Encode the key event + * exports.ghostty_key_encoder_encode( + * encoder, eventPtr, bufPtr, bufferSize, writtenPtr + * ); + * + * // Read encoded output + * const bytesWritten = view.getUint32(writtenPtr, true); + * const encoded = new Uint8Array(wasmMemory.buffer, bufPtr, bytesWritten); + * @endcode + * + * @remark The code above is pretty ugly! This is the lowest level interface + * to the libghostty-vt Wasm module. In practice, this should be wrapped + * in a higher-level API that abstracts away all this. + * + * @{ + */ + +/** + * Allocate an opaque pointer. This can be used for any opaque pointer + * types such as GhosttyKeyEncoder, GhosttyKeyEvent, etc. + * + * @return Pointer to allocated opaque pointer, or NULL if allocation failed + * @ingroup wasm + */ +void** ghostty_wasm_alloc_opaque(void); + +/** + * Free an opaque pointer allocated by ghostty_wasm_alloc_opaque(). + * + * @param ptr Pointer to free, or NULL (NULL is safely ignored) + * @ingroup wasm + */ +void ghostty_wasm_free_opaque(void **ptr); + +/** + * Allocate an array of uint8_t values. + * + * @param len Number of uint8_t elements to allocate + * @return Pointer to allocated array, or NULL if allocation failed + * @ingroup wasm + */ +uint8_t* ghostty_wasm_alloc_u8_array(size_t len); + +/** + * Free an array allocated by ghostty_wasm_alloc_u8_array(). + * + * @param ptr Pointer to the array to free, or NULL (NULL is safely ignored) + * @param len Length of the array (must match the length passed to alloc) + * @ingroup wasm + */ +void ghostty_wasm_free_u8_array(uint8_t *ptr, size_t len); + +/** + * Allocate an array of uint16_t values. + * + * @param len Number of uint16_t elements to allocate + * @return Pointer to allocated array, or NULL if allocation failed + * @ingroup wasm + */ +uint16_t* ghostty_wasm_alloc_u16_array(size_t len); + +/** + * Free an array allocated by ghostty_wasm_alloc_u16_array(). + * + * @param ptr Pointer to the array to free, or NULL (NULL is safely ignored) + * @param len Length of the array (must match the length passed to alloc) + * @ingroup wasm + */ +void ghostty_wasm_free_u16_array(uint16_t *ptr, size_t len); + +/** + * Allocate a single uint8_t value. + * + * @return Pointer to allocated uint8_t, or NULL if allocation failed + * @ingroup wasm + */ +uint8_t* ghostty_wasm_alloc_u8(void); + +/** + * Free a uint8_t allocated by ghostty_wasm_alloc_u8(). + * + * @param ptr Pointer to free, or NULL (NULL is safely ignored) + * @ingroup wasm + */ +void ghostty_wasm_free_u8(uint8_t *ptr); + +/** + * Allocate a single size_t value. + * + * @return Pointer to allocated size_t, or NULL if allocation failed + * @ingroup wasm + */ +size_t* ghostty_wasm_alloc_usize(void); + +/** + * Free a size_t allocated by ghostty_wasm_alloc_usize(). + * + * @param ptr Pointer to free, or NULL (NULL is safely ignored) + * @ingroup wasm + */ +void ghostty_wasm_free_usize(size_t *ptr); + +/** @} */ + +#endif /* __wasm__ */ + +#endif /* GHOSTTY_VT_WASM_H */ diff --git a/crates/ghostty-vt-capi/src/alloc.rs b/crates/ghostty-vt-capi/src/alloc.rs new file mode 100644 index 0000000..8cb88bb --- /dev/null +++ b/crates/ghostty-vt-capi/src/alloc.rs @@ -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); + +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(self, value: T) -> Option> { + let p = NonNull::new(self.alloc_raw(Layout::new::()))?.cast::(); + // 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::` on this allocator and not be used again. + pub unsafe fn destroy(self, p: NonNull) { + // SAFETY: per the function contract. + unsafe { + std::ptr::drop_in_place(p.as_ptr()); + self.free_raw(p.as_ptr().cast(), Layout::new::()); + } + } +} diff --git a/crates/ghostty-vt-capi/src/color.rs b/crates/ghostty-vt-capi/src/color.rs new file mode 100644 index 0000000..c7b70ce --- /dev/null +++ b/crates/ghostty-vt-capi/src/color.rs @@ -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 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)); + } +} diff --git a/crates/ghostty-vt-capi/src/key_encode.rs b/crates/ghostty-vt-capi/src/key_encode.rs new file mode 100644 index 0000000..996f246 --- /dev/null +++ b/crates/ghostty-vt-capi/src/key_encode.rs @@ -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 { + 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 { + 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::().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::().read_unaligned() & 0x1f); + } + KeyEncoderOption::MacosOptionAsAlt => { + let raw = value.cast::().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 { + 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); + } + } +} diff --git a/crates/ghostty-vt-capi/src/key_event.rs b/crates/ghostty-vt-capi/src/key_event.rs new file mode 100644 index 0000000..d0d20b7 --- /dev/null +++ b/crates/ghostty-vt-capi/src/key_event.rs @@ -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 { + match v { + 0 => Some(Action::Release), + 1 => Some(Action::Press), + 2 => Some(Action::Repeat), + _ => None, + } +} + +fn key_from_c(v: c_int) -> Option { + 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); + } +} diff --git a/crates/ghostty-vt-capi/src/lib.rs b/crates/ghostty-vt-capi/src/lib.rs new file mode 100644 index 0000000..2c7628a --- /dev/null +++ b/crates/ghostty-vt-capi/src/lib.rs @@ -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; diff --git a/crates/ghostty-vt-capi/src/osc.rs b/crates/ghostty-vt-capi/src/osc.rs new file mode 100644 index 0000000..384bcec --- /dev/null +++ b/crates/ghostty-vt-capi/src/osc.rs @@ -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, +} + +/// 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 { + 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); + } + } +} diff --git a/crates/ghostty-vt-capi/src/paste.rs b/crates/ghostty-vt-capi/src/paste.rs new file mode 100644 index 0000000..847fb85 --- /dev/null +++ b/crates/ghostty-vt-capi/src/paste.rs @@ -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) }); + } +} diff --git a/crates/ghostty-vt-capi/src/result.rs b/crates/ghostty-vt-capi/src/result.rs new file mode 100644 index 0000000..fe0b4f6 --- /dev/null +++ b/crates/ghostty-vt-capi/src/result.rs @@ -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, +} diff --git a/crates/ghostty-vt-capi/src/sgr.rs b/crates/ghostty-vt-capi/src/sgr.rs new file mode 100644 index 0000000..8ac12e9 --- /dev/null +++ b/crates/ghostty-vt-capi/src/sgr.rs @@ -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::(len).unwrap_or(Layout::new::()) + } + + 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::(); + 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::(), + (&raw mut attr).cast::().add(8) + ); + ghostty_sgr_free(p); + } + } +} diff --git a/crates/ghostty-vt-capi/src/wasm.rs b/crates/ghostty-vt-capi/src/wasm.rs new file mode 100644 index 0000000..07b6eba --- /dev/null +++ b/crates/ghostty-vt-capi/src/wasm.rs @@ -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::(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::(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::(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::(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::()) +} + +/// # 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::()) }; +} + +#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))] +pub extern "C" fn ghostty_wasm_alloc_usize() -> *mut usize { + alloc_layout(Layout::new::()).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::()) }; +} + +/// 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::()).cast::(); + 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::()) }; +} + +#[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); + } + } +} diff --git a/crates/ghostty-vt-capi/tests/abi.rs b/crates/ghostty-vt-capi/tests/abi.rs new file mode 100644 index 0000000..6d98636 --- /dev/null +++ b/crates/ghostty-vt-capi/tests/abi.rs @@ -0,0 +1,658 @@ +//! ABI verification: a C program generated from the unchanged headers and +//! compiled with `cc` prints `sizeof`/`alignof`/`offsetof` of every struct and +//! union, `sizeof` of every enum/typedef, the value of every enumerator and +//! `#define`d constant, and takes the address of every declared function (so +//! linking proves the symbol is exported). The values are compared against the +//! Rust definitions (`size_of`/`align_of`/`offset_of!`, enum discriminants). +#![cfg(unix)] +#![allow(clippy::unwrap_used)] + +mod common; + +use std::{ + collections::BTreeMap, + fmt::Write as _, + mem::{align_of, offset_of, size_of}, + process::Command, +}; + +use ghostty_input::{ + Mods, + key::Key, + key_mods::{ModSide, Side}, +}; +use ghostty_vt::{color::Name, kitty::key::Flags as KittyFlags}; +use ghostty_vt_capi::{ + alloc::{Allocator, AllocatorVtable}, + color::GhosttyColorRgb, + key_encode::KeyEncoderOption, + key_event::KeyAction, + osc::OscCommandData, + result::GhosttyResult, + sgr::{SgrAttribute, SgrAttributeTag, SgrAttributeValue, SgrUnknown}, +}; + +/// `GHOSTTY_FOO_BAR` / `FooBar` / `foo_bar` -> `foobar`. +fn norm(s: &str) -> String { + s.chars() + .filter(|c| *c != '_') + .map(|c| c.to_ascii_lowercase()) + .collect() +} + +/// Parsed `typedef enum { ... } Name;` blocks: typedef name -> enumerators. +fn header_enums() -> BTreeMap> { + let mut map = BTreeMap::new(); + for h in common::headers() { + let text = common::strip_comments(&std::fs::read_to_string(h).unwrap()); + let mut rest = text.as_str(); + while let Some(start) = rest.find("typedef enum") { + let after = &rest[start..]; + let open = after.find('{').unwrap(); + let close = after.find('}').unwrap(); + let name_end = after[close..].find(';').unwrap() + close; + let name = after[close + 1..name_end].trim().to_string(); + let names = after[open + 1..close] + .split(',') + .map(|e| e.split('=').next().unwrap().trim().to_string()) + .filter(|e| !e.is_empty()) + .collect(); + map.insert(name, names); + rest = &after[name_end..]; + } + } + map +} + +/// `#define GHOSTTY_X ...` constants that are not include guards. +fn header_defines() -> Vec { + let mut v = Vec::new(); + for h in common::headers() { + for line in std::fs::read_to_string(h).unwrap().lines() { + if let Some(rest) = line.strip_prefix("#define GHOSTTY_") { + let name = rest.split_whitespace().next().unwrap(); + if !name.ends_with("_H") && rest.split_whitespace().nth(1).is_some() { + v.push(format!("GHOSTTY_{name}")); + } + } + } + } + v +} + +/// Names and values of a Rust-side enum, keyed by normalized name. +fn vals(items: &[T], f: impl Fn(T) -> i64) -> BTreeMap { + items + .iter() + .map(|i| (norm(&format!("{i:?}")), f(*i))) + .collect() +} + +struct Probe { + c: String, + expected: BTreeMap, +} + +impl Probe { + fn emit(&mut self, key: &str, c_expr: &str, expected: i64) { + let _ = writeln!( + self.c, + " printf(\"%s=%lld\\n\", \"{key}\", (long long)({c_expr}));" + ); + self.expected.insert(key.to_string(), expected); + } + + fn layout(&mut self, ty: &str, size: usize, align: usize, fields: &[(&str, usize)]) { + self.emit( + &format!("sizeof({ty})"), + &format!("sizeof({ty})"), + size as i64, + ); + self.emit( + &format!("alignof({ty})"), + &format!("_Alignof({ty})"), + align as i64, + ); + for (f, off) in fields { + self.emit( + &format!("offsetof({ty},{f})"), + &format!("offsetof({ty}, {f})"), + *off as i64, + ); + } + } + + /// Check every enumerator of `typedef` against `rust` (names after + /// `prefix`); the C compiler provides the values. `exact` also requires + /// that Rust has no extra members. + fn enumerators( + &mut self, + enums: &BTreeMap>, + typedef: &str, + prefix: &str, + rust: &BTreeMap, + exact: bool, + ) { + let names = enums + .get(typedef) + .unwrap_or_else(|| panic!("no enum {typedef}")); + // All GHOSTTY enums are `int` sized. + self.emit( + &format!("sizeof({typedef})"), + &format!("sizeof({typedef})"), + 4, + ); + for n in names { + let short = n + .strip_prefix(prefix) + .unwrap_or_else(|| panic!("{n} lacks {prefix}")); + let want = *rust + .get(&norm(short)) + .unwrap_or_else(|| panic!("{typedef}: Rust has no member for {n}")); + self.emit(n, n, want); + } + if exact { + assert_eq!(names.len(), rust.len(), "{typedef}: member count differs"); + } + } +} + +fn build_probe() -> Probe { + let enums = header_enums(); + let mut p = Probe { + c: String::new(), + expected: BTreeMap::new(), + }; + + // ---- structs and unions ---- + p.layout( + "GhosttyColorRgb", + size_of::(), + align_of::(), + &[ + ("r", offset_of!(GhosttyColorRgb, r)), + ("g", offset_of!(GhosttyColorRgb, g)), + ("b", offset_of!(GhosttyColorRgb, b)), + ], + ); + p.layout( + "GhosttyAllocatorVtable", + size_of::(), + align_of::(), + &[ + ("alloc", offset_of!(AllocatorVtable, alloc)), + ("resize", offset_of!(AllocatorVtable, resize)), + ("remap", offset_of!(AllocatorVtable, remap)), + ("free", offset_of!(AllocatorVtable, free)), + ], + ); + p.layout( + "GhosttyAllocator", + size_of::(), + align_of::(), + &[ + ("ctx", offset_of!(Allocator, ctx)), + ("vtable", offset_of!(Allocator, vtable)), + ], + ); + p.layout( + "GhosttySgrUnknown", + size_of::(), + align_of::(), + &[ + ("full_ptr", offset_of!(SgrUnknown, full_ptr)), + ("full_len", offset_of!(SgrUnknown, full_len)), + ("partial_ptr", offset_of!(SgrUnknown, partial_ptr)), + ("partial_len", offset_of!(SgrUnknown, partial_len)), + ], + ); + p.layout( + "GhosttySgrAttributeValue", + size_of::(), + align_of::(), + &[ + ("unknown", offset_of!(SgrAttributeValue, unknown)), + ("underline", offset_of!(SgrAttributeValue, underline)), + ( + "underline_color", + offset_of!(SgrAttributeValue, underline_color), + ), + ( + "underline_color_256", + offset_of!(SgrAttributeValue, underline_color_256), + ), + ( + "direct_color_fg", + offset_of!(SgrAttributeValue, direct_color_fg), + ), + ( + "direct_color_bg", + offset_of!(SgrAttributeValue, direct_color_bg), + ), + ("bg_8", offset_of!(SgrAttributeValue, bg_8)), + ("fg_8", offset_of!(SgrAttributeValue, fg_8)), + ("bright_bg_8", offset_of!(SgrAttributeValue, bright_bg_8)), + ("bright_fg_8", offset_of!(SgrAttributeValue, bright_fg_8)), + ("bg_256", offset_of!(SgrAttributeValue, bg_256)), + ("fg_256", offset_of!(SgrAttributeValue, fg_256)), + ("_padding", offset_of!(SgrAttributeValue, _padding)), + ], + ); + p.layout( + "GhosttySgrAttribute", + size_of::(), + align_of::(), + &[ + ("tag", offset_of!(SgrAttribute, tag)), + ("value", offset_of!(SgrAttribute, value)), + ], + ); + + // ---- scalar typedefs and opaque handles ---- + for (ty, size) in [ + ("GhosttyMods", 2), + ("GhosttyKittyKeyFlags", 1), + ("GhosttyColorPaletteIndex", 1), + ("GhosttyOscParser", size_of::()), + ("GhosttyOscCommand", size_of::()), + ("GhosttySgrParser", size_of::()), + ("GhosttyKeyEvent", size_of::()), + ("GhosttyKeyEncoder", size_of::()), + ] { + p.emit( + &format!("sizeof({ty})"), + &format!("sizeof({ty})"), + size as i64, + ); + } + + // ---- enums ---- + p.enumerators( + &enums, + "GhosttyResult", + "GHOSTTY_", + &vals( + &[ + GhosttyResult::Success, + GhosttyResult::OutOfMemory, + GhosttyResult::InvalidValue, + ], + |v| v as i64, + ), + true, + ); + // The Rust OSC parser knows two commands newer than the header. + let osc = { + use ghostty_vt::osc::Key::*; + vals( + &[ + Invalid, + ChangeWindowTitle, + ChangeWindowIcon, + SemanticPrompt, + ClipboardContents, + ReportPwd, + MouseShape, + ColorOperation, + KittyColorProtocol, + ShowDesktopNotification, + HyperlinkStart, + HyperlinkEnd, + ConemuSleep, + ConemuShowMessageBox, + ConemuChangeTabTitle, + ConemuProgressReport, + ConemuWaitInput, + ConemuGuimacro, + ConemuRunProcess, + ConemuOutputEnvironmentVariable, + ConemuXtermEmulation, + ConemuComment, + KittyTextSizing, + KittyClipboardProtocol, + ContextSignal, + ], + |v| v as i64, + ) + }; + p.enumerators( + &enums, + "GhosttyOscCommandType", + "GHOSTTY_OSC_COMMAND_", + &osc, + false, + ); + p.enumerators( + &enums, + "GhosttyOscCommandData", + "GHOSTTY_OSC_DATA_", + &vals( + &[ + OscCommandData::Invalid, + OscCommandData::ChangeWindowTitleStr, + ], + |v| v as i64, + ), + true, + ); + p.enumerators( + &enums, + "GhosttySgrAttributeTag", + "GHOSTTY_SGR_ATTR_", + &sgr_tags(), + true, + ); + p.enumerators( + &enums, + "GhosttySgrUnderline", + "GHOSTTY_SGR_UNDERLINE_", + &vals( + &[ + ghostty_vt::sgr::Underline::None, + ghostty_vt::sgr::Underline::Single, + ghostty_vt::sgr::Underline::Double, + ghostty_vt::sgr::Underline::Curly, + ghostty_vt::sgr::Underline::Dotted, + ghostty_vt::sgr::Underline::Dashed, + ], + |v| v as i64, + ), + true, + ); + p.enumerators( + &enums, + "GhosttyKeyAction", + "GHOSTTY_KEY_ACTION_", + &vals( + &[KeyAction::Release, KeyAction::Press, KeyAction::Repeat], + |v| v as i64, + ), + true, + ); + // ghostty_input::Action must agree with the header too. + assert_eq!(ghostty_input::Action::Release as i32, 0); + assert_eq!(ghostty_input::Action::Press as i32, 1); + assert_eq!(ghostty_input::Action::Repeat as i32, 2); + + let keys: BTreeMap = Key::ALL + .iter() + .enumerate() + .map(|(i, k)| { + let n = k.name(); + (norm(n.strip_prefix("key_").unwrap_or(n)), i as i64) + }) + .collect(); + p.enumerators(&enums, "GhosttyKey", "GHOSTTY_KEY_", &keys, true); + for (i, k) in Key::ALL.iter().enumerate() { + assert_eq!(*k as usize, i, "Key discriminants are dense"); + } + p.enumerators( + &enums, + "GhosttyKeyEncoderOption", + "GHOSTTY_KEY_ENCODER_OPT_", + &vals( + &[ + KeyEncoderOption::CursorKeyApplication, + KeyEncoderOption::KeypadKeyApplication, + KeyEncoderOption::IgnoreKeypadWithNumlock, + KeyEncoderOption::AltEscPrefix, + KeyEncoderOption::ModifyOtherKeysState2, + KeyEncoderOption::KittyFlags, + KeyEncoderOption::MacosOptionAsAlt, + ], + |v| v as i64, + ), + true, + ); + p.enumerators( + &enums, + "GhosttyOptionAsAlt", + "GHOSTTY_OPTION_AS_ALT_", + &vals( + &[ + ghostty_input::OptionAsAlt::False, + ghostty_input::OptionAsAlt::True, + ghostty_input::OptionAsAlt::Left, + ghostty_input::OptionAsAlt::Right, + ], + |v| v as i64, + ), + true, + ); + + // ---- #define constants ---- + let right = |f: fn(&mut Side)| { + let mut s = Side::default(); + f(&mut s); + Mods { + sides: s, + ..Mods::default() + } + .int() as i64 + }; + let _ = ModSide::Right; + let mod_flag = |f: fn(&mut Mods)| { + let mut m = Mods::default(); + f(&mut m); + m.int() as i64 + }; + let kitty = |f: fn(&mut KittyFlags)| { + let mut k = KittyFlags::DISABLED; + f(&mut k); + k.int() as i64 + }; + let named = |n: Name| i64::from(n.0); + let defines: BTreeMap<&str, i64> = BTreeMap::from([ + ("GHOSTTY_MODS_SHIFT", mod_flag(|m| m.shift = true)), + ("GHOSTTY_MODS_CTRL", mod_flag(|m| m.ctrl = true)), + ("GHOSTTY_MODS_ALT", mod_flag(|m| m.alt = true)), + ("GHOSTTY_MODS_SUPER", mod_flag(|m| m.super_ = true)), + ("GHOSTTY_MODS_CAPS_LOCK", mod_flag(|m| m.caps_lock = true)), + ("GHOSTTY_MODS_NUM_LOCK", mod_flag(|m| m.num_lock = true)), + ( + "GHOSTTY_MODS_SHIFT_SIDE", + right(|s| s.shift = ModSide::Right), + ), + ("GHOSTTY_MODS_CTRL_SIDE", right(|s| s.ctrl = ModSide::Right)), + ("GHOSTTY_MODS_ALT_SIDE", right(|s| s.alt = ModSide::Right)), + ( + "GHOSTTY_MODS_SUPER_SIDE", + right(|s| s.super_ = ModSide::Right), + ), + ("GHOSTTY_KITTY_KEY_DISABLED", kitty(|_| {})), + ( + "GHOSTTY_KITTY_KEY_DISAMBIGUATE", + kitty(|k| k.disambiguate = true), + ), + ( + "GHOSTTY_KITTY_KEY_REPORT_EVENTS", + kitty(|k| k.report_events = true), + ), + ( + "GHOSTTY_KITTY_KEY_REPORT_ALTERNATES", + kitty(|k| k.report_alternates = true), + ), + ( + "GHOSTTY_KITTY_KEY_REPORT_ALL", + kitty(|k| k.report_all = true), + ), + ( + "GHOSTTY_KITTY_KEY_REPORT_ASSOCIATED", + kitty(|k| k.report_associated = true), + ), + ("GHOSTTY_KITTY_KEY_ALL", KittyFlags::TRUE.int() as i64), + ("GHOSTTY_COLOR_NAMED_BLACK", named(Name::BLACK)), + ("GHOSTTY_COLOR_NAMED_RED", named(Name::RED)), + ("GHOSTTY_COLOR_NAMED_GREEN", named(Name::GREEN)), + ("GHOSTTY_COLOR_NAMED_YELLOW", named(Name::YELLOW)), + ("GHOSTTY_COLOR_NAMED_BLUE", named(Name::BLUE)), + ("GHOSTTY_COLOR_NAMED_MAGENTA", named(Name::MAGENTA)), + ("GHOSTTY_COLOR_NAMED_CYAN", named(Name::CYAN)), + ("GHOSTTY_COLOR_NAMED_WHITE", named(Name::WHITE)), + ( + "GHOSTTY_COLOR_NAMED_BRIGHT_BLACK", + named(Name::BRIGHT_BLACK), + ), + ("GHOSTTY_COLOR_NAMED_BRIGHT_RED", named(Name::BRIGHT_RED)), + ( + "GHOSTTY_COLOR_NAMED_BRIGHT_GREEN", + named(Name::BRIGHT_GREEN), + ), + ( + "GHOSTTY_COLOR_NAMED_BRIGHT_YELLOW", + named(Name::BRIGHT_YELLOW), + ), + ("GHOSTTY_COLOR_NAMED_BRIGHT_BLUE", named(Name::BRIGHT_BLUE)), + ( + "GHOSTTY_COLOR_NAMED_BRIGHT_MAGENTA", + named(Name::BRIGHT_MAGENTA), + ), + ("GHOSTTY_COLOR_NAMED_BRIGHT_CYAN", named(Name::BRIGHT_CYAN)), + ( + "GHOSTTY_COLOR_NAMED_BRIGHT_WHITE", + named(Name::BRIGHT_WHITE), + ), + ]); + let declared = header_defines(); + for d in &declared { + let want = defines + .get(d.as_str()) + .unwrap_or_else(|| panic!("header #define {d} has no Rust expectation")); + p.emit(d, d, *want); + } + assert_eq!( + declared.len(), + defines.len(), + "stale expectation in the define table" + ); + p +} + +fn sgr_tags() -> BTreeMap { + use SgrAttributeTag::*; + vals( + &[ + Unset, + Unknown, + Bold, + ResetBold, + Italic, + ResetItalic, + Faint, + Underline, + UnderlineColor, + UnderlineColor256, + ResetUnderlineColor, + Overline, + ResetOverline, + Blink, + ResetBlink, + Inverse, + ResetInverse, + Invisible, + ResetInvisible, + Strikethrough, + ResetStrikethrough, + DirectColorFg, + DirectColorBg, + Bg8, + Fg8, + ResetFg, + ResetBg, + BrightBg8, + BrightFg8, + Bg256, + Fg256, + ], + |v| v as i64, + ) +} + +#[test] +fn abi_matches_headers() { + let lib = common::install_native_lib(); + let scratch = common::scratch_dir("abi"); + let mut probe = build_probe(); + + let fns: Vec = common::declared_functions() + .into_iter() + .filter(|f| !f.starts_with("ghostty_wasm_")) + .collect(); + assert!(fns.len() == 38, "expected the full API, got {fns:?}"); + let mut src = String::from( + "#include \n#include \n#include \n#include \n", + ); + src.push_str("static void *const fns[] = {\n"); + for f in &fns { + let _ = writeln!(src, " (void *){f},"); + } + src.push_str("};\nint main(void) {\n if (sizeof(fns) == 0) return 2;\n"); + src.push_str(&probe.c); + src.push_str(" return 0;\n}\n"); + + let c_file = scratch.join("probe.c"); + std::fs::write(&c_file, src).unwrap(); + let exe = scratch.join("probe"); + common::compile_c(&lib, &[&c_file], &exe, &["-Wall", "-Werror", "-std=c11"]); + let out = Command::new(&exe).output().unwrap(); + assert!(out.status.success()); + + let got: BTreeMap = String::from_utf8(out.stdout) + .unwrap() + .lines() + .map(|l| { + let (k, v) = l.rsplit_once('=').unwrap(); + (k.to_string(), v.parse().unwrap()) + }) + .collect(); + probe.expected.insert("_".into(), 0); + let mut bad = Vec::new(); + for (k, want) in &probe.expected { + if k == "_" { + continue; + } + match got.get(k) { + Some(g) if g == want => {} + g => bad.push(format!("{k}: C={g:?} Rust={want}")), + } + } + assert!(bad.is_empty(), "ABI mismatches:\n{}", bad.join("\n")); + assert!(got.len() > 300, "suspiciously few probes: {}", got.len()); +} + +/// Every exported function in the shared object is declared in the headers +/// (no accidental extra `ghostty_*` symbols on native targets). +#[test] +fn exported_symbols_match_headers() { + let lib = common::install_native_lib(); + let out = Command::new("nm") + .args(["-D", "--defined-only"]) + .arg(lib.join("libghostty-vt.so")) + .output(); + let Ok(out) = out else { + eprintln!("nm not available; skipping"); + return; + }; + let text = String::from_utf8(out.stdout).unwrap(); + let exported: Vec<&str> = text + .lines() + .filter_map(|l| l.split_whitespace().last()) + .filter(|s| s.starts_with("ghostty_")) + .collect(); + let declared: Vec = common::declared_functions() + .into_iter() + .filter(|f| !f.starts_with("ghostty_wasm_")) + .collect(); + for f in &declared { + assert!( + exported.contains(&f.as_str()), + "{f} declared but not exported" + ); + } + for e in &exported { + assert!( + declared.iter().any(|d| d == e), + "{e} exported but not declared" + ); + } +} diff --git a/crates/ghostty-vt-capi/tests/c_examples.rs b/crates/ghostty-vt-capi/tests/c_examples.rs new file mode 100644 index 0000000..b55e15d --- /dev/null +++ b/crates/ghostty-vt-capi/tests/c_examples.rs @@ -0,0 +1,94 @@ +//! Builds Ghostty's unchanged C examples (examples/c/*.c, from +//! ZIG/example/c-vt*) against the Rust `libghostty-vt` and checks their output. +#![cfg(unix)] +#![allow(clippy::unwrap_used)] + +mod common; + +use std::process::Command; + +fn run_example(name: &str) -> String { + let lib = common::install_native_lib(); + let scratch = common::scratch_dir("c_examples"); + let src = common::manifest_dir() + .join("examples/c") + .join(format!("{name}.c")); + let exe = scratch.join(name); + common::compile_c(&lib, &[&src], &exe, &["-Wall"]); + let out = Command::new(&exe).output().expect("run example"); + assert!(out.status.success(), "{name} exited with {:?}", out.status); + String::from_utf8(out.stdout).unwrap() +} + +#[test] +fn c_vt_osc() { + assert_eq!( + run_example("c-vt"), + "Command type: 1\nExtracted title: hello\n" + ); +} + +#[test] +fn c_vt_key_encode() { + assert_eq!( + run_example("c-vt-key-encode"), + "Encoding event: left ctrl release with all Kitty flags enabled\n\ + Required buffer size: 12 bytes\n\ + Encoded 12 bytes\n\ + Hex: 1b 5b 35 37 34 34 32 3b 35 3a 33 75 \n\ + String: \\x1b[57442;5:3u\n" + ); +} + +#[test] +fn c_vt_paste() { + assert_eq!( + run_example("c-vt-paste"), + "'hello world' is safe to paste\n\ + 'rm -rf /\n' is UNSAFE - contains newline\n\ + Data with escape sequence is UNSAFE\n\ + Empty data is safe\n" + ); +} + +#[test] +fn c_vt_sgr() { + assert_eq!( + run_example("c-vt-sgr"), + "Parsing Kakoune SGR sequence:\n\ + ESC[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136m\n\ + \n\ + Attribute 1: Underline style = curly\n\ + Attribute 2: Foreground RGB = (51, 51, 51)\n\ + Attribute 3: Background RGB = (170, 170, 170)\n\ + Attribute 4: Underline color RGB = (255, 97, 136)\n\ + \n\ + Total attributes parsed: 4\n" + ); +} + +/// The static archive links too (what embedders of `libghostty-vt.a` use). +#[test] +fn c_vt_links_statically() { + let lib = common::install_native_lib(); + let scratch = common::scratch_dir("c_examples_static"); + let src = common::manifest_dir().join("examples/c/c-vt-paste.c"); + let exe = scratch.join("paste-static"); + let out = Command::new("cc") + .arg("-I") + .arg(common::include_dir()) + .arg(&src) + .arg(lib.join("libghostty-vt.a")) + .args(["-lpthread", "-ldl", "-lm"]) + .arg("-o") + .arg(&exe) + .output() + .unwrap(); + assert!( + out.status.success(), + "{}", + String::from_utf8_lossy(&out.stderr) + ); + let run = Command::new(&exe).output().unwrap(); + assert!(String::from_utf8_lossy(&run.stdout).contains("Empty data is safe")); +} diff --git a/crates/ghostty-vt-capi/tests/common/mod.rs b/crates/ghostty-vt-capi/tests/common/mod.rs new file mode 100644 index 0000000..64f7315 --- /dev/null +++ b/crates/ghostty-vt-capi/tests/common/mod.rs @@ -0,0 +1,170 @@ +//! Shared helpers: build the library and install it under Ghostty's file +//! names (`libghostty-vt.{so,a}`) next to the Cargo artifacts. +#![allow(dead_code)] + +use std::{ + path::{Path, PathBuf}, + process::Command, + sync::OnceLock, +}; + +pub fn manifest_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) +} + +pub fn include_dir() -> PathBuf { + manifest_dir().join("include") +} + +/// `target/`, derived from the test executable's location. +pub fn profile_dir() -> PathBuf { + let exe = std::env::current_exe().expect("current_exe"); + exe.parent() // deps + .and_then(Path::parent) // + .expect("profile dir") + .to_path_buf() +} + +fn cargo() -> Command { + Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into())) +} + +static INSTALLED: OnceLock = OnceLock::new(); + +/// Build the cdylib/staticlib for the host and install them as +/// `libghostty-vt.so` / `libghostty-vt.a` in the profile dir. Returns that dir. +pub fn install_native_lib() -> PathBuf { + INSTALLED.get_or_init(install_native_lib_once).clone() +} + +fn install_native_lib_once() -> PathBuf { + let dir = profile_dir(); + let profile = dir.file_name().and_then(|s| s.to_str()).unwrap_or("debug"); + let mut cmd = cargo(); + cmd.args(["build", "-p", "ghostty-vt-capi"]); + if profile != "debug" { + cmd.args(["--profile", profile]); + } + let status = cmd.status().expect("run cargo build"); + assert!(status.success(), "cargo build -p ghostty-vt-capi failed"); + for (from, to) in [ + ("libghostty_vt_capi.so", "libghostty-vt.so"), + ("libghostty_vt_capi.a", "libghostty-vt.a"), + ("libghostty_vt_capi.dylib", "libghostty-vt.dylib"), + ] { + let src = dir.join(from); + if src.exists() { + let dst = dir.join(to); + // Copy + rename so a concurrent reader never sees a partial file. + let tmp = dir.join(format!("{to}.{}.tmp", std::process::id())); + std::fs::copy(&src, &tmp).expect("install library"); + std::fs::rename(&tmp, &dst).expect("install library"); + } + } + dir +} + +/// Compile `sources` (C files) with `cc` against the installed library. +pub fn compile_c(lib_dir: &Path, sources: &[&Path], out: &Path, extra_cflags: &[&str]) { + let mut cmd = Command::new("cc"); + cmd.arg("-I") + .arg(include_dir()) + .args(extra_cflags) + .args(sources) + .arg("-o") + .arg(out) + .arg("-L") + .arg(lib_dir) + .arg("-lghostty-vt") + .arg(format!("-Wl,-rpath,{}", lib_dir.display())); + let output = cmd.output().expect("run cc"); + assert!( + output.status.success(), + "cc failed:\n{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +pub fn scratch_dir(name: &str) -> PathBuf { + let d = profile_dir().join("vt-capi-tests").join(name); + std::fs::create_dir_all(&d).expect("scratch dir"); + d +} + +/// Header text with `/* */` and `//` comments removed. +pub fn strip_comments(src: &str) -> String { + let b = src.as_bytes(); + let mut out = String::with_capacity(src.len()); + let mut i = 0; + while i < b.len() { + if b[i..].starts_with(b"/*") { + i += 2; + while i < b.len() && !b[i..].starts_with(b"*/") { + i += 1; + } + i += 2; + } else if b[i..].starts_with(b"//") { + while i < b.len() && b[i] != b'\n' { + i += 1; + } + } else { + let ch = src[i..].chars().next().unwrap(); + out.push(ch); + i += ch.len_utf8(); + } + } + out +} + +/// All headers (relative to `include/`), in a stable order. +pub fn headers() -> Vec { + fn walk(dir: &Path, out: &mut Vec) { + for e in std::fs::read_dir(dir).unwrap() { + let p = e.unwrap().path(); + if p.is_dir() { + walk(&p, out); + } else if p.extension().is_some_and(|x| x == "h") { + out.push(p); + } + } + } + let mut v = Vec::new(); + walk(&include_dir(), &mut v); + v.sort(); + v +} + +/// `ghostty_*` function names declared in the headers (comments removed). +pub fn declared_functions() -> Vec { + let mut names = Vec::new(); + for h in headers() { + let text = strip_comments(&std::fs::read_to_string(h).unwrap()); + let b = text.as_bytes(); + let mut i = 0; + while let Some(pos) = text[i..].find("ghostty_") { + let start = i + pos; + let mut end = start; + while end < b.len() + && (b[end].is_ascii_lowercase() || b[end].is_ascii_digit() || b[end] == b'_') + { + end += 1; + } + let mut j = end; + while j < b.len() && b[j].is_ascii_whitespace() { + j += 1; + } + if j < b.len() + && b[j] == b'(' + && !text[..start].ends_with(|c: char| c.is_ascii_alphanumeric() || c == '_') + { + let n = text[start..end].to_string(); + if !names.contains(&n) { + names.push(n); + } + } + i = end.max(start + 1); + } + } + names.sort(); + names +} diff --git a/crates/ghostty-vt-capi/tests/wasm/run.mjs b/crates/ghostty-vt-capi/tests/wasm/run.mjs new file mode 100644 index 0000000..75a10f1 --- /dev/null +++ b/crates/ghostty-vt-capi/tests/wasm/run.mjs @@ -0,0 +1,146 @@ +// Runs Ghostty's unmodified wasm examples (examples/wasm/*.html) under node +// with a minimal DOM stub, against the Rust-built ghostty-vt wasm module. +// +// node run.mjs exports +// node run.mjs sgr [fix-tags] +// node run.mjs key +// +// Output is line-oriented for the Rust test to parse. +import fs from 'node:fs'; +import vm from 'node:vm'; + +const [mode, ...args] = process.argv.slice(2); + +function script(html) { + const m = html.match(/