From 943d3d2e8906cbd610868c36eccfc3a1360e0fd2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 15 Mar 2026 07:03:44 -0700 Subject: [PATCH] vt: add setopt_from_terminal to C API Expose the key encoder Options.fromTerminal function to the C API as ghostty_key_encoder_setopt_from_terminal. This lets C callers sync all terminal-derived encoding options (cursor key application mode, keypad mode, alt escape prefix, modifyOtherKeys, and Kitty flags) in a single call instead of setting each option individually. --- include/ghostty/vt/key.h | 36 +++++++++++++++-- include/ghostty/vt/key/encoder.h | 24 +++++++++++ include/ghostty/vt/terminal.h | 3 ++ src/lib_vt.zig | 1 + src/terminal/c/key_encode.zig | 68 ++++++++++++++++++++++++++++++++ src/terminal/c/main.zig | 1 + 6 files changed, 130 insertions(+), 3 deletions(-) diff --git a/include/ghostty/vt/key.h b/include/ghostty/vt/key.h index 772b5d43b..e82a7596c 100644 --- a/include/ghostty/vt/key.h +++ b/include/ghostty/vt/key.h @@ -15,7 +15,9 @@ * ## Basic Usage * * 1. Create an encoder instance with ghostty_key_encoder_new() - * 2. Configure encoder options with ghostty_key_encoder_setopt(). + * 2. Configure encoder options with ghostty_key_encoder_setopt() + * or ghostty_key_encoder_setopt_from_terminal() if you have a + * GhosttyTerminal. * 3. For each key event: * - Create a key event with ghostty_key_event_new() * - Set event properties (action, key, modifiers, etc.) @@ -25,6 +27,9 @@ * changing its properties. * 4. Free the encoder with ghostty_key_encoder_free() when done * + * For a complete working example, see example/c-vt-key-encode in the + * repository. + * * ## Example * * @code{.c} @@ -66,8 +71,33 @@ * } * @endcode * - * For a complete working example, see example/c-vt-key-encode in the - * repository. + * ## Example: Encoding with Terminal State + * + * When you have a GhosttyTerminal, you can sync its modes (cursor key + * application, Kitty flags, etc.) into the encoder automatically: + * + * @code{.c} + * // Create a terminal and feed it some VT data that changes modes + * GhosttyTerminal terminal; + * ghostty_terminal_new(NULL, &terminal, + * (GhosttyTerminalOptions){.cols = 80, .rows = 24, .max_scrollback = 0}); + * + * // Application might write data that enables Kitty keyboard protocol, etc. + * ghostty_terminal_vt_write(terminal, vt_data, vt_len); + * + * // Create an encoder and sync its options from the terminal + * GhosttyKeyEncoder encoder; + * ghostty_key_encoder_new(NULL, &encoder); + * ghostty_key_encoder_setopt_from_terminal(encoder, terminal); + * + * // Encode a key event using the terminal-derived options + * char buf[128]; + * size_t written = 0; + * ghostty_key_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * + * ghostty_key_encoder_free(encoder); + * ghostty_terminal_free(terminal); + * @endcode * * @{ */ diff --git a/include/ghostty/vt/key/encoder.h b/include/ghostty/vt/key/encoder.h index 5e8b6d80b..3053d73ef 100644 --- a/include/ghostty/vt/key/encoder.h +++ b/include/ghostty/vt/key/encoder.h @@ -11,6 +11,7 @@ #include #include #include +#include #include /** @@ -140,6 +141,10 @@ void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); * protocol selection (Kitty keyboard protocol flags), and platform-specific * behaviors (macOS option-as-alt). * + * If you are using a terminal instance, you can set the key encoding + * options based on the active terminal state (e.g. legacy vs Kitty mode + * and associated flags) with ghostty_key_encoder_setopt_from_terminal(). + * * A null pointer value does nothing. It does not reset the value to the * default. The setopt call will do nothing. * @@ -151,6 +156,25 @@ void ghostty_key_encoder_free(GhosttyKeyEncoder encoder); */ void ghostty_key_encoder_setopt(GhosttyKeyEncoder encoder, GhosttyKeyEncoderOption option, const void *value); +/** + * Set encoder options from a terminal's current state. + * + * Reads the terminal's current modes and flags and applies them to the + * encoder's options. This sets cursor key application mode, keypad mode, + * alt escape prefix, modifyOtherKeys state, and Kitty keyboard protocol + * flags from the terminal state. + * + * Note that the `macos_option_as_alt` option cannot be determined from + * terminal state and is reset to `GHOSTTY_OPTION_AS_ALT_FALSE` by this + * call. Use ghostty_key_encoder_setopt() to set it afterward if needed. + * + * @param encoder The encoder handle, must not be NULL + * @param terminal The terminal handle, must not be NULL + * + * @ingroup key + */ +void ghostty_key_encoder_setopt_from_terminal(GhosttyKeyEncoder encoder, GhosttyTerminal terminal); + /** * Encode a key event into a terminal escape sequence. * diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 7a79cb958..8c91920a0 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -23,6 +23,9 @@ extern "C" { * A terminal instance manages the full emulator state including the screen, * scrollback, cursor, styles, modes, and VT stream processing. * + * Once a terminal session is up and running, you can configure a key encoder + * to write keyboard input via ghostty_key_encoder_setopt_from_terminal(). + * * @{ */ diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 42ef1d8f5..0fa808b59 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -124,6 +124,7 @@ comptime { @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_setopt_from_terminal, .{ .name = "ghostty_key_encoder_setopt_from_terminal" }); @export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" }); @export(&c.osc_new, .{ .name = "ghostty_osc_new" }); @export(&c.osc_free, .{ .name = "ghostty_osc_free" }); diff --git a/src/terminal/c/key_encode.zig b/src/terminal/c/key_encode.zig index 063cd8df7..e83c9b221 100644 --- a/src/terminal/c/key_encode.zig +++ b/src/terminal/c/key_encode.zig @@ -8,6 +8,7 @@ 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; +const Terminal = @import("terminal.zig").Terminal; const log = std.log.scoped(.key_encode); @@ -115,6 +116,15 @@ fn setoptTyped( } } +pub fn setopt_from_terminal( + encoder_: Encoder, + terminal_: Terminal, +) callconv(.c) void { + const wrapper = encoder_ orelse return; + const t = terminal_ orelse return; + wrapper.opts = .fromTerminal(t); +} + pub fn encode( encoder_: Encoder, event_: KeyEvent, @@ -222,6 +232,64 @@ test "setopt macos option as alt" { try testing.expectEqual(OptionAsAlt.true, e.?.opts.macos_option_as_alt); } +test "setopt_from_terminal" { + const testing = std.testing; + const terminal_c = @import("terminal.zig"); + + // Create encoder + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Create terminal + var t: Terminal = undefined; + try testing.expectEqual(Result.success, terminal_c.new( + &lib_alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + )); + defer terminal_c.free(t); + + // Apply terminal state to encoder + setopt_from_terminal(e, t); + + // Options should reflect defaults from a fresh terminal + try testing.expect(!e.?.opts.cursor_key_application); + try testing.expect(!e.?.opts.alt_esc_prefix); + try testing.expectEqual(KittyFlags.disabled, e.?.opts.kitty_flags); + try testing.expectEqual(OptionAsAlt.false, e.?.opts.macos_option_as_alt); +} + +test "setopt_from_terminal null" { + // Both null should be no-ops + setopt_from_terminal(null, null); + + const testing = std.testing; + + // Encoder null with valid terminal + const terminal_c = @import("terminal.zig"); + var t: Terminal = undefined; + try testing.expectEqual(Result.success, terminal_c.new( + &lib_alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 0 }, + )); + defer terminal_c.free(t); + setopt_from_terminal(null, t); + + // Valid encoder with null terminal + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + setopt_from_terminal(e, null); +} + test "encode: kitty ctrl release with ctrl mod set" { const testing = std.testing; diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 4e95bb9d4..7842866dc 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -55,6 +55,7 @@ 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_setopt_from_terminal = key_encode.setopt_from_terminal; pub const key_encoder_encode = key_encode.encode; pub const paste_is_safe = paste.is_safe;