From 61fe78c1d304b31400609d8191e427466ebcec25 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Oct 2025 20:32:42 -0700 Subject: [PATCH] lib-vt: expose key encoding as a C API --- AGENTS.md | 9 +- example/c-vt-key-encode/README.md | 22 + example/c-vt-key-encode/build.zig | 42 ++ example/c-vt-key-encode/build.zig.zon | 24 + example/c-vt-key-encode/src/main.c | 59 +++ include/ghostty/vt.h | 639 +++++++++++++++++++++++++- src/input/key_encode.zig | 10 + src/lib_vt.zig | 20 + src/terminal/c/key_encode.zig | 269 +++++++++++ src/terminal/c/key_event.zig | 253 ++++++++++ src/terminal/c/main.zig | 26 ++ 11 files changed, 1371 insertions(+), 2 deletions(-) create mode 100644 example/c-vt-key-encode/README.md create mode 100644 example/c-vt-key-encode/build.zig create mode 100644 example/c-vt-key-encode/build.zig.zon create mode 100644 example/c-vt-key-encode/src/main.c create mode 100644 src/terminal/c/key_encode.zig create mode 100644 src/terminal/c/key_event.zig diff --git a/AGENTS.md b/AGENTS.md index 2e90fd94e..14fff7b3d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,10 +13,17 @@ A file for [guiding coding agents](https://agents.md/). ## Directory Structure - Shared Zig core: `src/` -- C API: `include/ghostty.h` +- C API: `include` - macOS app: `macos/` - GTK (Linux and FreeBSD) app: `src/apprt/gtk` +## libghostty-vt + +- Build: `zig build lib-vt` +- Test: `zig build test-lib-vt` +- Test filter: `zig build test-lib-vt -Dtest-filter=` +- When working on libghostty-vt, do not build the full app. + ## macOS App - Do not use `xcodebuild` diff --git a/example/c-vt-key-encode/README.md b/example/c-vt-key-encode/README.md new file mode 100644 index 000000000..05ee3fc31 --- /dev/null +++ b/example/c-vt-key-encode/README.md @@ -0,0 +1,22 @@ +# Example: `ghostty-vt` C Key Encoding + +This example demonstrates how to use the `ghostty-vt` C library to encode key +events into terminal escape sequences. + +This example specifically shows how to: + +1. Create a key encoder with the C API +2. Configure Kitty keyboard protocol flags (this example uses KKP) +3. Create and configure a key event +4. Encode the key event into a terminal escape sequence + +The example encodes a Ctrl key release event with the Ctrl modifier set, +producing the escape sequence `\x1b[57442;5:3u`. + +## Usage + +Run the program: + +```shell-session +zig build run +``` diff --git a/example/c-vt-key-encode/build.zig b/example/c-vt-key-encode/build.zig new file mode 100644 index 000000000..b4b759744 --- /dev/null +++ b/example/c-vt-key-encode/build.zig @@ -0,0 +1,42 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const run_step = b.step("run", "Run the app"); + + const exe_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + }); + exe_mod.addCSourceFiles(.{ + .root = b.path("src"), + .files = &.{"main.c"}, + }); + + // You'll want to use a lazy dependency here so that ghostty is only + // downloaded if you actually need it. + if (b.lazyDependency("ghostty", .{ + // Setting simd to false will force a pure static build that + // doesn't even require libc, but it has a significant performance + // penalty. If your embedding app requires libc anyway, you should + // always keep simd enabled. + // .simd = false, + })) |dep| { + exe_mod.linkLibrary(dep.artifact("ghostty-vt")); + } + + // Exe + const exe = b.addExecutable(.{ + .name = "c_vt_key_encode", + .root_module = exe_mod, + }); + b.installArtifact(exe); + + // Run + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + run_step.dependOn(&run_cmd.step); +} diff --git a/example/c-vt-key-encode/build.zig.zon b/example/c-vt-key-encode/build.zig.zon new file mode 100644 index 000000000..5da1a9168 --- /dev/null +++ b/example/c-vt-key-encode/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt, + .version = "0.0.0", + .fingerprint = 0x413a8529b1255f9a, + .minimum_zig_version = "0.15.1", + .dependencies = .{ + // Ghostty dependency. In reality, you'd probably use a URL-based + // dependency like the one showed (and commented out) below this one. + // We use a path dependency here for simplicity and to ensure our + // examples always test against the source they're bundled with. + .ghostty = .{ .path = "../../" }, + + // Example of what a URL-based dependency looks like: + // .ghostty = .{ + // .url = "https://github.com/ghostty-org/ghostty/archive/COMMIT.tar.gz", + // .hash = "N-V-__8AAMVLTABmYkLqhZPLXnMl-KyN38R8UVYqGrxqO36s", + // }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/example/c-vt-key-encode/src/main.c b/example/c-vt-key-encode/src/main.c new file mode 100644 index 000000000..82444f99d --- /dev/null +++ b/example/c-vt-key-encode/src/main.c @@ -0,0 +1,59 @@ +#include +#include +#include +#include +#include + +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; +} diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index 4b930a96f..7815ebb81 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -27,6 +27,7 @@ * @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 allocator "Memory Management" - Memory management and custom allocators * @@ -319,7 +320,643 @@ typedef struct { /** @} */ // end of allocator group //------------------------------------------------------------------- -// Functions +// Key Encoding + +/** @defgroup key Key Encoding + * + * Utilities for encoding key events into terminal escape sequences, + * supporting both legacy encoding as well as Kitty Keyboard Protocol. + * + * @{ + */ + +/** + * Opaque handle to a key event. + * + * This handle represents a keyboard input event containing information about + * the physical key pressed, modifiers, and generated text. The event can be + * configured using the `ghostty_key_event_set_*` functions. + * + * @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; + +/** + * 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; + +/** + * 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); + +/** + * Opaque handle to a key encoder instance. + * + * This handle represents a key encoder that converts key events into terminal + * escape sequences. The encoder supports both legacy encoding and the Kitty + * Keyboard Protocol, depending on the options set. + * + * @ingroup key + */ +typedef struct GhosttyKeyEncoder *GhosttyKeyEncoder; + +/** + * 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 + * + * @ingroup key + */ +GhosttyResult ghostty_key_encoder_encode(GhosttyKeyEncoder encoder, GhosttyKeyEvent event, char *out_buf, size_t out_buf_size, size_t *out_len); + +/** @} */ // end of key group + +//------------------------------------------------------------------- +// OSC Parser /** @defgroup osc OSC Parser * diff --git a/src/input/key_encode.zig b/src/input/key_encode.zig index c35cdebaa..fa641c1aa 100644 --- a/src/input/key_encode.zig +++ b/src/input/key_encode.zig @@ -36,6 +36,16 @@ pub const Options = struct { /// docs for a more detailed description of why this is needed. macos_option_as_alt: OptionAsAlt = .false, + pub const default: Options = .{ + .cursor_key_application = false, + .keypad_key_application = false, + .ignore_keypad_with_numlock = false, + .alt_esc_prefix = false, + .modify_other_keys_state_2 = false, + .kitty_flags = .disabled, + .macos_option_as_alt = .false, + }; + /// Initialize our options from the terminal state. /// /// Note that `macos_option_as_alt` cannot be determined from diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 4d51d1062..73a030333 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -102,6 +102,26 @@ comptime { @export(&c.osc_end, .{ .name = "ghostty_osc_end" }); @export(&c.osc_command_type, .{ .name = "ghostty_osc_command_type" }); @export(&c.osc_command_data, .{ .name = "ghostty_osc_command_data" }); + @export(&c.key_event_new, .{ .name = "ghostty_key_event_new" }); + @export(&c.key_event_free, .{ .name = "ghostty_key_event_free" }); + @export(&c.key_event_set_action, .{ .name = "ghostty_key_event_set_action" }); + @export(&c.key_event_get_action, .{ .name = "ghostty_key_event_get_action" }); + @export(&c.key_event_set_key, .{ .name = "ghostty_key_event_set_key" }); + @export(&c.key_event_get_key, .{ .name = "ghostty_key_event_get_key" }); + @export(&c.key_event_set_mods, .{ .name = "ghostty_key_event_set_mods" }); + @export(&c.key_event_get_mods, .{ .name = "ghostty_key_event_get_mods" }); + @export(&c.key_event_set_consumed_mods, .{ .name = "ghostty_key_event_set_consumed_mods" }); + @export(&c.key_event_get_consumed_mods, .{ .name = "ghostty_key_event_get_consumed_mods" }); + @export(&c.key_event_set_composing, .{ .name = "ghostty_key_event_set_composing" }); + @export(&c.key_event_get_composing, .{ .name = "ghostty_key_event_get_composing" }); + @export(&c.key_event_set_utf8, .{ .name = "ghostty_key_event_set_utf8" }); + @export(&c.key_event_get_utf8, .{ .name = "ghostty_key_event_get_utf8" }); + @export(&c.key_event_set_unshifted_codepoint, .{ .name = "ghostty_key_event_set_unshifted_codepoint" }); + @export(&c.key_event_get_unshifted_codepoint, .{ .name = "ghostty_key_event_get_unshifted_codepoint" }); + @export(&c.key_encoder_new, .{ .name = "ghostty_key_encoder_new" }); + @export(&c.key_encoder_free, .{ .name = "ghostty_key_encoder_free" }); + @export(&c.key_encoder_setopt, .{ .name = "ghostty_key_encoder_setopt" }); + @export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" }); } } diff --git a/src/terminal/c/key_encode.zig b/src/terminal/c/key_encode.zig new file mode 100644 index 000000000..96754d884 --- /dev/null +++ b/src/terminal/c/key_encode.zig @@ -0,0 +1,269 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const lib_alloc = @import("../../lib/allocator.zig"); +const CAllocator = lib_alloc.Allocator; +const key_encode = @import("../../input/key_encode.zig"); +const key_event = @import("key_event.zig"); +const KittyFlags = @import("../../terminal/kitty/key.zig").Flags; +const OptionAsAlt = @import("../../input/config.zig").OptionAsAlt; +const Result = @import("result.zig").Result; +const KeyEvent = @import("key_event.zig").Event; + +/// Wrapper around key encoding options that tracks the allocator for C API usage. +const KeyEncoderWrapper = struct { + opts: key_encode.Options, + alloc: Allocator, +}; + +/// C: GhosttyKeyEncoder +pub const Encoder = ?*KeyEncoderWrapper; + +pub fn new( + alloc_: ?*const CAllocator, + result: *Encoder, +) callconv(.c) Result { + const alloc = lib_alloc.default(alloc_); + const ptr = alloc.create(KeyEncoderWrapper) catch + return .out_of_memory; + ptr.* = .{ + .opts = .{}, + .alloc = alloc, + }; + result.* = ptr; + return .success; +} + +pub fn free(encoder_: Encoder) callconv(.c) void { + const wrapper = encoder_ orelse return; + const alloc = wrapper.alloc; + alloc.destroy(wrapper); +} + +/// C: GhosttyKeyEncoderOption +pub const Option = enum(c_int) { + cursor_key_application = 0, + keypad_key_application = 1, + ignore_keypad_with_numlock = 2, + alt_esc_prefix = 3, + modify_other_keys_state_2 = 4, + kitty_flags = 5, + macos_option_as_alt = 6, + + /// Input type expected for setting the option. + pub fn InType(comptime self: Option) type { + return switch (self) { + .cursor_key_application, + .keypad_key_application, + .ignore_keypad_with_numlock, + .alt_esc_prefix, + .modify_other_keys_state_2, + => bool, + .kitty_flags => u8, + .macos_option_as_alt => OptionAsAlt, + }; + } +}; + +pub fn setopt( + encoder_: Encoder, + option: Option, + value: ?*const anyopaque, +) callconv(.c) void { + return switch (option) { + inline else => |comptime_option| setoptTyped( + encoder_, + comptime_option, + @ptrCast(@alignCast(value orelse return)), + ), + }; +} + +fn setoptTyped( + encoder_: Encoder, + comptime option: Option, + value: *const option.InType(), +) void { + const opts = &encoder_.?.opts; + switch (option) { + .cursor_key_application => opts.cursor_key_application = value.*, + .keypad_key_application => opts.keypad_key_application = value.*, + .ignore_keypad_with_numlock => opts.ignore_keypad_with_numlock = value.*, + .alt_esc_prefix => opts.alt_esc_prefix = value.*, + .modify_other_keys_state_2 => opts.modify_other_keys_state_2 = value.*, + .kitty_flags => opts.kitty_flags = flags: { + const bits: u5 = @truncate(value.*); + break :flags @bitCast(bits); + }, + .macos_option_as_alt => opts.macos_option_as_alt = value.*, + } +} + +pub fn encode( + encoder_: Encoder, + event_: KeyEvent, + out_: ?[*]u8, + out_len: usize, + out_written: *usize, +) callconv(.c) Result { + // Attempt to write to this buffer + var writer: std.Io.Writer = .fixed(if (out_) |out| out[0..out_len] else &.{}); + key_encode.encode( + &writer, + event_.?.event, + encoder_.?.opts, + ) catch |err| switch (err) { + error.WriteFailed => { + // If we don't have space, use a discarding writer to count + // how much space we would have needed. + var discarding: std.Io.Writer.Discarding = .init(&.{}); + key_encode.encode( + &discarding.writer, + event_.?.event, + encoder_.?.opts, + ) catch unreachable; + + out_written.* = discarding.count; + return .out_of_memory; + }, + }; + + out_written.* = writer.end; + return .success; +} + +test "alloc" { + const testing = std.testing; + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + free(e); +} + +test "setopt bool" { + const testing = std.testing; + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Test setting bool options + const val_true: bool = true; + setopt(e, .cursor_key_application, &val_true); + try testing.expect(e.?.opts.cursor_key_application); + + const val_false: bool = false; + setopt(e, .cursor_key_application, &val_false); + try testing.expect(!e.?.opts.cursor_key_application); + + setopt(e, .keypad_key_application, &val_true); + try testing.expect(e.?.opts.keypad_key_application); +} + +test "setopt kitty flags" { + const testing = std.testing; + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Test setting kitty flags + const flags: KittyFlags = .{ + .disambiguate = true, + .report_events = true, + }; + const flags_int: u8 = @intCast(flags.int()); + setopt(e, .kitty_flags, &flags_int); + try testing.expect(e.?.opts.kitty_flags.disambiguate); + try testing.expect(e.?.opts.kitty_flags.report_events); + try testing.expect(!e.?.opts.kitty_flags.report_alternates); +} + +test "setopt macos option as alt" { + const testing = std.testing; + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Test setting option as alt + const opt_left: OptionAsAlt = .left; + setopt(e, .macos_option_as_alt, &opt_left); + try testing.expectEqual(OptionAsAlt.left, e.?.opts.macos_option_as_alt); + + const opt_true: OptionAsAlt = .true; + setopt(e, .macos_option_as_alt, &opt_true); + try testing.expectEqual(OptionAsAlt.true, e.?.opts.macos_option_as_alt); +} + +test "encode: kitty ctrl release with ctrl mod set" { + const testing = std.testing; + + // Create encoder + var encoder: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &encoder, + )); + defer free(encoder); + + // Set kitty flags with all features enabled + { + const flags: KittyFlags = .{ + .disambiguate = true, + .report_events = true, + .report_alternates = true, + .report_all = true, + .report_associated = true, + }; + const flags_int: u8 = @intCast(flags.int()); + setopt(encoder, .kitty_flags, &flags_int); + } + + // Create key event + var event: key_event.Event = undefined; + try testing.expectEqual(Result.success, key_event.new( + &lib_alloc.test_allocator, + &event, + )); + defer key_event.free(event); + + // Set event properties: release action, ctrl key, ctrl modifier + key_event.set_action(event, .release); + key_event.set_key(event, .control_left); + key_event.set_mods(event, .{ .ctrl = true }); + + // Encode null should give us the length required + var required: usize = 0; + try testing.expectEqual(Result.out_of_memory, encode( + encoder, + event, + null, + 0, + &required, + )); + + // Encode the key event + var buf: [128]u8 = undefined; + var written: usize = 0; + try testing.expectEqual(Result.success, encode( + encoder, + event, + &buf, + buf.len, + &written, + )); + try testing.expectEqual(required, written); + + // Expected: ESC[57442;5:3u (ctrl key code with mods and release event) + const actual = buf[0..written]; + try testing.expectEqualStrings("\x1b[57442;5:3u", actual); +} diff --git a/src/terminal/c/key_event.zig b/src/terminal/c/key_event.zig new file mode 100644 index 000000000..5befe4384 --- /dev/null +++ b/src/terminal/c/key_event.zig @@ -0,0 +1,253 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const lib_alloc = @import("../../lib/allocator.zig"); +const CAllocator = lib_alloc.Allocator; +const key = @import("../../input/key.zig"); +const Result = @import("result.zig").Result; + +/// 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. +const KeyEventWrapper = struct { + event: key.KeyEvent = .{}, + alloc: Allocator, +}; + +/// C: GhosttyKeyEvent +pub const Event = ?*KeyEventWrapper; + +pub fn new( + alloc_: ?*const CAllocator, + result: *Event, +) callconv(.c) Result { + const alloc = lib_alloc.default(alloc_); + const ptr = alloc.create(KeyEventWrapper) catch + return .out_of_memory; + ptr.* = .{ .alloc = alloc }; + result.* = ptr; + return .success; +} + +pub fn free(event_: Event) callconv(.c) void { + const wrapper = event_ orelse return; + const alloc = wrapper.alloc; + alloc.destroy(wrapper); +} + +pub fn set_action(event_: Event, action: key.Action) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.action = action; +} + +pub fn get_action(event_: Event) callconv(.c) key.Action { + const event: *key.KeyEvent = &event_.?.event; + return event.action; +} + +pub fn set_key(event_: Event, k: key.Key) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.key = k; +} + +pub fn get_key(event_: Event) callconv(.c) key.Key { + const event: *key.KeyEvent = &event_.?.event; + return event.key; +} + +pub fn set_mods(event_: Event, mods: key.Mods) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.mods = mods; +} + +pub fn get_mods(event_: Event) callconv(.c) key.Mods { + const event: *key.KeyEvent = &event_.?.event; + return event.mods; +} + +pub fn set_consumed_mods(event_: Event, consumed_mods: key.Mods) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.consumed_mods = consumed_mods; +} + +pub fn get_consumed_mods(event_: Event) callconv(.c) key.Mods { + const event: *key.KeyEvent = &event_.?.event; + return event.consumed_mods; +} + +pub fn set_composing(event_: Event, composing: bool) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.composing = composing; +} + +pub fn get_composing(event_: Event) callconv(.c) bool { + const event: *key.KeyEvent = &event_.?.event; + return event.composing; +} + +pub fn set_utf8(event_: Event, utf8: ?[*]const u8, len: usize) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.utf8 = if (utf8) |ptr| ptr[0..len] else ""; +} + +pub fn get_utf8(event_: Event, len: ?*usize) callconv(.c) ?[*]const u8 { + const event: *key.KeyEvent = &event_.?.event; + if (len) |l| l.* = event.utf8.len; + return if (event.utf8.len == 0) null else event.utf8.ptr; +} + +pub fn set_unshifted_codepoint(event_: Event, codepoint: u32) callconv(.c) void { + const event: *key.KeyEvent = &event_.?.event; + event.unshifted_codepoint = @truncate(codepoint); +} + +pub fn get_unshifted_codepoint(event_: Event) callconv(.c) u32 { + const event: *key.KeyEvent = &event_.?.event; + return event.unshifted_codepoint; +} + +test "alloc" { + const testing = std.testing; + var e: Event = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + free(e); +} + +test "set" { + const testing = std.testing; + var e: Event = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Test action + set_action(e, .press); + try testing.expectEqual(key.Action.press, e.?.event.action); + + // Test key + set_key(e, .key_a); + try testing.expectEqual(key.Key.key_a, e.?.event.key); + + // Test mods + const mods: key.Mods = .{ .shift = true, .ctrl = true }; + set_mods(e, mods); + try testing.expect(e.?.event.mods.shift); + try testing.expect(e.?.event.mods.ctrl); + + // Test consumed mods + const consumed: key.Mods = .{ .shift = true }; + set_consumed_mods(e, consumed); + try testing.expect(e.?.event.consumed_mods.shift); + try testing.expect(!e.?.event.consumed_mods.ctrl); + + // Test composing + set_composing(e, true); + try testing.expect(e.?.event.composing); + + // Test UTF-8 + const text = "hello"; + set_utf8(e, text.ptr, text.len); + try testing.expectEqualStrings(text, e.?.event.utf8); + + // Test UTF-8 null + set_utf8(e, null, 0); + try testing.expectEqualStrings("", e.?.event.utf8); + + // Test unshifted codepoint + set_unshifted_codepoint(e, 'a'); + try testing.expectEqual(@as(u21, 'a'), e.?.event.unshifted_codepoint); +} + +test "get" { + const testing = std.testing; + var e: Event = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Set some values + set_action(e, .repeat); + set_key(e, .key_z); + + const mods: key.Mods = .{ .alt = true, .super = true }; + set_mods(e, mods); + + const consumed: key.Mods = .{ .alt = true }; + set_consumed_mods(e, consumed); + + set_composing(e, true); + + const text = "test"; + set_utf8(e, text.ptr, text.len); + + set_unshifted_codepoint(e, 'z'); + + // Get them back + try testing.expectEqual(key.Action.repeat, get_action(e)); + try testing.expectEqual(key.Key.key_z, get_key(e)); + + const got_mods = get_mods(e); + try testing.expect(got_mods.alt); + try testing.expect(got_mods.super); + + const got_consumed = get_consumed_mods(e); + try testing.expect(got_consumed.alt); + try testing.expect(!got_consumed.super); + + try testing.expect(get_composing(e)); + + var utf8_len: usize = undefined; + const got_utf8 = get_utf8(e, &utf8_len); + try testing.expect(got_utf8 != null); + try testing.expectEqual(@as(usize, 4), utf8_len); + try testing.expectEqualStrings("test", got_utf8.?[0..utf8_len]); + + try testing.expectEqual(@as(u32, 'z'), get_unshifted_codepoint(e)); +} + +test "complete key event" { + const testing = std.testing; + var e: Event = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Build a complete key event for shift+a + set_action(e, .press); + set_key(e, .key_a); + + const mods: key.Mods = .{ .shift = true }; + set_mods(e, mods); + + const consumed: key.Mods = .{ .shift = true }; + set_consumed_mods(e, consumed); + + const text = "A"; + set_utf8(e, text.ptr, text.len); + + set_unshifted_codepoint(e, 'a'); + + // Verify all fields + try testing.expectEqual(key.Action.press, e.?.event.action); + try testing.expectEqual(key.Key.key_a, e.?.event.key); + try testing.expect(e.?.event.mods.shift); + try testing.expect(e.?.event.consumed_mods.shift); + try testing.expectEqualStrings("A", e.?.event.utf8); + try testing.expectEqual(@as(u21, 'a'), e.?.event.unshifted_codepoint); + + // Also test the getter + var utf8_len: usize = undefined; + const got_utf8 = get_utf8(e, &utf8_len); + try testing.expect(got_utf8 != null); + try testing.expectEqual(@as(usize, 1), utf8_len); + try testing.expectEqualStrings("A", got_utf8.?[0..utf8_len]); +} diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 68fd77edd..500dbf56c 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -1,4 +1,6 @@ pub const osc = @import("osc.zig"); +pub const key_event = @import("key_event.zig"); +pub const key_encode = @import("key_encode.zig"); // The full C API, unexported. pub const osc_new = osc.new; @@ -9,8 +11,32 @@ pub const osc_end = osc.end; pub const osc_command_type = osc.commandType; pub const osc_command_data = osc.commandData; +pub const key_event_new = key_event.new; +pub const key_event_free = key_event.free; +pub const key_event_set_action = key_event.set_action; +pub const key_event_get_action = key_event.get_action; +pub const key_event_set_key = key_event.set_key; +pub const key_event_get_key = key_event.get_key; +pub const key_event_set_mods = key_event.set_mods; +pub const key_event_get_mods = key_event.get_mods; +pub const key_event_set_consumed_mods = key_event.set_consumed_mods; +pub const key_event_get_consumed_mods = key_event.get_consumed_mods; +pub const key_event_set_composing = key_event.set_composing; +pub const key_event_get_composing = key_event.get_composing; +pub const key_event_set_utf8 = key_event.set_utf8; +pub const key_event_get_utf8 = key_event.get_utf8; +pub const key_event_set_unshifted_codepoint = key_event.set_unshifted_codepoint; +pub const key_event_get_unshifted_codepoint = key_event.get_unshifted_codepoint; + +pub const key_encoder_new = key_encode.new; +pub const key_encoder_free = key_encode.free; +pub const key_encoder_setopt = key_encode.setopt; +pub const key_encoder_encode = key_encode.encode; + test { _ = osc; + _ = key_event; + _ = key_encode; // We want to make sure we run the tests for the C allocator interface. _ = @import("../../lib/allocator.zig");