lib-vt: Wasm SGR helpers and example (#9362)

This adds some convenience functions for parsing SGR sequences
WebAssembly and adds an example demonstrating SGR parsing in the
browser.
pull/9364/head
Mitchell Hashimoto 2025-10-26 13:19:55 -07:00 committed by GitHub
parent 19d1377659
commit 7d7c0bf5cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 759 additions and 59 deletions

View File

@ -458,7 +458,7 @@
// Set UTF-8 text from the key event (the actual character produced) // Set UTF-8 text from the key event (the actual character produced)
if (event.key.length === 1) { if (event.key.length === 1) {
const utf8Bytes = new TextEncoder().encode(event.key); 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); new Uint8Array(getBuffer()).set(utf8Bytes, utf8Ptr);
wasmInstance.exports.ghostty_key_event_set_utf8(eventPtr, utf8Ptr, utf8Bytes.length); wasmInstance.exports.ghostty_key_event_set_utf8(eventPtr, utf8Ptr, utf8Bytes.length);
} }
@ -477,7 +477,7 @@
const required = new DataView(getBuffer()).getUint32(requiredPtr, true); 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 writtenPtr = wasmInstance.exports.ghostty_wasm_alloc_usize();
const encodeResult = wasmInstance.exports.ghostty_key_encoder_encode( const encodeResult = wasmInstance.exports.ghostty_key_encoder_encode(
encoderPtr, eventPtr, bufPtr, required, writtenPtr encoderPtr, eventPtr, bufPtr, required, writtenPtr

View File

@ -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/
```

457
example/wasm-sgr/index.html Normal file
View File

@ -0,0 +1,457 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ghostty VT SGR Parser - WebAssembly Example</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 900px;
margin: 40px auto;
padding: 0 20px;
line-height: 1.6;
}
h1 {
color: #333;
}
.input-section {
background: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
}
.input-section h3 {
margin-top: 0;
margin-bottom: 10px;
font-size: 16px;
}
textarea {
width: 100%;
padding: 10px;
font-family: 'Courier New', monospace;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
resize: vertical;
}
button {
background: #0066cc;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
}
button:hover {
background: #0052a3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.output {
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
font-size: 14px;
}
.error {
background: #fee;
border-color: #faa;
color: #c00;
}
.status {
color: #666;
font-size: 14px;
margin: 10px 0;
}
.attribute {
padding: 8px;
margin: 5px 0;
background: white;
border-left: 3px solid #0066cc;
}
.attribute-name {
font-weight: bold;
color: #0066cc;
}
</style>
</head>
<body>
<h1>Ghostty VT SGR Parser - WebAssembly Example</h1>
<p>This example demonstrates parsing terminal SGR (Select Graphic Rendition) sequences using the Ghostty VT WebAssembly module.</p>
<div class="status" id="status">Loading WebAssembly module...</div>
<div class="input-section">
<h3>SGR Sequence</h3>
<label for="sequence">Enter SGR sequence (numbers separated by ':' or ';'):</label>
<textarea id="sequence" rows="2" disabled>4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136</textarea>
<p style="font-size: 13px; color: #666; margin-top: 5px;">The parser runs live as you type.</p>
</div>
<div id="output" class="output">Waiting for input...</div>
<p><strong>Note:</strong> This example must be served via HTTP (not opened directly as a file). See the README for instructions.</p>
<script>
let wasmInstance = null;
let wasmMemory = null;
async function loadWasm() {
try {
const response = await fetch('../../zig-out/bin/ghostty-vt.wasm');
const wasmBytes = await response.arrayBuffer();
const wasmModule = await WebAssembly.instantiate(wasmBytes, {
env: {
log: (ptr, len) => {
const bytes = new Uint8Array(wasmModule.instance.exports.memory.buffer, ptr, len);
const text = new TextDecoder().decode(bytes);
console.log('[wasm]', text);
}
}
});
wasmInstance = wasmModule.instance;
wasmMemory = wasmInstance.exports.memory;
return true;
} catch (e) {
console.error('Failed to load WASM:', e);
if (window.location.protocol === 'file:') {
throw new Error('Cannot load WASM from file:// protocol. Please serve via HTTP (see README)');
}
return false;
}
}
function getBuffer() {
return wasmMemory.buffer;
}
// SGR attribute tag values from include/ghostty/vt/sgr.h
const SGR_ATTR_TAGS = {
UNSET: 0,
UNKNOWN: 1,
BOLD: 2,
RESET_BOLD: 3,
ITALIC: 4,
RESET_ITALIC: 5,
FAINT: 6,
UNDERLINE: 7,
RESET_UNDERLINE: 8,
UNDERLINE_COLOR: 9,
UNDERLINE_COLOR_256: 10,
RESET_UNDERLINE_COLOR: 11,
OVERLINE: 12,
RESET_OVERLINE: 13,
BLINK: 14,
RESET_BLINK: 15,
INVERSE: 16,
RESET_INVERSE: 17,
INVISIBLE: 18,
RESET_INVISIBLE: 19,
STRIKETHROUGH: 20,
RESET_STRIKETHROUGH: 21,
DIRECT_COLOR_FG: 22,
DIRECT_COLOR_BG: 23,
BG_8: 24,
FG_8: 25,
RESET_FG: 26,
RESET_BG: 27,
BRIGHT_BG_8: 28,
BRIGHT_FG_8: 29,
BG_256: 30,
FG_256: 31
};
// Underline style values
const UNDERLINE_STYLES = {
0: 'none',
1: 'single',
2: 'double',
3: 'curly',
4: 'dotted',
5: 'dashed'
};
function getTagName(tag) {
for (const [name, value] of Object.entries(SGR_ATTR_TAGS)) {
if (value === tag) return name;
}
return `UNKNOWN(${tag})`;
}
function parseSGR() {
const outputDiv = document.getElementById('output');
try {
const sequenceText = document.getElementById('sequence').value.trim();
if (!sequenceText) {
outputDiv.className = 'output';
outputDiv.textContent = 'Enter an SGR sequence to parse...';
return;
}
// Parse the raw sequence into parameters and separators
const params = [];
const separators = [];
let currentNum = '';
for (let i = 0; i < sequenceText.length; i++) {
const char = sequenceText[i];
if (char === ':' || char === ';') {
if (currentNum) {
const num = parseInt(currentNum, 10);
if (isNaN(num) || num < 0 || num > 65535) {
throw new Error(`Invalid parameter: ${currentNum}`);
}
params.push(num);
separators.push(char);
currentNum = '';
}
} else if (char >= '0' && char <= '9') {
currentNum += char;
} else if (char !== ' ' && char !== '\t' && char !== '\n') {
throw new Error(`Invalid character in sequence: '${char}'`);
}
}
// Don't forget the last number
if (currentNum) {
const num = parseInt(currentNum, 10);
if (isNaN(num) || num < 0 || num > 65535) {
throw new Error(`Invalid parameter: ${currentNum}`);
}
params.push(num);
}
if (params.length === 0) {
outputDiv.className = 'output error';
outputDiv.textContent = 'Error: No parameters found in sequence';
return;
}
// Create SGR parser
const parserPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
const result = wasmInstance.exports.ghostty_sgr_new(0, parserPtrPtr);
if (result !== 0) {
throw new Error(`ghostty_sgr_new failed with result ${result}`);
}
const parserPtr = new DataView(getBuffer()).getUint32(parserPtrPtr, true);
// Allocate and set parameters
const paramsPtr = wasmInstance.exports.ghostty_wasm_alloc_u16_array(params.length);
const paramsView = new Uint16Array(getBuffer(), paramsPtr, params.length);
params.forEach((p, i) => paramsView[i] = p);
// Allocate and set separators (or use null if empty)
let sepsPtr = 0;
if (separators.length > 0) {
sepsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(separators.length);
const sepsView = new Uint8Array(getBuffer(), sepsPtr, separators.length);
separators.forEach((s, i) => sepsView[i] = s.charCodeAt(0));
}
// Set parameters in parser
const setResult = wasmInstance.exports.ghostty_sgr_set_params(
parserPtr,
paramsPtr,
sepsPtr,
params.length
);
if (setResult !== 0) {
throw new Error(`ghostty_sgr_set_params failed with result ${setResult}`);
}
// Build output
let output = 'Parsing SGR sequence:\n';
output += 'ESC[';
params.forEach((p, i) => {
if (i > 0) output += separators[i - 1];
output += p;
});
output += 'm\n\n';
// Iterate through attributes
const attrPtr = wasmInstance.exports.ghostty_wasm_alloc_sgr_attribute();
let count = 0;
while (wasmInstance.exports.ghostty_sgr_next(parserPtr, attrPtr)) {
count++;
// Use the new ghostty_sgr_attribute_tag getter function
const tag = wasmInstance.exports.ghostty_sgr_attribute_tag(attrPtr);
// Use ghostty_sgr_attribute_value to get a pointer to the value union
const valuePtr = wasmInstance.exports.ghostty_sgr_attribute_value(attrPtr);
output += `Attribute ${count}: `;
switch (tag) {
case SGR_ATTR_TAGS.UNDERLINE: {
const view = new DataView(getBuffer(), valuePtr, 4);
const style = view.getUint32(0, true);
output += `Underline style = ${UNDERLINE_STYLES[style] || `unknown(${style})`}\n`;
break;
}
case SGR_ATTR_TAGS.DIRECT_COLOR_FG: {
// Use ghostty_color_rgb_get to extract RGB components
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
const r = new Uint8Array(getBuffer(), rPtr, 1)[0];
const g = new Uint8Array(getBuffer(), gPtr, 1)[0];
const b = new Uint8Array(getBuffer(), bPtr, 1)[0];
output += `Foreground RGB = (${r}, ${g}, ${b})\n`;
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
break;
}
case SGR_ATTR_TAGS.DIRECT_COLOR_BG: {
// Use ghostty_color_rgb_get to extract RGB components
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
const r = new Uint8Array(getBuffer(), rPtr, 1)[0];
const g = new Uint8Array(getBuffer(), gPtr, 1)[0];
const b = new Uint8Array(getBuffer(), bPtr, 1)[0];
output += `Background RGB = (${r}, ${g}, ${b})\n`;
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
break;
}
case SGR_ATTR_TAGS.UNDERLINE_COLOR: {
// Use ghostty_color_rgb_get to extract RGB components
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
const r = new Uint8Array(getBuffer(), rPtr, 1)[0];
const g = new Uint8Array(getBuffer(), gPtr, 1)[0];
const b = new Uint8Array(getBuffer(), bPtr, 1)[0];
output += `Underline color RGB = (${r}, ${g}, ${b})\n`;
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
break;
}
case SGR_ATTR_TAGS.FG_8:
case SGR_ATTR_TAGS.BG_8:
case SGR_ATTR_TAGS.FG_256:
case SGR_ATTR_TAGS.BG_256:
case SGR_ATTR_TAGS.UNDERLINE_COLOR_256: {
const view = new DataView(getBuffer(), valuePtr, 1);
const color = view.getUint8(0);
const colorType = tag === SGR_ATTR_TAGS.FG_8 ? 'Foreground 8-color' :
tag === SGR_ATTR_TAGS.BG_8 ? 'Background 8-color' :
tag === SGR_ATTR_TAGS.FG_256 ? 'Foreground 256-color' :
tag === SGR_ATTR_TAGS.BG_256 ? 'Background 256-color' :
'Underline 256-color';
output += `${colorType} = ${color}\n`;
break;
}
case SGR_ATTR_TAGS.BOLD:
output += 'Bold\n';
break;
case SGR_ATTR_TAGS.ITALIC:
output += 'Italic\n';
break;
case SGR_ATTR_TAGS.UNSET:
output += 'Reset all attributes\n';
break;
case SGR_ATTR_TAGS.UNKNOWN:
output += 'Unknown attribute\n';
break;
default:
output += `Other attribute (tag=${getTagName(tag)})\n`;
break;
}
}
output += `\nTotal attributes parsed: ${count}`;
outputDiv.className = 'output';
outputDiv.textContent = output;
// Cleanup
wasmInstance.exports.ghostty_wasm_free_sgr_attribute(attrPtr);
wasmInstance.exports.ghostty_sgr_free(parserPtr);
} catch (e) {
console.error('Parse error:', e);
outputDiv.className = 'output error';
outputDiv.textContent = `Error: ${e.message}\n\nStack trace:\n${e.stack}`;
}
}
async function init() {
const statusDiv = document.getElementById('status');
const sequenceInput = document.getElementById('sequence');
try {
statusDiv.textContent = 'Loading WebAssembly module...';
const loaded = await loadWasm();
if (!loaded) {
throw new Error('Failed to load WebAssembly module');
}
statusDiv.textContent = '';
sequenceInput.disabled = false;
// Parse live as user types
sequenceInput.addEventListener('input', parseSGR);
// Parse the default example on load
parseSGR();
} catch (e) {
statusDiv.textContent = `Error: ${e.message}`;
statusDiv.style.color = '#c00';
}
}
window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>

View File

@ -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 #ifdef __cplusplus
} }
#endif #endif

View File

@ -71,12 +71,12 @@
* @{ * @{
*/ */
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/color.h>
#include <ghostty/vt/result.h>
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
#include <ghostty/vt/result.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/color.h>
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@ -90,7 +90,7 @@ extern "C" {
* *
* @ingroup sgr * @ingroup sgr
*/ */
typedef struct GhosttySgrParser *GhosttySgrParser; typedef struct GhosttySgrParser* GhosttySgrParser;
/** /**
* SGR attribute tags. * SGR attribute tags.
@ -158,9 +158,9 @@ typedef enum {
* @ingroup sgr * @ingroup sgr
*/ */
typedef struct { typedef struct {
const uint16_t *full_ptr; const uint16_t* full_ptr;
size_t full_len; size_t full_len;
const uint16_t *partial_ptr; const uint16_t* partial_ptr;
size_t partial_len; size_t partial_len;
} GhosttySgrUnknown; } GhosttySgrUnknown;
@ -213,13 +213,15 @@ typedef struct {
* allocator. The parser must be freed using ghostty_sgr_free() when * allocator. The parser must be freed using ghostty_sgr_free() when
* no longer needed. * 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 * @param parser Pointer to store the created parser handle
* @return GHOSTTY_SUCCESS on success, or an error code on failure * @return GHOSTTY_SUCCESS on success, or an error code on failure
* *
* @ingroup sgr * @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. * Free an SGR parser instance.
@ -270,17 +272,17 @@ void ghostty_sgr_reset(GhosttySgrParser parser);
* *
* @param parser The parser handle, must not be NULL * @param parser The parser handle, must not be NULL
* @param params Array of SGR parameter values * @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) * @param len Number of parameters (and separators if provided)
* @return GHOSTTY_SUCCESS on success, or an error code on failure * @return GHOSTTY_SUCCESS on success, or an error code on failure
* *
* @ingroup sgr * @ingroup sgr
*/ */
GhosttyResult ghostty_sgr_set_params( GhosttyResult ghostty_sgr_set_params(GhosttySgrParser parser,
GhosttySgrParser parser, const uint16_t* params,
const uint16_t *params, const char* separators,
const char *separators, size_t len);
size_t len);
/** /**
* Get the next SGR attribute. * Get the next SGR attribute.
@ -295,7 +297,93 @@ GhosttyResult ghostty_sgr_set_params(
* *
* @ingroup sgr * @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 #ifdef __cplusplus
} }

View File

@ -47,7 +47,7 @@
* *
* // Allocate output buffer and size pointer * // Allocate output buffer and size pointer
* const bufferSize = 32; * 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(); * const writtenPtr = exports.ghostty_wasm_alloc_usize();
* *
* // Encode the key event * // Encode the key event
@ -85,22 +85,40 @@ void** ghostty_wasm_alloc_opaque(void);
void ghostty_wasm_free_opaque(void **ptr); 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 * @param len Number of uint8_t elements to allocate
* @return Pointer to allocated buffer, or NULL if allocation failed * @return Pointer to allocated array, or NULL if allocation failed
* @ingroup wasm * @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 ptr Pointer to the array to free, or NULL (NULL is safely ignored)
* @param len Length of the buffer (must match the length passed to alloc) * @param len Length of the array (must match the length passed to alloc)
* @ingroup wasm * @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. * Allocate a single uint8_t value.

View File

@ -24,12 +24,21 @@ pub fn freeOpaque(ptr: ?*Opaque) callconv(.c) void {
if (ptr) |p| alloc.destroy(p); 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; const slice = alloc.alloc(u8, len) catch return null;
return slice.ptr; 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]); if (ptr) |p| alloc.free(p[0..len]);
} }

View File

@ -126,23 +126,32 @@ comptime {
@export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" }); @export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" });
@export(&c.osc_command_data, .{ .name = "ghostty_osc_command_data" }); @export(&c.osc_command_data, .{ .name = "ghostty_osc_command_data" });
@export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" }); @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_new, .{ .name = "ghostty_sgr_new" });
@export(&c.sgr_free, .{ .name = "ghostty_sgr_free" }); @export(&c.sgr_free, .{ .name = "ghostty_sgr_free" });
@export(&c.sgr_reset, .{ .name = "ghostty_sgr_reset" }); @export(&c.sgr_reset, .{ .name = "ghostty_sgr_reset" });
@export(&c.sgr_set_params, .{ .name = "ghostty_sgr_set_params" }); @export(&c.sgr_set_params, .{ .name = "ghostty_sgr_set_params" });
@export(&c.sgr_next, .{ .name = "ghostty_sgr_next" }); @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. // On Wasm we need to export our allocator convenience functions.
if (builtin.target.cpu.arch.isWasm()) { if (builtin.target.cpu.arch.isWasm()) {
const alloc = @import("lib/allocator/convenience.zig"); const alloc = @import("lib/allocator/convenience.zig");
@export(&alloc.allocOpaque, .{ .name = "ghostty_wasm_alloc_opaque" }); @export(&alloc.allocOpaque, .{ .name = "ghostty_wasm_alloc_opaque" });
@export(&alloc.freeOpaque, .{ .name = "ghostty_wasm_free_opaque" }); @export(&alloc.freeOpaque, .{ .name = "ghostty_wasm_free_opaque" });
@export(&alloc.allocBuffer, .{ .name = "ghostty_wasm_alloc_buffer" }); @export(&alloc.allocU8Array, .{ .name = "ghostty_wasm_alloc_u8_array" });
@export(&alloc.freeBuffer, .{ .name = "ghostty_wasm_free_buffer" }); @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.allocU8, .{ .name = "ghostty_wasm_alloc_u8" });
@export(&alloc.freeU8, .{ .name = "ghostty_wasm_free_u8" }); @export(&alloc.freeU8, .{ .name = "ghostty_wasm_free_u8" });
@export(&alloc.allocUsize, .{ .name = "ghostty_wasm_alloc_usize" }); @export(&alloc.allocUsize, .{ .name = "ghostty_wasm_alloc_usize" });
@export(&alloc.freeUsize, .{ .name = "ghostty_wasm_free_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" });
} }
} }
} }

12
src/terminal/c/color.zig Normal file
View File

@ -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;
}

View File

@ -1,3 +1,4 @@
pub const color = @import("color.zig");
pub const osc = @import("osc.zig"); pub const osc = @import("osc.zig");
pub const key_event = @import("key_event.zig"); pub const key_event = @import("key_event.zig");
pub const key_encode = @import("key_encode.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_type = osc.commandType;
pub const osc_command_data = osc.commandData; pub const osc_command_data = osc.commandData;
pub const color_rgb_get = color.rgb_get;
pub const sgr_new = sgr.new; pub const sgr_new = sgr.new;
pub const sgr_free = sgr.free; pub const sgr_free = sgr.free;
pub const sgr_reset = sgr.reset; pub const sgr_reset = sgr.reset;
pub const sgr_set_params = sgr.setParams; pub const sgr_set_params = sgr.setParams;
pub const sgr_next = sgr.next; 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_new = key_event.new;
pub const key_event_free = key_event.free; 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; pub const paste_is_safe = paste.is_safe;
test { test {
_ = color;
_ = osc; _ = osc;
_ = key_event; _ = key_event;
_ = key_encode; _ = key_encode;

View File

@ -100,6 +100,45 @@ pub fn next(
return false; 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" { test "alloc" {
var p: Parser = undefined; var p: Parser = undefined;
try testing.expectEqual(Result.success, new( try testing.expectEqual(Result.success, new(