lib-vt: wasm convenience functions and a simple example (#9309)
This adds a set of Wasm convenience functions to ease memory management. These are all prefixed with `ghostty_wasm` and are documented as part of the standard Doxygen docs. I also added a very simple single-page HTML example that demonstrates how to use the Wasm module for key encoding. This also adds a bunch of safety checks to the C API to verify that valid values are actually passed to the function. This is an easy to hit bug. **AI disclosure:** The example is AI-written with Amp. I read through all the code and understand it but I can't claim there isn't a better way, I'm far from a JS expert. It is simple and works currently though. Happy to see improvements if anyone wants to contribute.pull/9313/head
parent
9dc2e5978f
commit
c133fac7e7
|
|
@ -20,6 +20,7 @@ A file for [guiding coding agents](https://agents.md/).
|
|||
## libghostty-vt
|
||||
|
||||
- Build: `zig build lib-vt`
|
||||
- Build Wasm Module: `zig build lib-vt -Dtarget=wasm32-freestanding`
|
||||
- Test: `zig build test-lib-vt`
|
||||
- Test filter: `zig build test-lib-vt -Dtest-filter=<test name>`
|
||||
- When working on libghostty-vt, do not build the full app.
|
||||
|
|
|
|||
10
Doxyfile
10
Doxyfile
|
|
@ -17,6 +17,16 @@ INLINE_SOURCES = NO
|
|||
REFERENCES_RELATION = YES
|
||||
REFERENCED_BY_RELATION = YES
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Preprocessor
|
||||
#---------------------------------------------------------------------------
|
||||
|
||||
# Enable preprocessing to handle #ifdef guards
|
||||
ENABLE_PREPROCESSING = YES
|
||||
MACRO_EXPANSION = YES
|
||||
EXPAND_ONLY_PREDEF = YES
|
||||
PREDEFINED = __wasm__
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# C API Optimization
|
||||
#---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
# WebAssembly Key Encoder Example
|
||||
|
||||
This example demonstrates how to use the Ghostty VT library from WebAssembly to encode key events into terminal escape sequences.
|
||||
|
||||
## What It Does
|
||||
|
||||
The example demonstrates using the Ghostty VT library from WebAssembly to encode key events:
|
||||
|
||||
1. Loads the `ghostty-vt.wasm` module
|
||||
2. Creates a key encoder with Kitty keyboard protocol support
|
||||
3. Creates a key event for left ctrl release
|
||||
4. Queries the required buffer size (optional)
|
||||
5. Encodes the event into a terminal escape sequence
|
||||
6. Displays the result in both hexadecimal and string format
|
||||
|
||||
## Building
|
||||
|
||||
First, build the WebAssembly module:
|
||||
|
||||
```bash
|
||||
zig build lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall
|
||||
```
|
||||
|
||||
This will create `zig-out/bin/ghostty-vt.wasm`.
|
||||
|
||||
## Running
|
||||
|
||||
**Important:** You must serve this via HTTP, not open it as a file directly. Browsers block loading WASM files from `file://` URLs.
|
||||
|
||||
From the **root of the ghostty repository**, serve with a local HTTP server:
|
||||
|
||||
```bash
|
||||
# Using Python (recommended)
|
||||
python3 -m http.server 8000
|
||||
|
||||
# Or using Node.js
|
||||
npx serve .
|
||||
|
||||
# Or using PHP
|
||||
php -S localhost:8000
|
||||
```
|
||||
|
||||
Then open your browser to:
|
||||
|
||||
```
|
||||
http://localhost:8000/example/wasm-key-encode/
|
||||
```
|
||||
|
||||
Click "Run Example" to see the key encoding in action.
|
||||
|
||||
## Expected Output
|
||||
|
||||
```
|
||||
Encoding event: left ctrl release with all Kitty flags enabled
|
||||
Required buffer size: 12 bytes
|
||||
Encoded 12 bytes
|
||||
Hex: 1b 5b 35 37 3a 33 3b 32 3a 33 75
|
||||
String: \x1b[57:3;2:3u
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The example uses the convenience allocator functions exported by the wasm module
|
||||
- Error handling is included to demonstrate proper usage patterns
|
||||
- The encoded sequence `\x1b[57:3;2:3u` is a Kitty keyboard protocol sequence for left ctrl release with all features enabled
|
||||
- The `env.log` function must be provided by the host environment for logging support
|
||||
|
||||
## Current Limitations
|
||||
|
||||
The current C API is verbose when called from WebAssembly because:
|
||||
|
||||
- Functions use output pointers requiring manual memory allocation in JavaScript
|
||||
- Options must be set via pointers to values
|
||||
- Buffer sizes require pointer parameters
|
||||
|
||||
See `WASM_API_PLAN.md` for proposed improvements to make the API more wasm-friendly.
|
||||
|
|
@ -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_buffer(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_buffer(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>
|
||||
|
|
@ -32,6 +32,7 @@
|
|||
* - @ref osc "OSC Parser" - Parse OSC (Operating System Command) 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
|
||||
*
|
||||
|
|
@ -69,6 +70,7 @@ extern "C" {
|
|||
#include <ghostty/vt/osc.h>
|
||||
#include <ghostty/vt/key.h>
|
||||
#include <ghostty/vt/paste.h>
|
||||
#include <ghostty/vt/wasm.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
/**
|
||||
* @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_buffer(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 a buffer of the specified length.
|
||||
*
|
||||
* @param len Number of bytes to allocate
|
||||
* @return Pointer to allocated buffer, or NULL if allocation failed
|
||||
* @ingroup wasm
|
||||
*/
|
||||
uint8_t* ghostty_wasm_alloc_buffer(size_t len);
|
||||
|
||||
/**
|
||||
* Free a buffer allocated by ghostty_wasm_alloc_buffer().
|
||||
*
|
||||
* @param ptr Pointer to the buffer to free, or NULL (NULL is safely ignored)
|
||||
* @param len Length of the buffer (must match the length passed to alloc)
|
||||
* @ingroup wasm
|
||||
*/
|
||||
void ghostty_wasm_free_buffer(uint8_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 */
|
||||
|
|
@ -77,7 +77,7 @@ pub fn encode(
|
|||
event: key.KeyEvent,
|
||||
opts: Options,
|
||||
) std.Io.Writer.Error!void {
|
||||
//std.log.warn("KEYENCODER event={} opts={}", .{ event, opts });
|
||||
std.log.warn("KEYENCODER event={} opts={}", .{ event, opts });
|
||||
return if (opts.kitty_flags.int() != 0) try kitty(
|
||||
writer,
|
||||
event,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ const std = @import("std");
|
|||
const builtin = @import("builtin");
|
||||
const testing = std.testing;
|
||||
|
||||
/// Convenience functions
|
||||
pub const convenience = @import("allocator/convenience.zig");
|
||||
|
||||
/// Useful alias since they're required to create Zig allocators
|
||||
pub const ZigVTable = std.mem.Allocator.VTable;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
//! This contains convenience functions for allocating various types.
|
||||
//!
|
||||
//! The primary use case for this is Wasm builds. Ghostty relies a lot on
|
||||
//! pointers to various types for ABI compatibility and creating those pointers
|
||||
//! in Wasm is tedious. This file contains a purely additive set of functions
|
||||
//! that can be exposed to the Wasm module without changing the API from the
|
||||
//! C library.
|
||||
//!
|
||||
//! Given these are convenience methods, they always use the default allocator.
|
||||
//! If a caller is using a custom allocator, they have the expertise to
|
||||
//! allocate these types manually using their custom allocator.
|
||||
|
||||
// Get our default allocator at comptime since it is known.
|
||||
const default = @import("../allocator.zig").default;
|
||||
const alloc = default(null);
|
||||
|
||||
pub const Opaque = *anyopaque;
|
||||
|
||||
pub fn allocOpaque() callconv(.c) ?*Opaque {
|
||||
return alloc.create(*anyopaque) catch return null;
|
||||
}
|
||||
|
||||
pub fn freeOpaque(ptr: ?*Opaque) callconv(.c) void {
|
||||
if (ptr) |p| alloc.destroy(p);
|
||||
}
|
||||
|
||||
pub fn allocBuffer(len: usize) callconv(.c) ?[*]u8 {
|
||||
const slice = alloc.alloc(u8, len) catch return null;
|
||||
return slice.ptr;
|
||||
}
|
||||
|
||||
pub fn freeBuffer(ptr: ?[*]u8, len: usize) callconv(.c) void {
|
||||
if (ptr) |p| alloc.free(p[0..len]);
|
||||
}
|
||||
|
||||
pub fn allocU8() callconv(.c) ?*u8 {
|
||||
return alloc.create(u8) catch return null;
|
||||
}
|
||||
|
||||
pub fn freeU8(ptr: ?*u8) callconv(.c) void {
|
||||
if (ptr) |p| alloc.destroy(p);
|
||||
}
|
||||
|
||||
pub fn allocUsize() callconv(.c) ?*usize {
|
||||
return alloc.create(usize) catch return null;
|
||||
}
|
||||
|
||||
pub fn freeUsize(ptr: ?*usize) callconv(.c) void {
|
||||
if (ptr) |p| alloc.destroy(p);
|
||||
}
|
||||
|
|
@ -126,6 +126,19 @@ comptime {
|
|||
@export(&c.key_encoder_setopt, .{ .name = "ghostty_key_encoder_setopt" });
|
||||
@export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" });
|
||||
@export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" });
|
||||
|
||||
// On Wasm we need to export our allocator convenience functions.
|
||||
if (builtin.target.cpu.arch.isWasm()) {
|
||||
const alloc = @import("lib/allocator/convenience.zig");
|
||||
@export(&alloc.allocOpaque, .{ .name = "ghostty_wasm_alloc_opaque" });
|
||||
@export(&alloc.freeOpaque, .{ .name = "ghostty_wasm_free_opaque" });
|
||||
@export(&alloc.allocBuffer, .{ .name = "ghostty_wasm_alloc_buffer" });
|
||||
@export(&alloc.freeBuffer, .{ .name = "ghostty_wasm_free_buffer" });
|
||||
@export(&alloc.allocU8, .{ .name = "ghostty_wasm_alloc_u8" });
|
||||
@export(&alloc.freeU8, .{ .name = "ghostty_wasm_free_u8" });
|
||||
@export(&alloc.allocUsize, .{ .name = "ghostty_wasm_alloc_usize" });
|
||||
@export(&alloc.freeUsize, .{ .name = "ghostty_wasm_free_usize" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ const OptionAsAlt = @import("../../input/config.zig").OptionAsAlt;
|
|||
const Result = @import("result.zig").Result;
|
||||
const KeyEvent = @import("key_event.zig").Event;
|
||||
|
||||
const log = std.log.scoped(.key_encode);
|
||||
|
||||
/// Wrapper around key encoding options that tracks the allocator for C API usage.
|
||||
const KeyEncoderWrapper = struct {
|
||||
opts: key_encode.Options,
|
||||
|
|
@ -70,6 +72,13 @@ pub fn setopt(
|
|||
option: Option,
|
||||
value: ?*const anyopaque,
|
||||
) callconv(.c) void {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(Option, @intFromEnum(option)) catch {
|
||||
log.warn("setopt invalid option value={d}", .{@intFromEnum(option)});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
return switch (option) {
|
||||
inline else => |comptime_option| setoptTyped(
|
||||
encoder_,
|
||||
|
|
@ -95,7 +104,15 @@ fn setoptTyped(
|
|||
const bits: u5 = @truncate(value.*);
|
||||
break :flags @bitCast(bits);
|
||||
},
|
||||
.macos_option_as_alt => opts.macos_option_as_alt = value.*,
|
||||
.macos_option_as_alt => {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(OptionAsAlt, @intFromEnum(value.*)) catch {
|
||||
log.warn("setopt invalid OptionAsAlt value={d}", .{@intFromEnum(value.*)});
|
||||
return;
|
||||
};
|
||||
}
|
||||
opts.macos_option_as_alt = value.*;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ const CAllocator = lib_alloc.Allocator;
|
|||
const key = @import("../../input/key.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
|
||||
const log = std.log.scoped(.key_event);
|
||||
|
||||
/// Wrapper around KeyEvent that tracks the allocator for C API usage.
|
||||
/// The UTF-8 text is not owned by this wrapper - the caller is responsible
|
||||
/// for ensuring the lifetime of any UTF-8 text set via set_utf8.
|
||||
|
|
@ -36,6 +38,13 @@ pub fn free(event_: Event) callconv(.c) void {
|
|||
}
|
||||
|
||||
pub fn set_action(event_: Event, action: key.Action) callconv(.c) void {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(key.Action, @intFromEnum(action)) catch {
|
||||
log.warn("set_action invalid action value={d}", .{@intFromEnum(action)});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
const event: *key.KeyEvent = &event_.?.event;
|
||||
event.action = action;
|
||||
}
|
||||
|
|
@ -46,6 +55,13 @@ pub fn get_action(event_: Event) callconv(.c) key.Action {
|
|||
}
|
||||
|
||||
pub fn set_key(event_: Event, k: key.Key) callconv(.c) void {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(key.Key, @intFromEnum(k)) catch {
|
||||
log.warn("set_key invalid key value={d}", .{@intFromEnum(k)});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
const event: *key.KeyEvent = &event_.?.event;
|
||||
event.key = k;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ const CAllocator = lib_alloc.Allocator;
|
|||
const osc = @import("../osc.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
|
||||
const log = std.log.scoped(.osc);
|
||||
|
||||
/// C: GhosttyOscParser
|
||||
pub const Parser = ?*osc.Parser;
|
||||
|
||||
|
|
@ -68,6 +70,13 @@ pub fn commandData(
|
|||
data: CommandData,
|
||||
out: ?*anyopaque,
|
||||
) callconv(.c) bool {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(CommandData, @intFromEnum(data)) catch {
|
||||
log.warn("commandData invalid data value={d}", .{@intFromEnum(data)});
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
return switch (data) {
|
||||
inline else => |comptime_data| commandDataTyped(
|
||||
command_,
|
||||
|
|
|
|||
Loading…
Reference in New Issue