feat(vt-capi): port the libghostty-vt C API

Ports ZIG/src/lib_vt.zig and terminal/c/* as crates/ghostty-vt-capi
(cdylib + staticlib + rlib, soname libghostty-vt.so) implementing the
unchanged include/ghostty/vt.h + vt/*.h ABI: OSC parser, SGR parser,
key events and key encoding, paste safety, colors, result codes - all
38 native symbols, plus the ghostty_wasm_* helpers on wasm32.

Verification:
- tests/abi.rs generates a C probe from the headers, compiled with cc,
  that checks sizeof/alignof/offsetof of every struct/union, every
  enumerator and #define against Rust, and links every declared symbol;
  nm -D exports equal the declared functions.
- Ghostty's example/c-vt* programs compile unchanged against the Rust
  library (shared and static) and produce the expected output.
- The wasm32 build runs Ghostty's wasm-key-encode/wasm-sgr example
  scripts unmodified under node (upstream's wasm-sgr tag table is stale
  vs sgr.h; a regenerated copy checks full output).

Test Plan:
- cargo test -p ghostty-vt-capi (22 unit + 2 ABI + 5 C + 3 wasm tests)
This commit is contained in:
ddidderr committed 2026-10-12 05:16:41 +02:00
1 parent 5b9a0723a9
commit 75252a7651
35 files changed
+6911

No files matched your search

+57
View File
@@ -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"
+14
View File
@@ -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");
}
@@ -0,0 +1,59 @@
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <ghostty/vt.h>
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;
}
@@ -0,0 +1,31 @@
#include <stdio.h>
#include <string.h>
#include <ghostty/vt.h>
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;
}
@@ -0,0 +1,131 @@
#include <assert.h>
#include <stdio.h>
#include <ghostty/vt.h>
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;
}
+36
View File
@@ -0,0 +1,36 @@
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <ghostty/vt.h>
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;
}
@@ -0,0 +1,687 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ghostty VT Key Encoder - WebAssembly Example</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 0 20px;
line-height: 1.6;
}
h1 {
color: #333;
}
.output {
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
word-break: break-all;
}
.error {
background: #fee;
border-color: #faa;
color: #c00;
}
button {
background: #0066cc;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #0052a3;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.key-input {
width: 100%;
padding: 15px;
font-size: 16px;
border: 2px solid #0066cc;
border-radius: 4px;
margin: 20px 0;
box-sizing: border-box;
}
.key-input:focus {
outline: none;
border-color: #0052a3;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
.status {
color: #666;
font-size: 14px;
margin: 10px 0;
}
.controls {
background: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
}
.controls h3 {
margin-top: 0;
margin-bottom: 10px;
font-size: 16px;
}
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 15px;
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
}
.checkbox-group input[type="checkbox"] {
cursor: pointer;
}
.radio-group {
display: flex;
gap: 15px;
}
.radio-group label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
}
.radio-group input[type="radio"] {
cursor: pointer;
}
.warning {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 4px;
padding: 15px;
margin: 20px 0;
color: #856404;
}
.warning strong {
display: block;
margin-bottom: 5px;
}
</style>
</head>
<body>
<h1>Ghostty VT Key Encoder - WebAssembly Example</h1>
<p>This example demonstrates encoding key events into terminal escape sequences using the Ghostty VT WebAssembly module.</p>
<div class="warning">
<strong>⚠️ Warning:</strong>
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.
</div>
<div class="status" id="status">Loading WebAssembly module...</div>
<div class="controls">
<h3>Key Action</h3>
<div class="radio-group">
<label><input type="radio" name="action" value="1" checked> Press</label>
<label><input type="radio" name="action" value="0"> Release</label>
<label><input type="radio" name="action" value="2"> Repeat</label>
</div>
</div>
<div class="controls">
<h3>Kitty Keyboard Protocol Flags</h3>
<div class="checkbox-group">
<label><input type="checkbox" id="flag_disambiguate" checked> Disambiguate</label>
<label><input type="checkbox" id="flag_report_events" checked> Report Events</label>
<label><input type="checkbox" id="flag_report_alternates" checked> Report Alternates</label>
<label><input type="checkbox" id="flag_report_all_as_escapes" checked> Report All As Escapes</label>
<label><input type="checkbox" id="flag_report_text" checked> Report Text</label>
</div>
</div>
<input type="text" class="key-input" id="keyInput" placeholder="Focus here and press any key combination (e.g., Ctrl+A, Shift+Enter)..." disabled>
<div id="output" class="output">Waiting for key events...</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;
let encoderPtr = null;
let lastKeyEvent = null;
async function loadWasm() {
try {
// Load the wasm module - adjust path as needed
const response = await fetch('../../zig-out/bin/ghostty-vt.wasm');
const wasmBytes = await response.arrayBuffer();
// Instantiate the wasm module
const wasmModule = await WebAssembly.instantiate(wasmBytes, {
env: {
// Logging function for wasm module
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;
}
function formatHex(bytes) {
return Array.from(bytes)
.map(b => b.toString(16).padStart(2, '0'))
.join(' ');
}
function formatString(bytes) {
let result = '';
for (let i = 0; i < bytes.length; i++) {
if (bytes[i] === 0x1b) {
result += '\\x1b';
} else {
result += String.fromCharCode(bytes[i]);
}
}
return result;
}
// Map W3C KeyboardEvent.code values to Ghostty key codes
// Based on include/ghostty/vt/key/event.h
const keyCodeMap = {
// Writing System Keys
'Backquote': 1, // GHOSTTY_KEY_BACKQUOTE
'Backslash': 2, // GHOSTTY_KEY_BACKSLASH
'BracketLeft': 3, // GHOSTTY_KEY_BRACKET_LEFT
'BracketRight': 4, // GHOSTTY_KEY_BRACKET_RIGHT
'Comma': 5, // GHOSTTY_KEY_COMMA
'Digit0': 6, // GHOSTTY_KEY_DIGIT_0
'Digit1': 7, // GHOSTTY_KEY_DIGIT_1
'Digit2': 8, // GHOSTTY_KEY_DIGIT_2
'Digit3': 9, // GHOSTTY_KEY_DIGIT_3
'Digit4': 10, // GHOSTTY_KEY_DIGIT_4
'Digit5': 11, // GHOSTTY_KEY_DIGIT_5
'Digit6': 12, // GHOSTTY_KEY_DIGIT_6
'Digit7': 13, // GHOSTTY_KEY_DIGIT_7
'Digit8': 14, // GHOSTTY_KEY_DIGIT_8
'Digit9': 15, // GHOSTTY_KEY_DIGIT_9
'Equal': 16, // GHOSTTY_KEY_EQUAL
'IntlBackslash': 17, // GHOSTTY_KEY_INTL_BACKSLASH
'IntlRo': 18, // GHOSTTY_KEY_INTL_RO
'IntlYen': 19, // GHOSTTY_KEY_INTL_YEN
'KeyA': 20, // GHOSTTY_KEY_A
'KeyB': 21, // GHOSTTY_KEY_B
'KeyC': 22, // GHOSTTY_KEY_C
'KeyD': 23, // GHOSTTY_KEY_D
'KeyE': 24, // GHOSTTY_KEY_E
'KeyF': 25, // GHOSTTY_KEY_F
'KeyG': 26, // GHOSTTY_KEY_G
'KeyH': 27, // GHOSTTY_KEY_H
'KeyI': 28, // GHOSTTY_KEY_I
'KeyJ': 29, // GHOSTTY_KEY_J
'KeyK': 30, // GHOSTTY_KEY_K
'KeyL': 31, // GHOSTTY_KEY_L
'KeyM': 32, // GHOSTTY_KEY_M
'KeyN': 33, // GHOSTTY_KEY_N
'KeyO': 34, // GHOSTTY_KEY_O
'KeyP': 35, // GHOSTTY_KEY_P
'KeyQ': 36, // GHOSTTY_KEY_Q
'KeyR': 37, // GHOSTTY_KEY_R
'KeyS': 38, // GHOSTTY_KEY_S
'KeyT': 39, // GHOSTTY_KEY_T
'KeyU': 40, // GHOSTTY_KEY_U
'KeyV': 41, // GHOSTTY_KEY_V
'KeyW': 42, // GHOSTTY_KEY_W
'KeyX': 43, // GHOSTTY_KEY_X
'KeyY': 44, // GHOSTTY_KEY_Y
'KeyZ': 45, // GHOSTTY_KEY_Z
'Minus': 46, // GHOSTTY_KEY_MINUS
'Period': 47, // GHOSTTY_KEY_PERIOD
'Quote': 48, // GHOSTTY_KEY_QUOTE
'Semicolon': 49, // GHOSTTY_KEY_SEMICOLON
'Slash': 50, // GHOSTTY_KEY_SLASH
// Functional Keys
'AltLeft': 51, // GHOSTTY_KEY_ALT_LEFT
'AltRight': 52, // GHOSTTY_KEY_ALT_RIGHT
'Backspace': 53, // GHOSTTY_KEY_BACKSPACE
'CapsLock': 54, // GHOSTTY_KEY_CAPS_LOCK
'ContextMenu': 55, // GHOSTTY_KEY_CONTEXT_MENU
'ControlLeft': 56, // GHOSTTY_KEY_CONTROL_LEFT
'ControlRight': 57, // GHOSTTY_KEY_CONTROL_RIGHT
'Enter': 58, // GHOSTTY_KEY_ENTER
'MetaLeft': 59, // GHOSTTY_KEY_META_LEFT
'MetaRight': 60, // GHOSTTY_KEY_META_RIGHT
'ShiftLeft': 61, // GHOSTTY_KEY_SHIFT_LEFT
'ShiftRight': 62, // GHOSTTY_KEY_SHIFT_RIGHT
'Space': 63, // GHOSTTY_KEY_SPACE
'Tab': 64, // GHOSTTY_KEY_TAB
'Convert': 65, // GHOSTTY_KEY_CONVERT
'KanaMode': 66, // GHOSTTY_KEY_KANA_MODE
'NonConvert': 67, // GHOSTTY_KEY_NON_CONVERT
// Control Pad Section
'Delete': 68, // GHOSTTY_KEY_DELETE
'End': 69, // GHOSTTY_KEY_END
'Help': 70, // GHOSTTY_KEY_HELP
'Home': 71, // GHOSTTY_KEY_HOME
'Insert': 72, // GHOSTTY_KEY_INSERT
'PageDown': 73, // GHOSTTY_KEY_PAGE_DOWN
'PageUp': 74, // GHOSTTY_KEY_PAGE_UP
// Arrow Pad Section
'ArrowDown': 75, // GHOSTTY_KEY_ARROW_DOWN
'ArrowLeft': 76, // GHOSTTY_KEY_ARROW_LEFT
'ArrowRight': 77, // GHOSTTY_KEY_ARROW_RIGHT
'ArrowUp': 78, // GHOSTTY_KEY_ARROW_UP
// Numpad Section
'NumLock': 79, // GHOSTTY_KEY_NUM_LOCK
'Numpad0': 80, // GHOSTTY_KEY_NUMPAD_0
'Numpad1': 81, // GHOSTTY_KEY_NUMPAD_1
'Numpad2': 82, // GHOSTTY_KEY_NUMPAD_2
'Numpad3': 83, // GHOSTTY_KEY_NUMPAD_3
'Numpad4': 84, // GHOSTTY_KEY_NUMPAD_4
'Numpad5': 85, // GHOSTTY_KEY_NUMPAD_5
'Numpad6': 86, // GHOSTTY_KEY_NUMPAD_6
'Numpad7': 87, // GHOSTTY_KEY_NUMPAD_7
'Numpad8': 88, // GHOSTTY_KEY_NUMPAD_8
'Numpad9': 89, // GHOSTTY_KEY_NUMPAD_9
'NumpadAdd': 90, // GHOSTTY_KEY_NUMPAD_ADD
'NumpadBackspace': 91, // GHOSTTY_KEY_NUMPAD_BACKSPACE
'NumpadClear': 92, // GHOSTTY_KEY_NUMPAD_CLEAR
'NumpadClearEntry': 93, // GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY
'NumpadComma': 94, // GHOSTTY_KEY_NUMPAD_COMMA
'NumpadDecimal': 95, // GHOSTTY_KEY_NUMPAD_DECIMAL
'NumpadDivide': 96, // GHOSTTY_KEY_NUMPAD_DIVIDE
'NumpadEnter': 97, // GHOSTTY_KEY_NUMPAD_ENTER
'NumpadEqual': 98, // GHOSTTY_KEY_NUMPAD_EQUAL
'NumpadMemoryAdd': 99, // GHOSTTY_KEY_NUMPAD_MEMORY_ADD
'NumpadMemoryClear': 100,// GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR
'NumpadMemoryRecall': 101,// GHOSTTY_KEY_NUMPAD_MEMORY_RECALL
'NumpadMemoryStore': 102,// GHOSTTY_KEY_NUMPAD_MEMORY_STORE
'NumpadMemorySubtract': 103,// GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT
'NumpadMultiply': 104, // GHOSTTY_KEY_NUMPAD_MULTIPLY
'NumpadParenLeft': 105, // GHOSTTY_KEY_NUMPAD_PAREN_LEFT
'NumpadParenRight': 106, // GHOSTTY_KEY_NUMPAD_PAREN_RIGHT
'NumpadSubtract': 107, // GHOSTTY_KEY_NUMPAD_SUBTRACT
'NumpadSeparator': 108, // GHOSTTY_KEY_NUMPAD_SEPARATOR
'NumpadUp': 109, // GHOSTTY_KEY_NUMPAD_UP
'NumpadDown': 110, // GHOSTTY_KEY_NUMPAD_DOWN
'NumpadRight': 111, // GHOSTTY_KEY_NUMPAD_RIGHT
'NumpadLeft': 112, // GHOSTTY_KEY_NUMPAD_LEFT
'NumpadBegin': 113, // GHOSTTY_KEY_NUMPAD_BEGIN
'NumpadHome': 114, // GHOSTTY_KEY_NUMPAD_HOME
'NumpadEnd': 115, // GHOSTTY_KEY_NUMPAD_END
'NumpadInsert': 116, // GHOSTTY_KEY_NUMPAD_INSERT
'NumpadDelete': 117, // GHOSTTY_KEY_NUMPAD_DELETE
'NumpadPageUp': 118, // GHOSTTY_KEY_NUMPAD_PAGE_UP
'NumpadPageDown': 119, // GHOSTTY_KEY_NUMPAD_PAGE_DOWN
// Function Section
'Escape': 120, // GHOSTTY_KEY_ESCAPE
'F1': 121, // GHOSTTY_KEY_F1
'F2': 122, // GHOSTTY_KEY_F2
'F3': 123, // GHOSTTY_KEY_F3
'F4': 124, // GHOSTTY_KEY_F4
'F5': 125, // GHOSTTY_KEY_F5
'F6': 126, // GHOSTTY_KEY_F6
'F7': 127, // GHOSTTY_KEY_F7
'F8': 128, // GHOSTTY_KEY_F8
'F9': 129, // GHOSTTY_KEY_F9
'F10': 130, // GHOSTTY_KEY_F10
'F11': 131, // GHOSTTY_KEY_F11
'F12': 132, // GHOSTTY_KEY_F12
'F13': 133, // GHOSTTY_KEY_F13
'F14': 134, // GHOSTTY_KEY_F14
'F15': 135, // GHOSTTY_KEY_F15
'F16': 136, // GHOSTTY_KEY_F16
'F17': 137, // GHOSTTY_KEY_F17
'F18': 138, // GHOSTTY_KEY_F18
'F19': 139, // GHOSTTY_KEY_F19
'F20': 140, // GHOSTTY_KEY_F20
'F21': 141, // GHOSTTY_KEY_F21
'F22': 142, // GHOSTTY_KEY_F22
'F23': 143, // GHOSTTY_KEY_F23
'F24': 144, // GHOSTTY_KEY_F24
'F25': 145, // GHOSTTY_KEY_F25
'Fn': 146, // GHOSTTY_KEY_FN
'FnLock': 147, // GHOSTTY_KEY_FN_LOCK
'PrintScreen': 148, // GHOSTTY_KEY_PRINT_SCREEN
'ScrollLock': 149, // GHOSTTY_KEY_SCROLL_LOCK
'Pause': 150, // GHOSTTY_KEY_PAUSE
// Media Keys
'BrowserBack': 151, // GHOSTTY_KEY_BROWSER_BACK
'BrowserFavorites': 152, // GHOSTTY_KEY_BROWSER_FAVORITES
'BrowserForward': 153, // GHOSTTY_KEY_BROWSER_FORWARD
'BrowserHome': 154, // GHOSTTY_KEY_BROWSER_HOME
'BrowserRefresh': 155, // GHOSTTY_KEY_BROWSER_REFRESH
'BrowserSearch': 156, // GHOSTTY_KEY_BROWSER_SEARCH
'BrowserStop': 157, // GHOSTTY_KEY_BROWSER_STOP
'Eject': 158, // GHOSTTY_KEY_EJECT
'LaunchApp1': 159, // GHOSTTY_KEY_LAUNCH_APP_1
'LaunchApp2': 160, // GHOSTTY_KEY_LAUNCH_APP_2
'LaunchMail': 161, // GHOSTTY_KEY_LAUNCH_MAIL
'MediaPlayPause': 162, // GHOSTTY_KEY_MEDIA_PLAY_PAUSE
'MediaSelect': 163, // GHOSTTY_KEY_MEDIA_SELECT
'MediaStop': 164, // GHOSTTY_KEY_MEDIA_STOP
'MediaTrackNext': 165, // GHOSTTY_KEY_MEDIA_TRACK_NEXT
'MediaTrackPrevious': 166,// GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS
'Power': 167, // GHOSTTY_KEY_POWER
'Sleep': 168, // GHOSTTY_KEY_SLEEP
'AudioVolumeDown': 169, // GHOSTTY_KEY_AUDIO_VOLUME_DOWN
'AudioVolumeMute': 170, // GHOSTTY_KEY_AUDIO_VOLUME_MUTE
'AudioVolumeUp': 171, // GHOSTTY_KEY_AUDIO_VOLUME_UP
'WakeUp': 172, // GHOSTTY_KEY_WAKE_UP
// Legacy, Non-standard, and Special Keys
'Copy': 173, // GHOSTTY_KEY_COPY
'Cut': 174, // GHOSTTY_KEY_CUT
'Paste': 175, // GHOSTTY_KEY_PASTE
};
function encodeKeyEvent(event) {
if (!encoderPtr) return null;
try {
// Create key event
const eventPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
const result = wasmInstance.exports.ghostty_key_event_new(0, eventPtrPtr);
if (result !== 0) {
throw new Error(`ghostty_key_event_new failed with result ${result}`);
}
const eventPtr = new DataView(getBuffer()).getUint32(eventPtrPtr, true);
// Get action from radio buttons
const actionRadio = document.querySelector('input[name="action"]:checked');
const action = parseInt(actionRadio.value);
wasmInstance.exports.ghostty_key_event_set_action(eventPtr, action);
// Map key code from event.code (preferred, layout-independent)
let keyCode = keyCodeMap[event.code] || 0; // GHOSTTY_KEY_UNIDENTIFIED = 0
wasmInstance.exports.ghostty_key_event_set_key(eventPtr, keyCode);
// Map modifiers with left/right side information
let mods = 0;
if (event.shiftKey) {
mods |= 0x01; // GHOSTTY_MODS_SHIFT
if (event.code === 'ShiftRight') mods |= 0x40; // GHOSTTY_MODS_SHIFT_SIDE
}
if (event.ctrlKey) {
mods |= 0x02; // GHOSTTY_MODS_CTRL
if (event.code === 'ControlRight') mods |= 0x80; // GHOSTTY_MODS_CTRL_SIDE
}
if (event.altKey) {
mods |= 0x04; // GHOSTTY_MODS_ALT
if (event.code === 'AltRight') mods |= 0x100; // GHOSTTY_MODS_ALT_SIDE
}
if (event.metaKey) {
mods |= 0x08; // GHOSTTY_MODS_SUPER
if (event.code === 'MetaRight') mods |= 0x200; // GHOSTTY_MODS_SUPER_SIDE
}
wasmInstance.exports.ghostty_key_event_set_mods(eventPtr, mods);
// 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_u8_array(utf8Bytes.length);
new Uint8Array(getBuffer()).set(utf8Bytes, utf8Ptr);
wasmInstance.exports.ghostty_key_event_set_utf8(eventPtr, utf8Ptr, utf8Bytes.length);
}
// Set unshifted codepoint
const unshiftedCodepoint = getUnshiftedCodepoint(event);
if (unshiftedCodepoint !== 0) {
wasmInstance.exports.ghostty_key_event_set_unshifted_codepoint(eventPtr, unshiftedCodepoint);
}
// Encode the key event
const requiredPtr = wasmInstance.exports.ghostty_wasm_alloc_usize();
wasmInstance.exports.ghostty_key_encoder_encode(
encoderPtr, eventPtr, 0, 0, requiredPtr
);
const required = new DataView(getBuffer()).getUint32(requiredPtr, true);
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
);
if (encodeResult !== 0) {
return null; // No encoding for this key
}
const written = new DataView(getBuffer()).getUint32(writtenPtr, true);
const encoded = new Uint8Array(getBuffer()).slice(bufPtr, bufPtr + written);
return {
bytes: Array.from(encoded),
hex: formatHex(encoded),
string: formatString(encoded)
};
} catch (e) {
console.error('Encoding error:', e);
return null;
}
}
function getUnshiftedCodepoint(event) {
// Derive unshifted codepoint from the physical key code
const code = event.code;
// Letter keys (KeyA-KeyZ) -> lowercase letters
if (code.startsWith('Key')) {
const letter = code.substring(3).toLowerCase();
return letter.codePointAt(0);
}
// Digit keys (Digit0-Digit9) -> the digit itself
if (code.startsWith('Digit')) {
const digit = code.substring(5);
return digit.codePointAt(0);
}
// Space
if (code === 'Space') {
return ' '.codePointAt(0);
}
// Symbol keys -> unshifted character
const unshiftedSymbols = {
'Minus': '-', 'Equal': '=', 'BracketLeft': '[', 'BracketRight': ']',
'Backslash': '\\', 'Semicolon': ';', 'Quote': "'",
'Backquote': '`', 'Comma': ',', 'Period': '.', 'Slash': '/'
};
if (unshiftedSymbols[code]) {
return unshiftedSymbols[code].codePointAt(0);
}
// Fallback: use the produced character's codepoint
if (event.key.length > 0) {
return event.key.codePointAt(0) || 0;
}
return 0;
}
function getKittyFlags() {
let flags = 0;
if (document.getElementById('flag_disambiguate').checked) flags |= 0x01;
if (document.getElementById('flag_report_events').checked) flags |= 0x02;
if (document.getElementById('flag_report_alternates').checked) flags |= 0x04;
if (document.getElementById('flag_report_all_as_escapes').checked) flags |= 0x08;
if (document.getElementById('flag_report_text').checked) flags |= 0x10;
return flags;
}
function updateEncoderFlags() {
if (!encoderPtr) return;
const flags = getKittyFlags();
const flagsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
new DataView(getBuffer()).setUint8(flagsPtr, flags);
wasmInstance.exports.ghostty_key_encoder_setopt(
encoderPtr,
5, // GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS
flagsPtr
);
// Re-encode last key with new flags
reencodeLastKey();
}
function displayEncoding(event) {
const outputDiv = document.getElementById('output');
const encoded = encodeKeyEvent(event);
const actionRadio = document.querySelector('input[name="action"]:checked');
const actionName = actionRadio.parentElement.textContent.trim();
let output = `Action: ${actionName}\n`;
output += `Key: ${event.key} (code: ${event.code})\n`;
output += `Modifiers: `;
const mods = [];
if (event.shiftKey) mods.push('Shift');
if (event.ctrlKey) mods.push('Ctrl');
if (event.altKey) mods.push('Alt');
if (event.metaKey) mods.push('Meta');
output += mods.length ? mods.join('+') : 'none';
output += '\n';
// Show Kitty flags state
const flags = [];
if (document.getElementById('flag_disambiguate').checked) flags.push('Disambiguate');
if (document.getElementById('flag_report_events').checked) flags.push('Report Events');
if (document.getElementById('flag_report_alternates').checked) flags.push('Report Alternates');
if (document.getElementById('flag_report_all_as_escapes').checked) flags.push('Report All As Escapes');
if (document.getElementById('flag_report_text').checked) flags.push('Report Text');
output += 'Kitty Flags:\n';
if (flags.length) {
flags.forEach(flag => output += ` - ${flag}\n`);
} else {
output += ' - none\n';
}
output += '\n';
if (encoded) {
output += `Encoded ${encoded.bytes.length} bytes\n`;
output += `Hex: ${encoded.hex}\n`;
output += `String: ${encoded.string}`;
} else {
output += 'No encoding for this key event';
}
outputDiv.textContent = output;
}
function handleKeyEvent(event) {
// Allow modifier keys to be pressed without clearing input
// Only prevent default for keys we want to capture
if (event.key !== 'Tab' && event.key !== 'F5') {
event.preventDefault();
}
lastKeyEvent = event;
displayEncoding(event);
}
function reencodeLastKey() {
if (lastKeyEvent) {
displayEncoding(lastKeyEvent);
}
}
async function init() {
const statusDiv = document.getElementById('status');
const keyInput = document.getElementById('keyInput');
const outputDiv = document.getElementById('output');
try {
statusDiv.textContent = 'Loading WebAssembly module...';
const loaded = await loadWasm();
if (!loaded) {
throw new Error('Failed to load WebAssembly module');
}
// Create key encoder
const encoderPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
const result = wasmInstance.exports.ghostty_key_encoder_new(0, encoderPtrPtr);
if (result !== 0) {
throw new Error(`ghostty_key_encoder_new failed with result ${result}`);
}
encoderPtr = new DataView(getBuffer()).getUint32(encoderPtrPtr, true);
// Set kitty flags based on checkboxes
updateEncoderFlags();
statusDiv.textContent = '';
keyInput.disabled = false;
keyInput.focus();
// Listen for key events (only keydown since action is selected manually)
keyInput.addEventListener('keydown', handleKeyEvent);
// Listen for flag changes
const flagCheckboxes = document.querySelectorAll('.checkbox-group input[type="checkbox"]');
flagCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', updateEncoderFlags);
});
// Listen for action changes
const actionRadios = document.querySelectorAll('input[name="action"]');
actionRadios.forEach(radio => {
radio.addEventListener('change', reencodeLastKey);
});
} catch (e) {
statusDiv.textContent = `Error: ${e.message}`;
statusDiv.style.color = '#c00';
outputDiv.className = 'output error';
outputDiv.textContent = `Error: ${e.message}\n\nStack trace:\n${e.stack}`;
}
}
// Initialize on page load
window.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
@@ -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>
@@ -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 <ghostty/vt/result.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/osc.h>
#include <ghostty/vt/sgr.h>
#include <ghostty/vt/key.h>
#include <ghostty/vt/paste.h>
#include <ghostty/vt/wasm.h>
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/** @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 */
@@ -0,0 +1,96 @@
/**
* @file color.h
*
* Color types and utilities.
*/
#ifndef GHOSTTY_VT_COLOR_H
#define GHOSTTY_VT_COLOR_H
#include <stdint.h>
#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 */
@@ -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 <assert.h>
* #include <stdio.h>
* #include <ghostty/vt.h>
*
* 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 <ghostty/vt/key/event.h>
#include <ghostty/vt/key/encoder.h>
/** @} */
#endif /* GHOSTTY_VT_KEY_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 <stddef.h>
#include <stdint.h>
#include <ghostty/vt/result.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/key/event.h>
/**
* 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 */
@@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/result.h>
#include <ghostty/vt/allocator.h>
/**
* 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 */
@@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/result.h>
#include <ghostty/vt/allocator.h>
/**
* 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 */
@@ -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 <stdio.h>
* #include <string.h>
* #include <ghostty/vt.h>
*
* 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 <stdbool.h>
#include <stddef.h>
#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 */
@@ -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 */
@@ -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 <assert.h>
* #include <stdio.h>
* #include <ghostty/vt.h>
*
* 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 <ghostty/vt/allocator.h>
#include <ghostty/vt/color.h>
#include <ghostty/vt/result.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#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 */
@@ -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 <stddef.h>
#include <stdint.h>
/** @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 */
+135
View File
@@ -0,0 +1,135 @@
//! The C allocator interface (ZIG/src/lib/allocator.zig, `include/ghostty/vt/allocator.h`).
//!
//! Wrapper objects handed out through the C API (parsers, events, encoders)
//! and the SGR parameter copy are allocated with the caller's allocator when
//! one is given and with the Rust global allocator otherwise (Zig: libc
//! `c_allocator` / `wasm_allocator`). Interior Rust collections (e.g. the OSC
//! parser's buffer) always use the global allocator.
#![allow(unsafe_code)]
use std::{alloc::Layout, ffi::c_void, ptr::NonNull};
/// C: `GhosttyAllocatorVtable`
#[repr(C)]
pub struct AllocatorVtable {
pub alloc: unsafe extern "C" fn(
ctx: *mut c_void,
len: usize,
alignment: u8,
ret_addr: usize,
) -> *mut c_void,
pub resize: unsafe extern "C" fn(
ctx: *mut c_void,
memory: *mut c_void,
memory_len: usize,
alignment: u8,
new_len: usize,
ret_addr: usize,
) -> bool,
pub remap: unsafe extern "C" fn(
ctx: *mut c_void,
memory: *mut c_void,
memory_len: usize,
alignment: u8,
new_len: usize,
ret_addr: usize,
) -> *mut c_void,
pub free: unsafe extern "C" fn(
ctx: *mut c_void,
memory: *mut c_void,
memory_len: usize,
alignment: u8,
ret_addr: usize,
),
}
/// C: `GhosttyAllocator`
#[derive(Clone, Copy)]
#[repr(C)]
pub struct Allocator {
pub ctx: *mut c_void,
pub vtable: *const AllocatorVtable,
}
/// A possibly-absent C allocator (`None` = default allocator). Stored inside
/// every object so `free` releases memory with the allocator that created it.
#[derive(Clone, Copy)]
pub struct Alloc(Option<Allocator>);
impl Alloc {
/// From the `const GhosttyAllocator *` parameter of a `*_new` function.
///
/// # Safety
/// `ptr` must be null or point to a valid [`Allocator`] with a valid vtable.
pub unsafe fn from_c(ptr: *const Allocator) -> Alloc {
// SAFETY: caller guarantees validity when non-null; we copy the struct
// so the original need not outlive this call (the vtable/ctx must).
Alloc(unsafe { ptr.as_ref() }.copied())
}
/// Allocate `layout`; null on failure. Zero-sized layouts return a dangling
/// aligned pointer without calling the allocator.
pub fn alloc_raw(self, layout: Layout) -> *mut u8 {
if layout.size() == 0 {
return layout.align() as *mut u8;
}
match self.0 {
// SAFETY: non-zero size layout.
None => unsafe { std::alloc::alloc(layout) },
// SAFETY: `from_c` contract: vtable is valid.
Some(a) => unsafe {
((*a.vtable).alloc)(
a.ctx,
layout.size(),
layout.align().trailing_zeros() as u8,
0,
)
.cast()
},
}
}
/// Free memory returned by [`Alloc::alloc_raw`] with the same layout.
///
/// # Safety
/// `ptr` must have been allocated by this allocator with `layout`.
pub unsafe fn free_raw(self, ptr: *mut u8, layout: Layout) {
if layout.size() == 0 {
return;
}
match self.0 {
// SAFETY: per the function contract.
None => unsafe { std::alloc::dealloc(ptr, layout) },
// SAFETY: per the function contract and `from_c`.
Some(a) => unsafe {
((*a.vtable).free)(
a.ctx,
ptr.cast(),
layout.size(),
layout.align().trailing_zeros() as u8,
0,
);
},
}
}
/// Move `value` into freshly allocated memory (Zig `alloc.create`).
pub fn create<T>(self, value: T) -> Option<NonNull<T>> {
let p = NonNull::new(self.alloc_raw(Layout::new::<T>()))?.cast::<T>();
// SAFETY: freshly allocated, correctly sized and aligned.
unsafe { p.as_ptr().write(value) };
Some(p)
}
/// Drop and free an object made by [`Alloc::create`] (Zig `alloc.destroy`).
///
/// # Safety
/// `p` must come from `create::<T>` on this allocator and not be used again.
pub unsafe fn destroy<T>(self, p: NonNull<T>) {
// SAFETY: per the function contract.
unsafe {
std::ptr::drop_in_place(p.as_ptr());
self.free_raw(p.as_ptr().cast(), Layout::new::<T>());
}
}
}
+56
View File
@@ -0,0 +1,56 @@
//! `ghostty_color_rgb_get` (ZIG/src/terminal/c/color.zig).
#![allow(unsafe_code)]
/// C: `GhosttyColorRgb`
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(C)]
pub struct GhosttyColorRgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl From<ghostty_vt::color::Rgb> for GhosttyColorRgb {
fn from(c: ghostty_vt::color::Rgb) -> Self {
Self {
r: c.r,
g: c.g,
b: c.b,
}
}
}
/// Extract the RGB components of a color.
///
/// # Safety
/// `r`, `g` and `b` must be valid for writing a `u8` (Zig: non-optional
/// pointers).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_color_rgb_get(
color: GhosttyColorRgb,
r: *mut u8,
g: *mut u8,
b: *mut u8,
) {
// SAFETY: the caller guarantees all three pointers are valid for writes.
unsafe {
*r = color.r;
*g = color.g;
*b = color.b;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rgb_get() {
let (mut r, mut g, mut b) = (0u8, 0u8, 0u8);
// SAFETY: pointers to live locals.
unsafe {
ghostty_color_rgb_get(GhosttyColorRgb { r: 1, g: 2, b: 3 }, &mut r, &mut g, &mut b);
}
assert_eq!((r, g, b), (1, 2, 3));
}
}
+303
View File
@@ -0,0 +1,303 @@
//! Key encoder C API (ZIG/src/terminal/c/key_encode.zig, `include/ghostty/vt/key/encoder.h`).
#![allow(unsafe_code)]
use std::{
ffi::{c_int, c_void},
io::{self, Write},
ptr::NonNull,
};
use ghostty_input::{
OptionAsAlt,
key_encode::{Options, encode},
};
use ghostty_vt::kitty::key::Flags as KittyFlags;
use crate::{
alloc::{Alloc, Allocator},
key_event::KeyEventWrapper,
result::GhosttyResult,
};
/// C: `GhosttyKeyEncoder` (opaque). Mirrors Zig's `KeyEncoderWrapper`.
pub struct KeyEncoderWrapper {
alloc: Alloc,
opts: Options,
}
/// C: `GhosttyKeyEncoderOption`
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum KeyEncoderOption {
CursorKeyApplication = 0,
KeypadKeyApplication = 1,
IgnoreKeypadWithNumlock = 2,
AltEscPrefix = 3,
ModifyOtherKeysState2 = 4,
KittyFlags = 5,
MacosOptionAsAlt = 6,
}
impl KeyEncoderOption {
fn from_c(v: c_int) -> Option<Self> {
Some(match v {
0 => Self::CursorKeyApplication,
1 => Self::KeypadKeyApplication,
2 => Self::IgnoreKeypadWithNumlock,
3 => Self::AltEscPrefix,
4 => Self::ModifyOtherKeysState2,
5 => Self::KittyFlags,
6 => Self::MacosOptionAsAlt,
_ => return None,
})
}
}
/// C: `GhosttyOptionAsAlt` (values of [`OptionAsAlt`]).
fn option_as_alt_from_c(v: c_int) -> Option<OptionAsAlt> {
Some(match v {
0 => OptionAsAlt::False,
1 => OptionAsAlt::True,
2 => OptionAsAlt::Left,
3 => OptionAsAlt::Right,
_ => return None,
})
}
/// # Safety
/// `allocator` must be null or valid; `encoder` must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_encoder_new(
allocator: *const Allocator,
encoder: *mut *mut KeyEncoderWrapper,
) -> GhosttyResult {
// SAFETY: forwarded caller contract.
let alloc = unsafe { Alloc::from_c(allocator) };
let Some(p) = alloc.create(KeyEncoderWrapper {
alloc,
opts: Options::default(),
}) else {
return GhosttyResult::OutOfMemory;
};
// SAFETY: caller guarantees `encoder` is valid for writes.
unsafe { *encoder = p.as_ptr() };
GhosttyResult::Success
}
/// # Safety
/// `encoder` must be null or a live encoder from `ghostty_key_encoder_new`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_encoder_free(encoder: *mut KeyEncoderWrapper) {
let Some(p) = NonNull::new(encoder) else {
return;
};
// SAFETY: `p` came from `Alloc::create` in `ghostty_key_encoder_new`.
unsafe {
let alloc = p.as_ref().alloc;
alloc.destroy(p);
}
}
/// # Safety
/// `encoder` must be null or live; `value` must be null or point to the type
/// documented for `option` (`bool`, `uint8_t` or `GhosttyOptionAsAlt`).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_encoder_setopt(
encoder: *mut KeyEncoderWrapper,
option: c_int,
value: *const c_void,
) {
let Some(option) = KeyEncoderOption::from_c(option) else {
log::warn!("setopt invalid option value={option}");
return;
};
if value.is_null() {
return;
}
// SAFETY: caller guarantees a live encoder (or null).
let Some(enc) = (unsafe { encoder.as_mut() }) else {
return;
};
let opts = &mut enc.opts;
// SAFETY (all reads): `value` is non-null and points to the option's type.
unsafe {
let flag = || value.cast::<u8>().read_unaligned() != 0;
match option {
KeyEncoderOption::CursorKeyApplication => opts.cursor_key_application = flag(),
KeyEncoderOption::KeypadKeyApplication => opts.keypad_key_application = flag(),
KeyEncoderOption::IgnoreKeypadWithNumlock => {
opts.ignore_keypad_with_numlock = flag();
}
KeyEncoderOption::AltEscPrefix => opts.alt_esc_prefix = flag(),
KeyEncoderOption::ModifyOtherKeysState2 => opts.modify_other_keys_state_2 = flag(),
KeyEncoderOption::KittyFlags => {
// Zig: truncate to the 5 flag bits.
opts.kitty_flags = KittyFlags::from_int(value.cast::<u8>().read_unaligned() & 0x1f);
}
KeyEncoderOption::MacosOptionAsAlt => {
let raw = value.cast::<c_int>().read_unaligned();
match option_as_alt_from_c(raw) {
Some(v) => opts.macos_option_as_alt = v,
None => log::warn!("setopt invalid OptionAsAlt value={raw}"),
}
}
}
}
}
/// Counts bytes without storing them (Zig `Writer.Discarding`).
struct Counter(usize);
impl Write for Counter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0 += buf.len();
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
/// # Safety
/// `encoder`/`event` must be live; `out_buf` must be null or point to
/// `out_buf_size` writable bytes; `out_len` must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_encoder_encode(
encoder: *mut KeyEncoderWrapper,
event: *mut KeyEventWrapper,
out_buf: *mut u8,
out_buf_size: usize,
out_len: *mut usize,
) -> GhosttyResult {
// SAFETY: caller guarantees live handles.
let (Some(enc), Some(ev)) = (unsafe { encoder.as_ref() }, unsafe { event.as_ref() }) else {
return GhosttyResult::InvalidValue;
};
// SAFETY: the event's UTF-8 bytes must be alive per `set_utf8`'s contract.
let ev = unsafe { ev.as_event() };
let buf: &mut [u8] = if out_buf.is_null() {
&mut []
} else {
// SAFETY: caller guarantees `out_buf_size` writable bytes.
unsafe { std::slice::from_raw_parts_mut(out_buf, out_buf_size) }
};
let capacity = buf.len();
let mut cursor: &mut [u8] = buf;
let result = encode(&mut cursor, &ev, &enc.opts);
let written = capacity - cursor.len();
let (code, n) = if result.is_ok() {
(GhosttyResult::Success, written)
} else {
// Not enough space: count how much we would have needed.
let mut counter = Counter(0);
let _ = encode(&mut counter, &ev, &enc.opts);
(GhosttyResult::OutOfMemory, counter.0)
};
if !out_len.is_null() {
// SAFETY: non-null and valid per the contract.
unsafe { *out_len = n };
}
code
}
#[cfg(test)]
mod tests {
use ghostty_input::Key;
use super::*;
use crate::key_event::{
ghostty_key_event_free,
ghostty_key_event_new,
ghostty_key_event_set_action,
ghostty_key_event_set_key,
ghostty_key_event_set_mods,
};
fn new() -> *mut KeyEncoderWrapper {
let mut e = std::ptr::null_mut();
// SAFETY: valid out pointer, default allocator.
assert_eq!(
unsafe { ghostty_key_encoder_new(std::ptr::null(), &mut e) },
GhosttyResult::Success
);
e
}
#[test]
fn setopt_bool_kitty_and_option_as_alt() {
let e = new();
// SAFETY: live encoder; values point to the right types.
unsafe {
let t = true;
ghostty_key_encoder_setopt(e, 0, (&raw const t).cast());
assert!((*e).opts.cursor_key_application);
let f = false;
ghostty_key_encoder_setopt(e, 0, (&raw const f).cast());
assert!(!(*e).opts.cursor_key_application);
ghostty_key_encoder_setopt(e, 1, (&raw const t).cast());
assert!((*e).opts.keypad_key_application);
let flags = 0b0000_0011u8;
ghostty_key_encoder_setopt(e, 5, (&raw const flags).cast());
assert!((*e).opts.kitty_flags.disambiguate && (*e).opts.kitty_flags.report_events);
assert!(!(*e).opts.kitty_flags.report_alternates);
let left: c_int = 2;
ghostty_key_encoder_setopt(e, 6, (&raw const left).cast());
assert_eq!((*e).opts.macos_option_as_alt, OptionAsAlt::Left);
let bad: c_int = 77;
ghostty_key_encoder_setopt(e, 6, (&raw const bad).cast());
assert_eq!((*e).opts.macos_option_as_alt, OptionAsAlt::Left);
// Invalid option / null value are ignored.
ghostty_key_encoder_setopt(e, 99, (&raw const t).cast());
ghostty_key_encoder_setopt(e, 0, std::ptr::null());
ghostty_key_encoder_free(e);
}
}
#[test]
fn encode_kitty_ctrl_release_with_ctrl_mod_set() {
let enc = new();
let mut ev = std::ptr::null_mut();
// SAFETY: live handles, valid buffers.
unsafe {
let all = 0x1fu8;
ghostty_key_encoder_setopt(enc, 5, (&raw const all).cast());
assert_eq!(
ghostty_key_event_new(std::ptr::null(), &mut ev),
GhosttyResult::Success
);
ghostty_key_event_set_action(ev, 0);
ghostty_key_event_set_key(ev, Key::ControlLeft as c_int);
ghostty_key_event_set_mods(ev, 0b10);
let mut required = 0usize;
assert_eq!(
ghostty_key_encoder_encode(enc, ev, std::ptr::null_mut(), 0, &mut required),
GhosttyResult::OutOfMemory
);
let mut buf = [0u8; 128];
let mut written = 0usize;
assert_eq!(
ghostty_key_encoder_encode(enc, ev, buf.as_mut_ptr(), buf.len(), &mut written),
GhosttyResult::Success
);
assert_eq!(required, written);
assert_eq!(&buf[..written], b"\x1b[57442;5:3u");
// Exactly-too-small buffer reports the needed size.
let mut small = [0u8; 3];
assert_eq!(
ghostty_key_encoder_encode(enc, ev, small.as_mut_ptr(), small.len(), &mut written),
GhosttyResult::OutOfMemory
);
assert_eq!(written, required);
ghostty_key_event_free(ev);
ghostty_key_encoder_free(enc);
}
}
}
+346
View File
@@ -0,0 +1,346 @@
//! Key event C API (ZIG/src/terminal/c/key_event.zig, `include/ghostty/vt/key/event.h`).
#![allow(unsafe_code)]
use std::{ffi::c_int, ptr::NonNull};
use ghostty_input::{Action, Key, KeyEvent, Mods};
use crate::{
alloc::{Alloc, Allocator},
result::GhosttyResult,
};
/// C: `GhosttyKeyEvent` (opaque). Mirrors Zig's `KeyEventWrapper`.
///
/// The UTF-8 text is not owned by the wrapper: the caller is responsible for
/// keeping the bytes passed to `ghostty_key_event_set_utf8` alive.
pub struct KeyEventWrapper {
alloc: Alloc,
action: Action,
key: Key,
mods: Mods,
consumed_mods: Mods,
composing: bool,
utf8: *const u8,
utf8_len: usize,
unshifted_codepoint: u32,
}
impl KeyEventWrapper {
/// Borrow as the Rust [`KeyEvent`]. Zig's `utf8` is a byte slice, ours is
/// `&str`: invalid UTF-8 is truncated to its longest valid prefix.
///
/// # Safety
/// The caller-provided UTF-8 bytes must still be alive.
pub(crate) unsafe fn as_event(&self) -> KeyEvent<'_> {
let bytes: &[u8] = if self.utf8.is_null() {
&[]
} else {
// SAFETY: per the contract of `ghostty_key_event_set_utf8`.
unsafe { std::slice::from_raw_parts(self.utf8, self.utf8_len) }
};
let text = match std::str::from_utf8(bytes) {
Ok(s) => s,
Err(e) => std::str::from_utf8(&bytes[..e.valid_up_to()]).unwrap_or(""),
};
KeyEvent {
action: self.action,
key: self.key,
mods: self.mods,
consumed_mods: self.consumed_mods,
composing: self.composing,
utf8: text,
unshifted_codepoint: self.unshifted_codepoint,
}
}
}
/// C: `GhosttyKeyAction`
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum KeyAction {
Release = 0,
Press = 1,
Repeat = 2,
}
fn action_from_c(v: c_int) -> Option<Action> {
match v {
0 => Some(Action::Release),
1 => Some(Action::Press),
2 => Some(Action::Repeat),
_ => None,
}
}
fn key_from_c(v: c_int) -> Option<Key> {
usize::try_from(v)
.ok()
.and_then(|i| Key::ALL.get(i).copied())
}
/// # Safety
/// `allocator` must be null or valid; `event` must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_new(
allocator: *const Allocator,
event: *mut *mut KeyEventWrapper,
) -> GhosttyResult {
// SAFETY: forwarded caller contract.
let alloc = unsafe { Alloc::from_c(allocator) };
let d = KeyEvent::default();
let Some(p) = alloc.create(KeyEventWrapper {
alloc,
action: d.action,
key: d.key,
mods: d.mods,
consumed_mods: d.consumed_mods,
composing: d.composing,
utf8: std::ptr::null(),
utf8_len: 0,
unshifted_codepoint: d.unshifted_codepoint,
}) else {
return GhosttyResult::OutOfMemory;
};
// SAFETY: caller guarantees `event` is valid for writes.
unsafe { *event = p.as_ptr() };
GhosttyResult::Success
}
/// # Safety
/// `event` must be null or a live event from `ghostty_key_event_new`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_free(event: *mut KeyEventWrapper) {
let Some(p) = NonNull::new(event) else {
return;
};
// SAFETY: `p` came from `Alloc::create` in `ghostty_key_event_new`.
unsafe {
let alloc = p.as_ref().alloc;
alloc.destroy(p);
}
}
macro_rules! with_event {
($event:expr, $e:ident => $body:expr, $default:expr) => {
// SAFETY: caller guarantees a live event (or null).
match unsafe { $event.as_mut() } {
Some($e) => $body,
None => $default,
}
};
}
/// # Safety
/// `event` must be null or live.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_set_action(event: *mut KeyEventWrapper, action: c_int) {
let Some(action) = action_from_c(action) else {
log::warn!("set_action invalid action value={action}");
return;
};
with_event!(event, e => e.action = action, ());
}
/// # Safety
/// `event` must be null or live.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_get_action(event: *mut KeyEventWrapper) -> c_int {
with_event!(event, e => e.action as c_int, KeyAction::Press as c_int)
}
/// # Safety
/// `event` must be null or live.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_set_key(event: *mut KeyEventWrapper, key: c_int) {
let Some(key) = key_from_c(key) else {
log::warn!("set_key invalid key value={key}");
return;
};
with_event!(event, e => e.key = key, ());
}
/// # Safety
/// `event` must be null or live.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_get_key(event: *mut KeyEventWrapper) -> c_int {
with_event!(event, e => e.key as c_int, 0)
}
/// # Safety
/// `event` must be null or live.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_set_mods(event: *mut KeyEventWrapper, mods: u16) {
with_event!(event, e => e.mods = Mods::from_int(mods), ());
}
/// # Safety
/// `event` must be null or live.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_get_mods(event: *mut KeyEventWrapper) -> u16 {
with_event!(event, e => e.mods.int(), 0)
}
/// # Safety
/// `event` must be null or live.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_set_consumed_mods(
event: *mut KeyEventWrapper,
consumed_mods: u16,
) {
with_event!(event, e => e.consumed_mods = Mods::from_int(consumed_mods), ());
}
/// # Safety
/// `event` must be null or live.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_get_consumed_mods(event: *mut KeyEventWrapper) -> u16 {
with_event!(event, e => e.consumed_mods.int(), 0)
}
/// # Safety
/// `event` must be null or live.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_set_composing(
event: *mut KeyEventWrapper,
composing: bool,
) {
with_event!(event, e => e.composing = composing, ());
}
/// # Safety
/// `event` must be null or live.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_get_composing(event: *mut KeyEventWrapper) -> bool {
with_event!(event, e => e.composing, false)
}
/// # Safety
/// `event` must be null or live; `utf8` must be null or point to `len`
/// bytes that stay valid while the event is used.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_set_utf8(
event: *mut KeyEventWrapper,
utf8: *const u8,
len: usize,
) {
with_event!(event, e => {
if utf8.is_null() {
e.utf8 = std::ptr::null();
e.utf8_len = 0;
} else {
e.utf8 = utf8;
e.utf8_len = len;
}
}, ());
}
/// # Safety
/// `event` must be null or live; `len`, if non-null, must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_get_utf8(
event: *mut KeyEventWrapper,
len: *mut usize,
) -> *const u8 {
with_event!(event, e => {
if !len.is_null() {
// SAFETY: non-null and valid per the contract.
unsafe { *len = e.utf8_len };
}
if e.utf8_len == 0 { std::ptr::null() } else { e.utf8 }
}, std::ptr::null())
}
/// # Safety
/// `event` must be null or live.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_set_unshifted_codepoint(
event: *mut KeyEventWrapper,
codepoint: u32,
) {
// Zig: `@truncate` to u21.
with_event!(event, e => e.unshifted_codepoint = codepoint & 0x1f_ffff, ());
}
/// # Safety
/// `event` must be null or live.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_key_event_get_unshifted_codepoint(
event: *mut KeyEventWrapper,
) -> u32 {
with_event!(event, e => e.unshifted_codepoint, 0)
}
#[cfg(test)]
mod tests {
use super::*;
fn new() -> *mut KeyEventWrapper {
let mut e = std::ptr::null_mut();
// SAFETY: valid out pointer, default allocator.
assert_eq!(
unsafe { ghostty_key_event_new(std::ptr::null(), &mut e) },
GhosttyResult::Success
);
e
}
#[test]
fn alloc() {
let e = new();
// SAFETY: live event.
unsafe { ghostty_key_event_free(e) };
}
#[test]
fn set_and_get_round_trip() {
let e = new();
let text = b"test";
// SAFETY: live event; `text` outlives its use.
unsafe {
ghostty_key_event_set_action(e, KeyAction::Repeat as c_int);
ghostty_key_event_set_key(e, Key::KeyZ as c_int);
ghostty_key_event_set_mods(e, 0b1100); // alt | super
ghostty_key_event_set_consumed_mods(e, 0b0100);
ghostty_key_event_set_composing(e, true);
ghostty_key_event_set_utf8(e, text.as_ptr(), text.len());
ghostty_key_event_set_unshifted_codepoint(e, 'z' as u32);
assert_eq!(ghostty_key_event_get_action(e), KeyAction::Repeat as c_int);
assert_eq!(ghostty_key_event_get_key(e), Key::KeyZ as c_int);
assert_eq!(ghostty_key_event_get_mods(e), 0b1100);
assert_eq!(ghostty_key_event_get_consumed_mods(e), 0b0100);
assert!(ghostty_key_event_get_composing(e));
let mut len = 0usize;
let p = ghostty_key_event_get_utf8(e, &mut len);
assert_eq!(std::slice::from_raw_parts(p, len), b"test");
assert_eq!(ghostty_key_event_get_unshifted_codepoint(e), 'z' as u32);
ghostty_key_event_set_utf8(e, std::ptr::null(), 0);
assert!(ghostty_key_event_get_utf8(e, std::ptr::null_mut()).is_null());
ghostty_key_event_free(e);
}
}
#[test]
fn invalid_enum_values_are_ignored() {
let e = new();
// SAFETY: live event.
unsafe {
ghostty_key_event_set_action(e, 99);
assert_eq!(ghostty_key_event_get_action(e), KeyAction::Press as c_int);
ghostty_key_event_set_key(e, -1);
ghostty_key_event_set_key(e, 100_000);
assert_eq!(ghostty_key_event_get_key(e), 0);
ghostty_key_event_free(e);
}
}
#[test]
fn key_numbering_matches_header() {
assert_eq!(Key::ALL.len(), 176);
assert_eq!(key_from_c(1), Some(Key::Backquote));
assert_eq!(key_from_c(175), Some(Key::Paste));
assert_eq!(key_from_c(176), None);
}
}
+26
View File
@@ -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;
+241
View File
@@ -0,0 +1,241 @@
//! OSC parser C API (ZIG/src/terminal/c/osc.zig, `include/ghostty/vt/osc.h`).
#![allow(unsafe_code)]
use std::{
ffi::{c_int, c_void},
ptr::NonNull,
};
use ghostty_vt::osc::{Command, Parser};
use crate::{
alloc::{Alloc, Allocator},
result::GhosttyResult,
};
/// C: `GhosttyOscParser` (opaque).
pub struct OscParser {
alloc: Alloc,
parser: Parser,
/// Backing store for the last command returned by `ghostty_osc_end`.
command: OscCommand,
}
/// C: `GhosttyOscCommand` (opaque).
///
/// Zig hands out a pointer to the parser's own command; the Rust `Command`
/// is returned by value, so it is parked here (with a NUL-terminated copy of
/// string payloads, as Zig's sentinel slices are) and stays valid until the
/// next `ghostty_osc_end`/`ghostty_osc_reset`/`ghostty_osc_free`.
pub struct OscCommand {
command: Command,
/// NUL-terminated window title (only for `ChangeWindowTitle`).
title_z: Vec<u8>,
}
/// C: `GhosttyOscCommandData`
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum OscCommandData {
Invalid = 0,
ChangeWindowTitleStr = 1,
}
impl OscCommandData {
fn from_c(v: c_int) -> Option<Self> {
match v {
0 => Some(Self::Invalid),
1 => Some(Self::ChangeWindowTitleStr),
_ => None,
}
}
}
/// # Safety
/// `allocator` must be null or valid; `result` must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_osc_new(
allocator: *const Allocator,
result: *mut *mut OscParser,
) -> GhosttyResult {
// SAFETY: forwarded caller contract.
let alloc = unsafe { Alloc::from_c(allocator) };
// C-built parsers have an allocator (Zig: `.init(alloc)`), so commands
// that need unbounded buffers (OSC 52, ...) are accepted.
let Some(p) = alloc.create(OscParser {
alloc,
parser: Parser::init(true),
command: OscCommand {
command: Command::Invalid,
title_z: Vec::new(),
},
}) else {
return GhosttyResult::OutOfMemory;
};
// SAFETY: caller guarantees `result` is valid for writes.
unsafe { *result = p.as_ptr() };
GhosttyResult::Success
}
/// # Safety
/// `parser` must be null or a live parser from `ghostty_osc_new`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_osc_free(parser: *mut OscParser) {
let Some(p) = NonNull::new(parser) else {
return;
};
// SAFETY: `p` came from `Alloc::create` in `ghostty_osc_new`.
unsafe {
let alloc = p.as_ref().alloc;
alloc.destroy(p);
}
}
/// # Safety
/// `parser` must be null or a live parser.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_osc_reset(parser: *mut OscParser) {
// SAFETY: caller guarantees a live parser (or null).
if let Some(p) = unsafe { parser.as_mut() } {
p.parser.reset();
}
}
/// # Safety
/// `parser` must be null or a live parser.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_osc_next(parser: *mut OscParser, byte: u8) {
// SAFETY: caller guarantees a live parser (or null).
if let Some(p) = unsafe { parser.as_mut() } {
p.parser.next(byte);
}
}
/// # Safety
/// `parser` must be null or a live parser. The returned command is owned by
/// the parser.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_osc_end(
parser: *mut OscParser,
terminator: u8,
) -> *mut OscCommand {
// SAFETY: caller guarantees a live parser (or null).
let Some(p) = (unsafe { parser.as_mut() }) else {
return std::ptr::null_mut();
};
let cmd = p.parser.end(Some(terminator)).unwrap_or(Command::Invalid);
p.command.title_z.clear();
if let Command::ChangeWindowTitle(t) = &cmd {
p.command.title_z.extend_from_slice(t);
p.command.title_z.push(0);
}
p.command.command = cmd;
&raw mut p.command
}
/// Returns the `GhosttyOscCommandType` (an `int` enum).
///
/// # Safety
/// `command` must be null or a command returned by `ghostty_osc_end`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_osc_command_type(command: *mut OscCommand) -> c_int {
// SAFETY: caller guarantees validity (or null).
match unsafe { command.as_ref() } {
Some(c) => c.command.key() as c_int,
None => 0, // invalid
}
}
/// # Safety
/// `command` must be null or valid; `out` must point to storage of the type
/// documented for `data` (for the title: a `const char *`).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_osc_command_data(
command: *mut OscCommand,
data: c_int,
out: *mut c_void,
) -> bool {
let Some(data) = OscCommandData::from_c(data) else {
log::warn!("commandData invalid data value={data}");
return false;
};
// SAFETY: caller guarantees validity (or null).
let Some(command) = (unsafe { command.as_ref() }) else {
return false;
};
match data {
OscCommandData::Invalid => false,
OscCommandData::ChangeWindowTitleStr => match &command.command {
Command::ChangeWindowTitle(_) if !out.is_null() => {
// SAFETY: caller guarantees `out` is a `const char **`.
unsafe {
out.cast::<*const u8>()
.write_unaligned(command.title_z.as_ptr());
}
true
}
_ => false,
},
}
}
#[cfg(test)]
mod tests {
use std::ffi::CStr;
use super::*;
fn new() -> *mut OscParser {
let mut p = std::ptr::null_mut();
// SAFETY: valid out pointer, default allocator.
assert_eq!(
unsafe { ghostty_osc_new(std::ptr::null(), &mut p) },
GhosttyResult::Success
);
p
}
#[test]
fn alloc() {
let p = new();
// SAFETY: `p` is live.
unsafe { ghostty_osc_free(p) };
}
#[test]
fn command_type_null() {
// SAFETY: null is allowed.
assert_eq!(unsafe { ghostty_osc_command_type(std::ptr::null_mut()) }, 0);
}
#[test]
fn change_window_title() {
let p = new();
// SAFETY: `p` is live; `title` is a valid out slot.
unsafe {
ghostty_osc_next(p, b'0');
ghostty_osc_next(p, b';');
ghostty_osc_next(p, b'a');
let cmd = ghostty_osc_end(p, 0);
assert_eq!(ghostty_osc_command_type(cmd), 1);
let mut title: *const u8 = std::ptr::null();
assert!(ghostty_osc_command_data(cmd, 1, (&raw mut title).cast()));
assert_eq!(CStr::from_ptr(title.cast()).to_str().unwrap(), "a");
assert!(!ghostty_osc_command_data(cmd, 0, std::ptr::null_mut()));
assert!(!ghostty_osc_command_data(cmd, 99, std::ptr::null_mut()));
ghostty_osc_free(p);
}
}
#[test]
fn invalid_command_is_type_zero() {
let p = new();
// SAFETY: `p` is live.
unsafe {
ghostty_osc_next(p, b'x');
let cmd = ghostty_osc_end(p, 0);
assert_eq!(ghostty_osc_command_type(cmd), 0);
ghostty_osc_free(p);
}
}
}
+53
View File
@@ -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) });
}
}
+10
View File
@@ -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,
}
+544
View File
@@ -0,0 +1,544 @@
//! SGR parser C API (ZIG/src/terminal/c/sgr.zig, `include/ghostty/vt/sgr.h`).
#![allow(unsafe_code)]
use std::{alloc::Layout, ffi::c_int, ptr::NonNull};
use ghostty_vt::{
parser::{MAX_PARAMS, SepList},
sgr::{Attribute, Parser},
};
use crate::{
alloc::{Alloc, Allocator},
color::GhosttyColorRgb,
result::GhosttyResult,
};
/// C: `GhosttySgrParser` (opaque). Mirrors Zig's `ParserWrapper`: the SGR
/// parser state plus the allocator that owns the parameter copy.
pub struct SgrParser {
alloc: Alloc,
/// Owned copy of the parameters (allocated with `alloc`), or null.
params: *mut u16,
params_len: usize,
params_sep: SepList,
idx: usize,
}
impl SgrParser {
fn params_layout(len: usize) -> Layout {
Layout::array::<u16>(len).unwrap_or(Layout::new::<u16>())
}
fn free_params(&mut self) {
if !self.params.is_null() {
// SAFETY: allocated by `set_params` with this allocator/layout.
unsafe {
self.alloc
.free_raw(self.params.cast(), Self::params_layout(self.params_len));
}
self.params = std::ptr::null_mut();
self.params_len = 0;
}
}
fn params(&self) -> &[u16] {
if self.params.is_null() {
&[]
} else {
// SAFETY: `params` points to `params_len` initialized u16s that
// live until `free_params`.
unsafe { std::slice::from_raw_parts(self.params, self.params_len) }
}
}
}
/// C: `GhosttySgrAttributeTag`
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum SgrAttributeTag {
Unset = 0,
Unknown = 1,
Bold = 2,
ResetBold = 3,
Italic = 4,
ResetItalic = 5,
Faint = 6,
Underline = 7,
UnderlineColor = 8,
UnderlineColor256 = 9,
ResetUnderlineColor = 10,
Overline = 11,
ResetOverline = 12,
Blink = 13,
ResetBlink = 14,
Inverse = 15,
ResetInverse = 16,
Invisible = 17,
ResetInvisible = 18,
Strikethrough = 19,
ResetStrikethrough = 20,
DirectColorFg = 21,
DirectColorBg = 22,
Bg8 = 23,
Fg8 = 24,
ResetFg = 25,
ResetBg = 26,
BrightBg8 = 27,
BrightFg8 = 28,
Bg256 = 29,
Fg256 = 30,
}
/// C: `GhosttySgrUnknown`
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct SgrUnknown {
pub full_ptr: *const u16,
pub full_len: usize,
pub partial_ptr: *const u16,
pub partial_len: usize,
}
/// C: `GhosttySgrAttributeValue`
#[derive(Clone, Copy)]
#[repr(C)]
pub union SgrAttributeValue {
pub unknown: SgrUnknown,
/// A `GhosttySgrUnderline` (`int`).
pub underline: c_int,
pub underline_color: GhosttyColorRgb,
pub underline_color_256: u8,
pub direct_color_fg: GhosttyColorRgb,
pub direct_color_bg: GhosttyColorRgb,
pub bg_8: u8,
pub fg_8: u8,
pub bright_bg_8: u8,
pub bright_fg_8: u8,
pub bg_256: u8,
pub fg_256: u8,
pub _padding: [u64; 8],
}
/// C: `GhosttySgrAttribute`
#[derive(Clone, Copy)]
#[repr(C)]
pub struct SgrAttribute {
/// A [`SgrAttributeTag`].
pub tag: c_int,
pub value: SgrAttributeValue,
}
impl SgrAttribute {
fn from_attr(attr: &Attribute<'_>) -> SgrAttribute {
use SgrAttributeTag as T;
let mut value = SgrAttributeValue { _padding: [0; 8] };
let tag = match *attr {
Attribute::Unset => T::Unset,
Attribute::Unknown(u) => {
value.unknown = SgrUnknown {
full_ptr: u.full.as_ptr(),
full_len: u.full.len(),
partial_ptr: u.partial.as_ptr(),
partial_len: u.partial.len(),
};
T::Unknown
}
Attribute::Bold => T::Bold,
Attribute::ResetBold => T::ResetBold,
Attribute::Italic => T::Italic,
Attribute::ResetItalic => T::ResetItalic,
Attribute::Faint => T::Faint,
Attribute::Underline(u) => {
value.underline = c_int::from(u as u8);
T::Underline
}
Attribute::UnderlineColor(c) => {
value.underline_color = c.into();
T::UnderlineColor
}
Attribute::UnderlineColor256(v) => {
value.underline_color_256 = v;
T::UnderlineColor256
}
Attribute::ResetUnderlineColor => T::ResetUnderlineColor,
Attribute::Overline => T::Overline,
Attribute::ResetOverline => T::ResetOverline,
Attribute::Blink => T::Blink,
Attribute::ResetBlink => T::ResetBlink,
Attribute::Inverse => T::Inverse,
Attribute::ResetInverse => T::ResetInverse,
Attribute::Invisible => T::Invisible,
Attribute::ResetInvisible => T::ResetInvisible,
Attribute::Strikethrough => T::Strikethrough,
Attribute::ResetStrikethrough => T::ResetStrikethrough,
Attribute::DirectColorFg(c) => {
value.direct_color_fg = c.into();
T::DirectColorFg
}
Attribute::DirectColorBg(c) => {
value.direct_color_bg = c.into();
T::DirectColorBg
}
Attribute::Bg8(n) => {
value.bg_8 = n.0;
T::Bg8
}
Attribute::Fg8(n) => {
value.fg_8 = n.0;
T::Fg8
}
Attribute::ResetFg => T::ResetFg,
Attribute::ResetBg => T::ResetBg,
Attribute::Bg8Bright(n) => {
value.bright_bg_8 = n.0;
T::BrightBg8
}
Attribute::Fg8Bright(n) => {
value.bright_fg_8 = n.0;
T::BrightFg8
}
Attribute::Bg256(v) => {
value.bg_256 = v;
T::Bg256
}
Attribute::Fg256(v) => {
value.fg_256 = v;
T::Fg256
}
};
SgrAttribute {
tag: tag as c_int,
value,
}
}
}
/// # Safety
/// `allocator` must be null or valid; `result` must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_new(
allocator: *const Allocator,
result: *mut *mut SgrParser,
) -> GhosttyResult {
// SAFETY: forwarded caller contract.
let alloc = unsafe { Alloc::from_c(allocator) };
let Some(p) = alloc.create(SgrParser {
alloc,
params: std::ptr::null_mut(),
params_len: 0,
params_sep: SepList::init_empty(),
idx: 0,
}) else {
return GhosttyResult::OutOfMemory;
};
// SAFETY: caller guarantees `result` is valid for writes.
unsafe { *result = p.as_ptr() };
GhosttyResult::Success
}
/// # Safety
/// `parser` must be null or a live parser from `ghostty_sgr_new`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_free(parser: *mut SgrParser) {
let Some(mut p) = NonNull::new(parser) else {
return;
};
// SAFETY: `p` came from `Alloc::create` in `ghostty_sgr_new`.
unsafe {
let alloc = p.as_ref().alloc;
p.as_mut().free_params();
alloc.destroy(p);
}
}
/// # Safety
/// `parser` must be null or a live parser.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_reset(parser: *mut SgrParser) {
// SAFETY: caller guarantees a live parser (or null).
if let Some(p) = unsafe { parser.as_mut() } {
p.idx = 0;
}
}
/// # Safety
/// `parser` must be null or live; `params` must point to `len` `u16`s;
/// `separators`, if non-null, must point to `len` bytes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_set_params(
parser: *mut SgrParser,
params: *const u16,
separators: *const u8,
len: usize,
) -> GhosttyResult {
// SAFETY: caller guarantees a live parser (or null).
let Some(p) = (unsafe { parser.as_mut() }) else {
return GhosttyResult::InvalidValue;
};
if params.is_null() && len > 0 {
return GhosttyResult::InvalidValue;
}
// Copy the new parameters (Zig: `alloc.dupe`).
let layout = SgrParser::params_layout(len);
let new = p.alloc.alloc_raw(layout).cast::<u16>();
if new.is_null() {
return GhosttyResult::OutOfMemory;
}
if len > 0 {
// SAFETY: `params` has `len` readable elements; `new` has room for `len`.
unsafe { std::ptr::copy_nonoverlapping(params, new, len) };
}
p.free_params();
p.params = new;
p.params_len = len;
// If we have separators, set that state too.
p.params_sep = SepList::init_empty();
if !separators.is_null() {
if len > MAX_PARAMS {
log::warn!(
"ghostty_sgr_set_params: separators length {len} exceeds max supported length {MAX_PARAMS}"
);
return GhosttyResult::InvalidValue;
}
// SAFETY: caller guarantees `len` readable separator bytes.
let seps = unsafe { std::slice::from_raw_parts(separators, len) };
for (i, sep) in seps.iter().enumerate() {
if *sep == b':' {
p.params_sep.set(i);
}
}
}
// Reset our parsing state.
p.idx = 0;
GhosttyResult::Success
}
/// # Safety
/// `parser` must be null or live; `result` must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_next(
parser: *mut SgrParser,
result: *mut SgrAttribute,
) -> bool {
// SAFETY: caller guarantees a live parser (or null).
let Some(p) = (unsafe { parser.as_mut() }) else {
return false;
};
let mut sp = Parser {
params: p.params(),
params_sep: p.params_sep,
idx: p.idx,
};
let attr = sp.next();
let idx = sp.idx;
// `attr` borrows `p.params`; convert before touching `p` mutably.
let Some(attr) = attr else {
p.idx = idx;
return false;
};
let c = SgrAttribute::from_attr(&attr);
p.idx = idx;
// SAFETY: caller guarantees `result` is valid for writes.
unsafe { result.write(c) };
true
}
/// # Safety
/// `ptr`, if non-null, must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_unknown_full(
unknown: SgrUnknown,
ptr: *mut *const u16,
) -> usize {
if !ptr.is_null() {
// SAFETY: non-null and valid per the contract.
unsafe { *ptr = unknown.full_ptr };
}
unknown.full_len
}
/// # Safety
/// `ptr`, if non-null, must be valid for writes.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_unknown_partial(
unknown: SgrUnknown,
ptr: *mut *const u16,
) -> usize {
if !ptr.is_null() {
// SAFETY: non-null and valid per the contract.
unsafe { *ptr = unknown.partial_ptr };
}
unknown.partial_len
}
/// Returns the `GhosttySgrAttributeTag` of `attr`.
#[unsafe(no_mangle)]
pub extern "C" fn ghostty_sgr_attribute_tag(attr: SgrAttribute) -> c_int {
attr.tag
}
/// # Safety
/// `attr` must point to a valid attribute; the result points into it.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn ghostty_sgr_attribute_value(
attr: *mut SgrAttribute,
) -> *mut SgrAttributeValue {
// SAFETY: caller guarantees `attr` is valid; no reference is created.
unsafe { &raw mut (*attr).value }
}
#[cfg(test)]
mod tests {
use super::*;
fn new() -> *mut SgrParser {
let mut p = std::ptr::null_mut();
// SAFETY: valid out pointer, default allocator.
assert_eq!(
unsafe { ghostty_sgr_new(std::ptr::null(), &mut p) },
GhosttyResult::Success
);
p
}
fn zeroed() -> SgrAttribute {
SgrAttribute {
tag: 0,
value: SgrAttributeValue { _padding: [0; 8] },
}
}
#[test]
fn alloc() {
let p = new();
// SAFETY: live parser.
unsafe { ghostty_sgr_free(p) };
}
#[test]
fn simple_params_no_seps() {
let p = new();
let mut attr = zeroed();
// SAFETY: live parser; valid buffers.
unsafe {
let params = [1u16];
// Set it twice on purpose to make sure we don't leak.
for _ in 0..2 {
assert_eq!(
ghostty_sgr_set_params(p, params.as_ptr(), std::ptr::null(), 1),
GhosttyResult::Success
);
}
assert!(ghostty_sgr_next(p, &mut attr));
assert_eq!(attr.tag, SgrAttributeTag::Bold as c_int);
assert!(!ghostty_sgr_next(p, &mut attr));
ghostty_sgr_free(p);
}
}
#[test]
fn colon_separated_underline_and_colors() {
let p = new();
let params: [u16; 17] = [
4, 3, 38, 2, 51, 51, 51, 48, 2, 170, 170, 170, 58, 2, 255, 97, 136,
];
let mut seps = [b';'; 17];
seps[0] = b':';
let mut attr = zeroed();
// SAFETY: live parser; valid buffers.
unsafe {
assert_eq!(
ghostty_sgr_set_params(p, params.as_ptr(), seps.as_ptr(), 17),
GhosttyResult::Success
);
assert!(ghostty_sgr_next(p, &mut attr));
assert_eq!(attr.tag, SgrAttributeTag::Underline as c_int);
assert_eq!(attr.value.underline, 3);
assert!(ghostty_sgr_next(p, &mut attr));
assert_eq!(attr.tag, SgrAttributeTag::DirectColorFg as c_int);
assert_eq!(
attr.value.direct_color_fg,
GhosttyColorRgb {
r: 51,
g: 51,
b: 51
}
);
assert!(ghostty_sgr_next(p, &mut attr));
assert_eq!(attr.tag, SgrAttributeTag::DirectColorBg as c_int);
assert!(ghostty_sgr_next(p, &mut attr));
assert_eq!(attr.tag, SgrAttributeTag::UnderlineColor as c_int);
assert_eq!(
attr.value.underline_color,
GhosttyColorRgb {
r: 255,
g: 97,
b: 136
}
);
assert!(!ghostty_sgr_next(p, &mut attr));
ghostty_sgr_reset(p);
assert!(ghostty_sgr_next(p, &mut attr));
ghostty_sgr_free(p);
}
}
#[test]
fn empty_params_is_unset_and_too_many_seps_is_invalid() {
let p = new();
let mut attr = zeroed();
// SAFETY: live parser; valid buffers.
unsafe {
assert_eq!(
ghostty_sgr_set_params(p, [].as_ptr(), std::ptr::null(), 0),
GhosttyResult::Success
);
assert!(ghostty_sgr_next(p, &mut attr));
assert_eq!(attr.tag, SgrAttributeTag::Unset as c_int);
assert!(!ghostty_sgr_next(p, &mut attr));
let params = [0u16; 25];
let seps = [b';'; 25];
assert_eq!(
ghostty_sgr_set_params(p, params.as_ptr(), seps.as_ptr(), 25),
GhosttyResult::InvalidValue
);
assert_eq!(
ghostty_sgr_set_params(std::ptr::null_mut(), params.as_ptr(), std::ptr::null(), 1),
GhosttyResult::InvalidValue
);
ghostty_sgr_free(p);
}
}
#[test]
fn unknown_attribute_exposes_params() {
let p = new();
let params = [9999u16];
let mut attr = zeroed();
// SAFETY: live parser; valid buffers.
unsafe {
ghostty_sgr_set_params(p, params.as_ptr(), std::ptr::null(), 1);
assert!(ghostty_sgr_next(p, &mut attr));
assert_eq!(attr.tag, SgrAttributeTag::Unknown as c_int);
let mut ptr: *const u16 = std::ptr::null();
let n = ghostty_sgr_unknown_full(attr.value.unknown, &mut ptr);
assert_eq!(std::slice::from_raw_parts(ptr, n), &[9999]);
let n = ghostty_sgr_unknown_partial(attr.value.unknown, std::ptr::null_mut());
assert!(n <= 1);
assert_eq!(
ghostty_sgr_attribute_tag(attr),
SgrAttributeTag::Unknown as c_int
);
assert_eq!(
ghostty_sgr_attribute_value(&mut attr).cast::<u8>(),
(&raw mut attr).cast::<u8>().add(8)
);
ghostty_sgr_free(p);
}
}
}
+149
View File
@@ -0,0 +1,149 @@
//! Wasm allocation helpers (ZIG/src/lib/allocator/convenience.zig and
//! `wasm_alloc_attribute` in ZIG/src/terminal/c/sgr.zig,
//! `include/ghostty/vt/wasm.h`).
//!
//! JavaScript hosts cannot easily create the pointers the C API wants, so
//! upstream exports these helpers on wasm. They always use the default
//! allocator. They are exported (`no_mangle`) only when compiling for wasm.
#![allow(unsafe_code)]
use std::{alloc::Layout, ffi::c_void};
use crate::sgr::SgrAttribute;
fn alloc_layout(layout: Layout) -> *mut u8 {
if layout.size() == 0 {
return layout.align() as *mut u8;
}
// SAFETY: non-zero size.
unsafe { std::alloc::alloc(layout) }
}
/// # Safety
/// `ptr` must be null or come from `alloc_layout` with the same `layout`.
unsafe fn free_layout(ptr: *mut u8, layout: Layout) {
if !ptr.is_null() && layout.size() != 0 {
// SAFETY: per the contract.
unsafe { std::alloc::dealloc(ptr, layout) };
}
}
/// Allocate one `void *` (e.g. an out-parameter for `*_new`).
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_opaque() -> *mut *mut c_void {
alloc_layout(Layout::new::<*mut c_void>()).cast()
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_opaque`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_opaque(ptr: *mut *mut c_void) {
// SAFETY: per the contract.
unsafe { free_layout(ptr.cast(), Layout::new::<*mut c_void>()) };
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_u8_array(len: usize) -> *mut u8 {
Layout::array::<u8>(len).map_or(std::ptr::null_mut(), alloc_layout)
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_u8_array(len)`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_u8_array(ptr: *mut u8, len: usize) {
if let Ok(l) = Layout::array::<u8>(len) {
// SAFETY: per the contract.
unsafe { free_layout(ptr, l) };
}
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_u16_array(len: usize) -> *mut u16 {
Layout::array::<u16>(len).map_or(std::ptr::null_mut(), |l| alloc_layout(l).cast())
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_u16_array(len)`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_u16_array(ptr: *mut u16, len: usize) {
if let Ok(l) = Layout::array::<u16>(len) {
// SAFETY: per the contract.
unsafe { free_layout(ptr.cast(), l) };
}
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_u8() -> *mut u8 {
alloc_layout(Layout::new::<u8>())
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_u8`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_u8(ptr: *mut u8) {
// SAFETY: per the contract.
unsafe { free_layout(ptr, Layout::new::<u8>()) };
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_usize() -> *mut usize {
alloc_layout(Layout::new::<usize>()).cast()
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_usize`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_usize(ptr: *mut usize) {
// SAFETY: per the contract.
unsafe { free_layout(ptr.cast(), Layout::new::<usize>()) };
}
/// Allocate a `GhosttySgrAttribute` (zero-initialized).
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_sgr_attribute() -> *mut SgrAttribute {
let p = alloc_layout(Layout::new::<SgrAttribute>()).cast::<SgrAttribute>();
if !p.is_null() {
// SAFETY: freshly allocated, correctly sized; all-zero is a valid value.
unsafe { p.write_bytes(0, 1) };
}
p
}
/// # Safety
/// `attr` must be null or from `ghostty_wasm_alloc_sgr_attribute`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_sgr_attribute(attr: *mut SgrAttribute) {
// SAFETY: per the contract.
unsafe { free_layout(attr.cast(), Layout::new::<SgrAttribute>()) };
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alloc_and_free_round_trip() {
// SAFETY: pointers come from the matching allocators.
unsafe {
let p = ghostty_wasm_alloc_u8_array(16);
assert!(!p.is_null());
p.write_bytes(7, 16);
ghostty_wasm_free_u8_array(p, 16);
let z = ghostty_wasm_alloc_u8_array(0);
ghostty_wasm_free_u8_array(z, 0);
let u = ghostty_wasm_alloc_u16_array(5);
ghostty_wasm_free_u16_array(u, 5);
let o = ghostty_wasm_alloc_opaque();
ghostty_wasm_free_opaque(o);
let b = ghostty_wasm_alloc_u8();
ghostty_wasm_free_u8(b);
let s = ghostty_wasm_alloc_usize();
ghostty_wasm_free_usize(s);
let a = ghostty_wasm_alloc_sgr_attribute();
assert_eq!((*a).tag, 0);
ghostty_wasm_free_sgr_attribute(a);
}
}
}
+658
View File
@@ -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<String, Vec<String>> {
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<String> {
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<T: std::fmt::Debug + Copy>(items: &[T], f: impl Fn(T) -> i64) -> BTreeMap<String, i64> {
items
.iter()
.map(|i| (norm(&format!("{i:?}")), f(*i)))
.collect()
}
struct Probe {
c: String,
expected: BTreeMap<String, i64>,
}
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<String, Vec<String>>,
typedef: &str,
prefix: &str,
rust: &BTreeMap<String, i64>,
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::<GhosttyColorRgb>(),
align_of::<GhosttyColorRgb>(),
&[
("r", offset_of!(GhosttyColorRgb, r)),
("g", offset_of!(GhosttyColorRgb, g)),
("b", offset_of!(GhosttyColorRgb, b)),
],
);
p.layout(
"GhosttyAllocatorVtable",
size_of::<AllocatorVtable>(),
align_of::<AllocatorVtable>(),
&[
("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::<Allocator>(),
align_of::<Allocator>(),
&[
("ctx", offset_of!(Allocator, ctx)),
("vtable", offset_of!(Allocator, vtable)),
],
);
p.layout(
"GhosttySgrUnknown",
size_of::<SgrUnknown>(),
align_of::<SgrUnknown>(),
&[
("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::<SgrAttributeValue>(),
align_of::<SgrAttributeValue>(),
&[
("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::<SgrAttribute>(),
align_of::<SgrAttribute>(),
&[
("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::<usize>()),
("GhosttyOscCommand", size_of::<usize>()),
("GhosttySgrParser", size_of::<usize>()),
("GhosttyKeyEvent", size_of::<usize>()),
("GhosttyKeyEncoder", size_of::<usize>()),
] {
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<String, i64> = 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<String, i64> {
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<String> = 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 <stdio.h>\n#include <stddef.h>\n#include <stdalign.h>\n#include <ghostty/vt.h>\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, i64> = 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<String> = 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"
);
}
}
@@ -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"));
}
+170
View File
@@ -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/<profile>`, 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) // <profile>
.expect("profile dir")
.to_path_buf()
}
fn cargo() -> Command {
Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()))
}
static INSTALLED: OnceLock<PathBuf> = 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<PathBuf> {
fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
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<String> {
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
}
+146
View File
@@ -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 <wasm> <names-file>
// node run.mjs sgr <sgr.html> <wasm> <sgr.h> [fix-tags]
// node run.mjs key <key-encode.html> <wasm>
//
// 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(/<script>([\s\S]*)<\/script>/);
if (!m) throw new Error('no <script> in html');
return m[1];
}
function el(extra = {}) {
const listeners = {};
return {
textContent: '', className: '', value: '', checked: false, disabled: true,
style: {}, listeners,
addEventListener(type, fn) { (listeners[type] ??= []).push(fn); },
focus() {},
...extra,
};
}
/** Load an example page into a fresh context; returns helpers. */
async function load(htmlPath, wasmPath, transform = (s) => s) {
const html = fs.readFileSync(htmlPath, 'utf8');
const elements = {};
// Initial textarea contents (the sgr example's default sequence).
const ta = html.match(/<textarea id="(\w+)"[^>]*>([^<]*)<\/textarea>/);
if (ta) elements[ta[1]] = el({ value: ta[2] });
const winListeners = {};
const document = {
getElementById(id) { return (elements[id] ??= el()); },
querySelector() { return elements.__action ??= el({ value: '1', parentElement: { textContent: 'Press' } }); },
querySelectorAll(sel) {
if (sel.includes('checkbox')) return Object.keys(elements).filter((k) => k.startsWith('flag_')).map((k) => elements[k]);
return [];
},
};
const window = {
location: { protocol: 'http:' },
addEventListener(type, fn) { (winListeners[type] ??= []).push(fn); },
};
const wasmBytes = fs.readFileSync(wasmPath);
const fetch = async () => ({ arrayBuffer: async () => wasmBytes.buffer.slice(wasmBytes.byteOffset, wasmBytes.byteOffset + wasmBytes.byteLength) });
const ctx = vm.createContext({
document, window, fetch, console, TextEncoder, TextDecoder, DataView, Uint8Array, Uint16Array,
WebAssembly, Array, Object, parseInt, isNaN, Error, String,
});
vm.runInContext(transform(script(html)), ctx, { filename: htmlPath });
return { ctx, elements, winListeners, document };
}
async function runInit(page) {
for (const fn of page.winListeners.DOMContentLoaded ?? []) await fn();
}
function sgrTagsFromHeader(headerPath) {
const h = fs.readFileSync(headerPath, 'utf8');
const out = [];
for (const m of h.matchAll(/GHOSTTY_SGR_ATTR_([A-Z0-9_]+)\s*=\s*(\d+)/g)) out.push(`${m[1]}: ${m[2]}`);
return `const SGR_ATTR_TAGS = {\n${out.join(',\n')}\n};`;
}
if (mode === 'exports') {
const [wasmPath, namesFile] = args;
const mod = new WebAssembly.Module(fs.readFileSync(wasmPath));
const exported = new Set(WebAssembly.Module.exports(mod).map((e) => e.name));
const imports = WebAssembly.Module.imports(mod).map((i) => `${i.module}.${i.name}`);
console.log(`imports: ${imports.join(',')}`);
console.log(`has_memory: ${exported.has('memory')}`);
for (const n of fs.readFileSync(namesFile, 'utf8').split('\n').filter(Boolean)) {
if (!exported.has(n)) console.log(`MISSING ${n}`);
}
console.log('done');
} else if (mode === 'sgr') {
const [html, wasm, header, fix] = args;
const transform = fix === 'fix-tags'
? (s) => s.replace(/const SGR_ATTR_TAGS = \{[\s\S]*?\};/, sgrTagsFromHeader(header))
: (s) => s;
const page = await load(html, wasm, transform);
await runInit(page);
const out = page.elements.output;
const seq = page.elements.sequence;
console.log('=== default');
console.log(out.textContent);
for (const s of ['1;3;31;42', '38:5:123;48;5;200', '0', '4:0;4:1;4:2;4:3;4:4;4:5', '9999', '91;102;53']) {
seq.value = s;
for (const fn of seq.listeners.input ?? []) fn();
console.log(`=== ${s}`);
console.log(out.textContent);
}
} else if (mode === 'key') {
const [html, wasm] = args;
const page = await load(html, wasm);
await runInit(page);
const out = page.elements.output;
const input = page.elements.keyInput;
const press = (name, ev) => {
const e = { key: 'a', code: 'KeyA', shiftKey: false, ctrlKey: false, altKey: false, metaKey: false, preventDefault() {}, ...ev };
for (const fn of input.listeners.keydown ?? []) fn(e);
const m = out.textContent.match(/^Hex: (.*)$/m);
console.log(`${name}\t${m ? m[1].trim() : 'NONE'}`);
};
const setAction = (v, label) => {
const r = page.document.querySelector();
r.value = String(v); r.parentElement.textContent = label;
};
const setFlags = (on) => {
for (const id of Object.keys(page.elements).filter((k) => k.startsWith('flag_'))) page.elements[id].checked = on;
for (const id of Object.keys(page.elements).filter((k) => k.startsWith('flag_'))) {
for (const fn of page.elements[id].listeners.change ?? []) fn();
}
};
// The page creates the checkbox elements lazily via getElementById, which
// happens in init(); make sure they start checked (as in the HTML).
setFlags(true);
console.log('flags: all');
press('ctrl+a', { ctrlKey: true });
press('a', {});
press('shift+Enter', { key: 'Enter', code: 'Enter', shiftKey: true });
press('ArrowUp', { key: 'ArrowUp', code: 'ArrowUp' });
press('alt+x', { key: 'x', code: 'KeyX', altKey: true });
setAction(0, 'Release');
press('release a', {});
setAction(1, 'Press');
setFlags(false);
console.log('flags: none');
press('ctrl+a', { ctrlKey: true });
press('a', {});
press('shift+Enter', { key: 'Enter', code: 'Enter', shiftKey: true });
press('ArrowUp', { key: 'ArrowUp', code: 'ArrowUp' });
press('alt+x', { key: 'x', code: 'KeyX', altKey: true });
press('Escape', { key: 'Escape', code: 'Escape' });
press('F5', { key: 'F5', code: 'F5' });
press('unidentified', { key: 'Dead', code: 'Foo' });
} else {
throw new Error(`unknown mode ${mode}`);
}
@@ -0,0 +1,377 @@
//! Builds the crate for `wasm32-unknown-unknown` and runs Ghostty's wasm
//! examples (examples/wasm/*.html, unmodified) under node, checking that the
//! exported symbols match the headers and the examples produce the right
//! output.
#![cfg(unix)]
#![allow(clippy::unwrap_used)]
mod common;
use std::{path::PathBuf, process::Command, sync::OnceLock};
fn tool_available(cmd: &str, arg: &str) -> bool {
Command::new(cmd)
.arg(arg)
.output()
.is_ok_and(|o| o.status.success())
}
fn wasm_target_installed() -> bool {
Command::new("rustc")
.args([
"--print",
"target-libdir",
"--target",
"wasm32-unknown-unknown",
])
.output()
.ok()
.map(|o| PathBuf::from(String::from_utf8_lossy(&o.stdout).trim()))
.is_some_and(|p| {
p.join("libstd.rlib").exists()
|| std::fs::read_dir(p).is_ok_and(|mut d| {
d.any(|e| {
e.is_ok_and(|e| e.file_name().to_string_lossy().starts_with("libstd-"))
})
})
})
}
/// Path of the built wasm module, or `None` (with a note) if the toolchain
/// pieces are missing.
fn wasm_module() -> Option<PathBuf> {
static WASM: OnceLock<Option<PathBuf>> = OnceLock::new();
WASM.get_or_init(|| {
if !tool_available("node", "--version") {
eprintln!("node not installed; skipping wasm tests");
return None;
}
if !wasm_target_installed() {
eprintln!("wasm32-unknown-unknown target not installed; skipping wasm tests");
return None;
}
let profile = common::profile_dir();
let profile = profile
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("debug")
.to_string();
let mut cmd = Command::new(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()));
cmd.args([
"build",
"-p",
"ghostty-vt-capi",
"--target",
"wasm32-unknown-unknown",
]);
if profile != "debug" {
cmd.args(["--profile", &profile]);
}
assert!(cmd.status().unwrap().success(), "wasm build failed");
let target = common::profile_dir()
.parent()
.unwrap()
.join("wasm32-unknown-unknown");
let wasm = target.join(&profile).join("ghostty_vt_capi.wasm");
assert!(wasm.exists(), "{} missing", wasm.display());
// Install under the upstream name too (`ghostty-vt.wasm`).
let named = wasm.with_file_name("ghostty-vt.wasm");
std::fs::copy(&wasm, &named).unwrap();
Some(named)
})
.clone()
}
fn node(args: &[&str]) -> String {
let script = common::manifest_dir().join("tests/wasm/run.mjs");
let out = Command::new("node")
.arg(&script)
.args(args)
.output()
.expect("run node");
assert!(
out.status.success(),
"node failed:\n{}\n{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap()
}
fn example(name: &str) -> String {
common::manifest_dir()
.join("examples/wasm")
.join(name)
.display()
.to_string()
}
#[test]
fn wasm_exports_match_headers() {
let Some(wasm) = wasm_module() else { return };
// Every function in every header, including the `__wasm__`-only ones.
let names = common::declared_functions().join("\n");
assert!(
names.contains("ghostty_wasm_alloc_sgr_attribute")
&& names.contains("ghostty_wasm_alloc_u8_array")
);
let names_file = common::scratch_dir("wasm").join("names.txt");
std::fs::write(&names_file, names).unwrap();
let out = node(&[
"exports",
&wasm.display().to_string(),
&names_file.display().to_string(),
]);
assert!(!out.contains("MISSING"), "{out}");
assert!(out.contains("has_memory: true"), "{out}");
// No host imports are needed (upstream imports `env.log`; ours logs nowhere).
assert!(out.contains("imports: \n"), "{out}");
}
fn sections(out: &str) -> Vec<(String, String)> {
let mut v: Vec<(String, String)> = Vec::new();
for line in out.lines() {
if let Some(name) = line.strip_prefix("=== ") {
v.push((name.to_string(), String::new()));
} else if let Some((_, body)) = v.last_mut() {
body.push_str(line);
body.push('\n');
}
}
v
}
#[test]
fn wasm_sgr_example() {
let Some(wasm) = wasm_module() else { return };
let html = example("sgr.html");
let wasm = wasm.display().to_string();
let header = common::include_dir()
.join("ghostty/vt/sgr.h")
.display()
.to_string();
// As shipped upstream, the page's tag table is stale relative to sgr.h
// (upstream commit a384af5e2 changed the header only), so only the
// attributes whose tags still agree (underline = 7) are labelled right.
let stock = node(&["sgr", &html, &wasm, &header]);
let (_, text) = &sections(&stock)[0];
assert!(
text.contains("Attribute 1: Underline style = curly"),
"{text}"
);
assert!(text.contains("Total attributes parsed: 4"), "{text}");
// With the tag table regenerated from sgr.h the page output is exact.
let out = node(&["sgr", &html, &wasm, &header, "fix-tags"]);
let s = sections(&out);
let get = |n: &str| {
s.iter()
.find(|(k, _)| k == n)
.unwrap_or_else(|| panic!("{n}"))
.1
.clone()
};
let head = "Parsing SGR sequence:\n";
assert_eq!(
get("default"),
format!(
"{head}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\
\nTotal attributes parsed: 4\n"
)
);
assert_eq!(
get("1;3;31;42"),
format!(
"{head}ESC[1;3;31;42m\n\n\
Attribute 1: Bold\n\
Attribute 2: Italic\n\
Attribute 3: Foreground 8-color = 1\n\
Attribute 4: Background 8-color = 2\n\
\nTotal attributes parsed: 4\n"
)
);
assert_eq!(
get("38:5:123;48;5;200"),
format!(
"{head}ESC[38:5:123;48;5;200m\n\n\
Attribute 1: Foreground 256-color = 123\n\
Attribute 2: Background 256-color = 200\n\
\nTotal attributes parsed: 2\n"
)
);
assert_eq!(
get("0"),
format!(
"{head}ESC[0m\n\nAttribute 1: Reset all attributes\n\nTotal attributes parsed: 1\n"
)
);
assert_eq!(
get("9999"),
format!(
"{head}ESC[9999m\n\nAttribute 1: Unknown attribute\n\nTotal attributes parsed: 1\n"
)
);
let underlines = get("4:0;4:1;4:2;4:3;4:4;4:5");
for style in ["none", "single", "double", "curly", "dotted", "dashed"] {
assert!(
underlines.contains(&format!("Underline style = {style}\n")),
"{underlines}"
);
}
let bright = get("91;102;53");
assert!(
bright.contains("Other attribute (tag=BRIGHT_FG_8)"),
"{bright}"
);
assert!(
bright.contains("Other attribute (tag=BRIGHT_BG_8)"),
"{bright}"
);
assert!(
bright.contains("Other attribute (tag=OVERLINE)"),
"{bright}"
);
}
fn hex(bytes: &[u8]) -> String {
bytes
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(" ")
}
#[test]
fn wasm_key_encode_example() {
use ghostty_input::{
Action,
Key,
KeyEvent,
Mods,
key_encode::{Options, encode},
};
use ghostty_vt::kitty::key::Flags;
let Some(wasm) = wasm_module() else { return };
let out = node(&[
"key",
&example("key-encode.html"),
&wasm.display().to_string(),
]);
let idx = out.lines().position(|l| l == "flags: none").unwrap();
let (with_flags, without): (Vec<_>, Vec<_>) = {
let lines: Vec<&str> = out.lines().collect();
(lines[..idx].to_vec(), lines[idx..].to_vec())
};
let parse = |lines: &[&str]| -> Vec<(String, String)> {
lines
.iter()
.filter_map(|l| l.split_once('\t'))
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect()
};
let (with_flags, without) = (parse(&with_flags), parse(&without));
let enc = |opts: Options, action: Action, key: Key, mods: Mods, utf8: &str, unshifted: char| {
let ev = KeyEvent {
action,
key,
mods,
utf8,
unshifted_codepoint: unshifted as u32,
..KeyEvent::default()
};
let mut buf = Vec::new();
encode(&mut buf, &ev, &opts).unwrap();
hex(&buf)
};
let ctrl = Mods {
ctrl: true,
..Mods::default()
};
let shift = Mods {
shift: true,
..Mods::default()
};
let alt = Mods {
alt: true,
..Mods::default()
};
let none = Mods::default();
let all = Options {
kitty_flags: Flags::TRUE,
..Options::default()
};
let legacy = Options::default();
let want_all = [
("ctrl+a", enc(all, Action::Press, Key::KeyA, ctrl, "a", 'a')),
("a", enc(all, Action::Press, Key::KeyA, none, "a", 'a')),
(
"shift+Enter",
enc(all, Action::Press, Key::Enter, shift, "", '\0'),
),
(
"ArrowUp",
enc(all, Action::Press, Key::ArrowUp, none, "", '\0'),
),
("alt+x", enc(all, Action::Press, Key::KeyX, alt, "x", 'x')),
(
"release a",
enc(all, Action::Release, Key::KeyA, none, "a", 'a'),
),
];
assert_eq!(with_flags.len(), want_all.len(), "{with_flags:?}");
for ((name, got), (wname, want)) in with_flags.iter().zip(want_all) {
assert_eq!(name, wname);
assert_eq!(*got, want, "kitty-all: {name}");
}
// Spot-check against the protocol, so a shared bug can't hide.
assert_eq!(with_flags[0].1, hex(b"\x1b[97;5u"));
assert_eq!(with_flags[2].1, hex(b"\x1b[13;2u"));
assert_eq!(with_flags[5].1, hex(b"\x1b[97;1:3u"));
let want_none = [
(
"ctrl+a",
enc(legacy, Action::Press, Key::KeyA, ctrl, "a", 'a'),
),
("a", enc(legacy, Action::Press, Key::KeyA, none, "a", 'a')),
(
"shift+Enter",
enc(legacy, Action::Press, Key::Enter, shift, "", '\0'),
),
(
"ArrowUp",
enc(legacy, Action::Press, Key::ArrowUp, none, "", '\0'),
),
(
"alt+x",
enc(legacy, Action::Press, Key::KeyX, alt, "x", 'x'),
),
(
"Escape",
enc(legacy, Action::Press, Key::Escape, none, "", '\0'),
),
("F5", enc(legacy, Action::Press, Key::F5, none, "", '\0')),
(
"unidentified",
enc(legacy, Action::Press, Key::Unidentified, none, "", '\0'),
),
];
assert_eq!(without.len(), want_none.len(), "{without:?}");
for ((name, got), (wname, want)) in without.iter().zip(want_none) {
assert_eq!(name, wname);
assert_eq!(*got, want, "legacy: {name}");
}
assert_eq!(without[0].1, "01");
assert_eq!(without[1].1, "61");
assert_eq!(without[3].1, hex(b"\x1b[A"));
// alt_esc_prefix is off by default in the C encoder, so alt+x is plain "x".
assert_eq!(without[4].1, hex(b"x"));
}