diff --git a/example/wasm-key-encode/index.html b/example/wasm-key-encode/index.html index 714988b4d..9f4d8bebb 100644 --- a/example/wasm-key-encode/index.html +++ b/example/wasm-key-encode/index.html @@ -458,7 +458,7 @@ // Set UTF-8 text from the key event (the actual character produced) if (event.key.length === 1) { const utf8Bytes = new TextEncoder().encode(event.key); - const utf8Ptr = wasmInstance.exports.ghostty_wasm_alloc_buffer(utf8Bytes.length); + const utf8Ptr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(utf8Bytes.length); new Uint8Array(getBuffer()).set(utf8Bytes, utf8Ptr); wasmInstance.exports.ghostty_key_event_set_utf8(eventPtr, utf8Ptr, utf8Bytes.length); } @@ -477,7 +477,7 @@ const required = new DataView(getBuffer()).getUint32(requiredPtr, true); - const bufPtr = wasmInstance.exports.ghostty_wasm_alloc_buffer(required); + const bufPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(required); const writtenPtr = wasmInstance.exports.ghostty_wasm_alloc_usize(); const encodeResult = wasmInstance.exports.ghostty_key_encoder_encode( encoderPtr, eventPtr, bufPtr, required, writtenPtr diff --git a/example/wasm-sgr/README.md b/example/wasm-sgr/README.md new file mode 100644 index 000000000..a107c910d --- /dev/null +++ b/example/wasm-sgr/README.md @@ -0,0 +1,39 @@ +# WebAssembly SGR Parser Example + +This example demonstrates how to use the Ghostty VT library from WebAssembly +to parse terminal SGR (Select Graphic Rendition) sequences and extract text +styling attributes. + +## Building + +First, build the WebAssembly module: + +```bash +zig build lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall +``` + +This will create `zig-out/bin/ghostty-vt.wasm`. + +## Running + +**Important:** You must serve this via HTTP, not open it as a file directly. +Browsers block loading WASM files from `file://` URLs. + +From the **root of the ghostty repository**, serve with a local HTTP server: + +```bash +# Using Python (recommended) +python3 -m http.server 8000 + +# Or using Node.js +npx serve . + +# Or using PHP +php -S localhost:8000 +``` + +Then open your browser to: + +``` +http://localhost:8000/example/wasm-sgr/ +``` diff --git a/example/wasm-sgr/index.html b/example/wasm-sgr/index.html new file mode 100644 index 000000000..e62b26c7e --- /dev/null +++ b/example/wasm-sgr/index.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/include/ghostty/vt/color.h b/include/ghostty/vt/color.h index 9e7fe6f4d..0d57b8db4 100644 --- a/include/ghostty/vt/color.h +++ b/include/ghostty/vt/color.h @@ -70,6 +70,25 @@ typedef uint8_t GhosttyColorPaletteIndex; /** @} */ +/** + * 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 diff --git a/include/ghostty/vt/sgr.h b/include/ghostty/vt/sgr.h index a296a280a..0c1afc309 100644 --- a/include/ghostty/vt/sgr.h +++ b/include/ghostty/vt/sgr.h @@ -18,9 +18,9 @@ * * 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. + * 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 * @@ -35,18 +35,18 @@ * #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)) { @@ -61,7 +61,7 @@ * break; * } * } - * + * * // Cleanup * ghostty_sgr_free(parser); * return 0; @@ -71,12 +71,12 @@ * @{ */ +#include +#include +#include #include #include #include -#include -#include -#include #ifdef __cplusplus extern "C" { @@ -84,13 +84,13 @@ extern "C" { /** * 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; +typedef struct GhosttySgrParser* GhosttySgrParser; /** * SGR attribute tags. @@ -158,9 +158,9 @@ typedef enum { * @ingroup sgr */ typedef struct { - const uint16_t *full_ptr; + const uint16_t* full_ptr; size_t full_len; - const uint16_t *partial_ptr; + const uint16_t* partial_ptr; size_t partial_len; } GhosttySgrUnknown; @@ -196,7 +196,7 @@ typedef union { * 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 + * identified by tag alone; the value union is not used for these and * the memory in the value field is undefined. * * @ingroup sgr @@ -208,94 +208,182 @@ typedef struct { /** * 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 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); +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 + * 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 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); +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); +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 } diff --git a/include/ghostty/vt/wasm.h b/include/ghostty/vt/wasm.h index 7edee529f..37a826326 100644 --- a/include/ghostty/vt/wasm.h +++ b/include/ghostty/vt/wasm.h @@ -47,7 +47,7 @@ * * // Allocate output buffer and size pointer * const bufferSize = 32; - * const bufPtr = exports.ghostty_wasm_alloc_buffer(bufferSize); + * const bufPtr = exports.ghostty_wasm_alloc_u8_array(bufferSize); * const writtenPtr = exports.ghostty_wasm_alloc_usize(); * * // Encode the key event @@ -85,22 +85,40 @@ void** ghostty_wasm_alloc_opaque(void); void ghostty_wasm_free_opaque(void **ptr); /** - * Allocate a buffer of the specified length. + * Allocate an array of uint8_t values. * - * @param len Number of bytes to allocate - * @return Pointer to allocated buffer, or NULL if allocation failed + * @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_buffer(size_t len); +uint8_t* ghostty_wasm_alloc_u8_array(size_t len); /** - * Free a buffer allocated by ghostty_wasm_alloc_buffer(). + * Free an array allocated by ghostty_wasm_alloc_u8_array(). * - * @param ptr Pointer to the buffer to free, or NULL (NULL is safely ignored) - * @param len Length of the buffer (must match the length passed to alloc) + * @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_buffer(uint8_t *ptr, size_t len); +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. diff --git a/src/lib/allocator/convenience.zig b/src/lib/allocator/convenience.zig index 19543ad0e..0f5f88d29 100644 --- a/src/lib/allocator/convenience.zig +++ b/src/lib/allocator/convenience.zig @@ -24,12 +24,21 @@ pub fn freeOpaque(ptr: ?*Opaque) callconv(.c) void { if (ptr) |p| alloc.destroy(p); } -pub fn allocBuffer(len: usize) callconv(.c) ?[*]u8 { +pub fn allocU8Array(len: usize) callconv(.c) ?[*]u8 { const slice = alloc.alloc(u8, len) catch return null; return slice.ptr; } -pub fn freeBuffer(ptr: ?[*]u8, len: usize) callconv(.c) void { +pub fn freeU8Array(ptr: ?[*]u8, len: usize) callconv(.c) void { + if (ptr) |p| alloc.free(p[0..len]); +} + +pub fn allocU16Array(len: usize) callconv(.c) ?[*]u16 { + const slice = alloc.alloc(u16, len) catch return null; + return slice.ptr; +} + +pub fn freeU16Array(ptr: ?[*]u16, len: usize) callconv(.c) void { if (ptr) |p| alloc.free(p[0..len]); } diff --git a/src/lib_vt.zig b/src/lib_vt.zig index e1aa69659..aa37c6110 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -126,23 +126,32 @@ comptime { @export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" }); @export(&c.osc_command_data, .{ .name = "ghostty_osc_command_data" }); @export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" }); + @export(&c.color_rgb_get, .{ .name = "ghostty_color_rgb_get" }); @export(&c.sgr_new, .{ .name = "ghostty_sgr_new" }); @export(&c.sgr_free, .{ .name = "ghostty_sgr_free" }); @export(&c.sgr_reset, .{ .name = "ghostty_sgr_reset" }); @export(&c.sgr_set_params, .{ .name = "ghostty_sgr_set_params" }); @export(&c.sgr_next, .{ .name = "ghostty_sgr_next" }); + @export(&c.sgr_unknown_full, .{ .name = "ghostty_sgr_unknown_full" }); + @export(&c.sgr_unknown_partial, .{ .name = "ghostty_sgr_unknown_partial" }); + @export(&c.sgr_attribute_tag, .{ .name = "ghostty_sgr_attribute_tag" }); + @export(&c.sgr_attribute_value, .{ .name = "ghostty_sgr_attribute_value" }); // On Wasm we need to export our allocator convenience functions. if (builtin.target.cpu.arch.isWasm()) { const alloc = @import("lib/allocator/convenience.zig"); @export(&alloc.allocOpaque, .{ .name = "ghostty_wasm_alloc_opaque" }); @export(&alloc.freeOpaque, .{ .name = "ghostty_wasm_free_opaque" }); - @export(&alloc.allocBuffer, .{ .name = "ghostty_wasm_alloc_buffer" }); - @export(&alloc.freeBuffer, .{ .name = "ghostty_wasm_free_buffer" }); + @export(&alloc.allocU8Array, .{ .name = "ghostty_wasm_alloc_u8_array" }); + @export(&alloc.freeU8Array, .{ .name = "ghostty_wasm_free_u8_array" }); + @export(&alloc.allocU16Array, .{ .name = "ghostty_wasm_alloc_u16_array" }); + @export(&alloc.freeU16Array, .{ .name = "ghostty_wasm_free_u16_array" }); @export(&alloc.allocU8, .{ .name = "ghostty_wasm_alloc_u8" }); @export(&alloc.freeU8, .{ .name = "ghostty_wasm_free_u8" }); @export(&alloc.allocUsize, .{ .name = "ghostty_wasm_alloc_usize" }); @export(&alloc.freeUsize, .{ .name = "ghostty_wasm_free_usize" }); + @export(&c.wasm_alloc_sgr_attribute, .{ .name = "ghostty_wasm_alloc_sgr_attribute" }); + @export(&c.wasm_free_sgr_attribute, .{ .name = "ghostty_wasm_free_sgr_attribute" }); } } } diff --git a/src/terminal/c/color.zig b/src/terminal/c/color.zig new file mode 100644 index 000000000..199339706 --- /dev/null +++ b/src/terminal/c/color.zig @@ -0,0 +1,12 @@ +const color = @import("../color.zig"); + +pub fn rgb_get( + c: color.RGB.C, + r: *u8, + g: *u8, + b: *u8, +) callconv(.c) void { + r.* = c.r; + g.* = c.g; + b.* = c.b; +} diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index b935b264d..bc92597f5 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -1,3 +1,4 @@ +pub const color = @import("color.zig"); pub const osc = @import("osc.zig"); pub const key_event = @import("key_event.zig"); pub const key_encode = @import("key_encode.zig"); @@ -13,11 +14,19 @@ pub const osc_end = osc.end; pub const osc_command_type = osc.commandType; pub const osc_command_data = osc.commandData; +pub const color_rgb_get = color.rgb_get; + pub const sgr_new = sgr.new; pub const sgr_free = sgr.free; pub const sgr_reset = sgr.reset; pub const sgr_set_params = sgr.setParams; pub const sgr_next = sgr.next; +pub const sgr_unknown_full = sgr.unknown_full; +pub const sgr_unknown_partial = sgr.unknown_partial; +pub const sgr_attribute_tag = sgr.attribute_tag; +pub const sgr_attribute_value = sgr.attribute_value; +pub const wasm_alloc_sgr_attribute = sgr.wasm_alloc_attribute; +pub const wasm_free_sgr_attribute = sgr.wasm_free_attribute; pub const key_event_new = key_event.new; pub const key_event_free = key_event.free; @@ -44,6 +53,7 @@ pub const key_encoder_encode = key_encode.encode; pub const paste_is_safe = paste.is_safe; test { + _ = color; _ = osc; _ = key_event; _ = key_encode; diff --git a/src/terminal/c/sgr.zig b/src/terminal/c/sgr.zig index f01a8a25b..e65b9e3ee 100644 --- a/src/terminal/c/sgr.zig +++ b/src/terminal/c/sgr.zig @@ -100,6 +100,45 @@ pub fn next( return false; } +pub fn unknown_full( + unknown: sgr.Attribute.Unknown.C, + ptr: ?*[*]const u16, +) callconv(.c) usize { + if (ptr) |p| p.* = unknown.full_ptr; + return unknown.full_len; +} + +pub fn unknown_partial( + unknown: sgr.Attribute.Unknown.C, + ptr: ?*[*]const u16, +) callconv(.c) usize { + if (ptr) |p| p.* = unknown.partial_ptr; + return unknown.partial_len; +} + +pub fn attribute_tag( + attr: sgr.Attribute.C, +) callconv(.c) sgr.Attribute.Tag { + return attr.tag; +} + +pub fn attribute_value( + attr: *sgr.Attribute.C, +) callconv(.c) *sgr.Attribute.CValue { + return &attr.value; +} + +pub fn wasm_alloc_attribute() callconv(.c) *sgr.Attribute.C { + const alloc = std.heap.wasm_allocator; + const ptr = alloc.create(sgr.Attribute.C) catch @panic("out of memory"); + return ptr; +} + +pub fn wasm_free_attribute(attr: *sgr.Attribute.C) callconv(.c) void { + const alloc = std.heap.wasm_allocator; + alloc.destroy(attr); +} + test "alloc" { var p: Parser = undefined; try testing.expectEqual(Result.success, new(