feat(vt-capi): port the libghostty-vt C API
Ports ZIG/src/lib_vt.zig and terminal/c/* as crates/ghostty-vt-capi (cdylib + staticlib + rlib, soname libghostty-vt.so) implementing the unchanged include/ghostty/vt.h + vt/*.h ABI: OSC parser, SGR parser, key events and key encoding, paste safety, colors, result codes - all 38 native symbols, plus the ghostty_wasm_* helpers on wasm32. Verification: - tests/abi.rs generates a C probe from the headers, compiled with cc, that checks sizeof/alignof/offsetof of every struct/union, every enumerator and #define against Rust, and links every declared symbol; nm -D exports equal the declared functions. - Ghostty's example/c-vt* programs compile unchanged against the Rust library (shared and static) and produce the expected output. - The wasm32 build runs Ghostty's wasm-key-encode/wasm-sgr example scripts unmodified under node (upstream's wasm-sgr tag table is stale vs sgr.h; a regenerated copy checks full output). Test Plan: - cargo test -p ghostty-vt-capi (22 unit + 2 ABI + 5 C + 3 wasm tests)
This commit is contained in:
1 parent
5b9a0723a9
commit
75252a7651
35 files changed
+6911
No files matched your search
@@ -0,0 +1,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"
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) });
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) = §ions(&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"));
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
# libghostty-vt C API (Phase 8, agent L2)
|
||||
|
||||
Crate `crates/ghostty-vt-capi` (crate types `cdylib` + `staticlib` + `rlib`).
|
||||
Depends only on `ghostty-vt` and `ghostty-input` (no `ghostty-terminal`), so it
|
||||
builds for `wasm32-unknown-unknown`.
|
||||
|
||||
## Mapping
|
||||
|
||||
| Zig | Rust |
|
||||
| --- | --- |
|
||||
| `lib_vt.zig` `@export` list | `#[unsafe(no_mangle)] extern "C"` fns (38 native symbols) |
|
||||
| `terminal/c/color.zig` | `color.rs` (`ghostty_color_rgb_get`, `GhosttyColorRgb`) |
|
||||
| `terminal/c/osc.zig` | `osc.rs` |
|
||||
| `terminal/c/sgr.zig` | `sgr.rs` (+ wasm attribute helpers in `wasm.rs`) |
|
||||
| `terminal/c/key_event.zig` | `key_event.rs` |
|
||||
| `terminal/c/key_encode.zig` | `key_encode.rs` |
|
||||
| `terminal/c/paste.zig` | `paste.rs` |
|
||||
| `terminal/c/result.zig` | `result.rs` |
|
||||
| `lib/allocator.zig` (C allocator) | `alloc.rs` |
|
||||
| `lib/allocator/convenience.zig` | `wasm.rs` (exported only on wasm32) |
|
||||
|
||||
Headers are copied unchanged to `include/ghostty/` (`diff -r` against
|
||||
`ZIG/include/ghostty` is empty). The C examples (`examples/c/*.c`) and wasm
|
||||
examples (`examples/wasm/*.html`) are unchanged copies of
|
||||
`ZIG/example/{c-vt,c-vt-key-encode,c-vt-paste,c-vt-sgr,wasm-key-encode,wasm-sgr}`.
|
||||
|
||||
## Library file names
|
||||
|
||||
Cargo forbids hyphens in `[lib] name`, so the artifacts are
|
||||
`libghostty_vt_capi.{so,a}` / `ghostty_vt_capi.wasm`. `build.rs` sets the
|
||||
soname to `libghostty-vt.so`; `just vt-lib [profile]` (and the integration
|
||||
tests) install copies as `libghostty-vt.{so,a}` and `ghostty-vt.wasm`
|
||||
(upstream's names).
|
||||
|
||||
## Tests
|
||||
|
||||
* unit tests: ports of the Zig `test` blocks in `terminal/c/*.zig`
|
||||
(22 tests; the `lib_alloc.test_allocator` leak-check variants map to plain
|
||||
alloc/free round trips).
|
||||
* `tests/abi.rs`: generates a C probe from the headers (enum typedefs and
|
||||
`#define`s are parsed from the header text, every declared function's address
|
||||
is taken), compiles it with `cc`, links it against `libghostty-vt.so`, and
|
||||
compares sizeof/alignof/offsetof of every struct/union, sizeof of every enum
|
||||
and typedef, the value of every enumerator and `#define` against the Rust
|
||||
definitions (`size_of`/`align_of`/`offset_of!`, `Key::ALL`, discriminants).
|
||||
A second test checks `nm -D` exports == declared functions.
|
||||
* `tests/c_examples.rs`: compiles the four C examples with `cc` and checks
|
||||
stdout (upstream has no expected-output files; the strings were derived by
|
||||
reading the example sources), plus a static-link run against `libghostty-vt.a`.
|
||||
* `tests/wasm_examples.rs` + `tests/wasm/run.mjs`: builds
|
||||
`--target wasm32-unknown-unknown`, checks every header function (including
|
||||
the `__wasm__`-only `ghostty_wasm_*`) is exported and that the module has no
|
||||
imports, then runs the two wasm example pages' unmodified `<script>` under node
|
||||
with a DOM stub; key-encode output is compared against the native Rust
|
||||
encoder, SGR output against literals. Skipped (with a note) if `node` or the
|
||||
wasm target is missing.
|
||||
|
||||
`cargo test -p ghostty-vt-capi` runs all of it (`just vt-test`). The tests run a
|
||||
nested `cargo build` of the crate (and the wasm target) into the same target dir.
|
||||
|
||||
## Deviations / notes
|
||||
|
||||
* `unsafe_code = "deny"` for this crate (lints table copied from the workspace
|
||||
because `workspace = true` cannot be partially overridden); FFI modules carry
|
||||
`#![allow(unsafe_code)]` and SAFETY comments.
|
||||
* Enum parameters from C (`action`, `key`, `option`, `data`) are taken as
|
||||
`c_int` and validated (Zig only validates in safe builds); invalid values are
|
||||
ignored/rejected like Zig's `runtime_safety` path. Null handles are no-ops /
|
||||
return defaults instead of Zig's `.?` panic.
|
||||
* Wrapper objects and the SGR parameter copy use the caller's
|
||||
`GhosttyAllocator` (copied by value at `*_new`); interior Rust collections
|
||||
(OSC buffer) always use the Rust global allocator.
|
||||
* `ghostty_osc_end` returns a pointer to a command slot owned by the parser
|
||||
(Zig returns a pointer to the parser's command); the window title is
|
||||
re-materialized NUL-terminated there. Valid until the next `end`/`free`.
|
||||
* `GhosttyOscCommandType` can also yield 23/24 (`kitty_clipboard_protocol`,
|
||||
`context_signal`) which exist in the Rust/Zig parser but not in `osc.h`.
|
||||
* Key event `utf8` is a Rust `&str` in `ghostty-input`; bytes that are not
|
||||
valid UTF-8 are truncated to the valid prefix when encoding. `get_utf8`
|
||||
returns the caller's original pointer/len. Unknown mod bits (padding) are
|
||||
dropped on `set_mods`.
|
||||
* `ghostty_sgr_set_params` with `separators != NULL` and `len > 24` returns
|
||||
`INVALID_VALUE` after replacing the params (same order as Zig). Non-NULL
|
||||
required `params` with `len > 0` and NULL pointer returns `INVALID_VALUE`
|
||||
(Zig: UB).
|
||||
* Upstream wasm builds import `env.log` for `std.log`; the Rust build has no
|
||||
host imports (the `log` crate has no logger installed).
|
||||
* Upstream finding: `example/wasm-sgr/index.html` has a stale `SGR_ATTR_TAGS`
|
||||
table (header changed in upstream commit `a384af5e2`, example not updated), so
|
||||
the stock page mislabels e.g. direct colors. The test runs the stock page
|
||||
(checks only the still-correct underline line) and a copy whose table is
|
||||
regenerated from `sgr.h` (checks full output).
|
||||
* The Zig reference could not be built here (needs Zig 0.15; 0.16 installed),
|
||||
so outputs were validated against the Zig sources and the protocol rather
|
||||
than a Zig-built library.
|
||||
Reference in new issue
Block a user