688 lines
30 KiB
HTML
688 lines
30 KiB
HTML
<!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>
|