diff --git a/example/c-vt-mouse-encode/README.md b/example/c-vt-mouse-encode/README.md new file mode 100644 index 000000000..754e09805 --- /dev/null +++ b/example/c-vt-mouse-encode/README.md @@ -0,0 +1,23 @@ +# Example: `ghostty-vt` C Mouse Encoding + +This example demonstrates how to use the `ghostty-vt` C library to encode mouse +events into terminal escape sequences. + +This example specifically shows how to: + +1. Create a mouse encoder with the C API +2. Configure tracking mode and output format (this example uses SGR) +3. Set terminal geometry for pixel-to-cell coordinate mapping +4. Create and configure a mouse event +5. Encode the mouse event into a terminal escape sequence + +The example encodes a left button press at pixel position (50, 40) using SGR +format, producing an escape sequence like `\x1b[<0;6;3M`. + +## Usage + +Run the program: + +```shell-session +zig build run +``` diff --git a/example/c-vt-mouse-encode/build.zig b/example/c-vt-mouse-encode/build.zig new file mode 100644 index 000000000..350d0a89e --- /dev/null +++ b/example/c-vt-mouse-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_mouse_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-mouse-encode/build.zig.zon b/example/c-vt-mouse-encode/build.zig.zon new file mode 100644 index 000000000..1ab5da284 --- /dev/null +++ b/example/c-vt-mouse-encode/build.zig.zon @@ -0,0 +1,24 @@ +.{ + .name = .c_vt, + .version = "0.0.0", + .fingerprint = 0x413a8529a6dd3c51, + .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-mouse-encode/src/main.c b/example/c-vt-mouse-encode/src/main.c new file mode 100644 index 000000000..55f867252 --- /dev/null +++ b/example/c-vt-mouse-encode/src/main.c @@ -0,0 +1,78 @@ +#include +#include +#include +#include +#include + +int main() { + GhosttyMouseEncoder encoder; + GhosttyResult result = ghostty_mouse_encoder_new(NULL, &encoder); + assert(result == GHOSTTY_SUCCESS); + + // Set tracking mode to normal (button press/release) + ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_EVENT, + &(GhosttyMouseTrackingMode){GHOSTTY_MOUSE_TRACKING_NORMAL}); + + // Set output format to SGR + ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_FORMAT, + &(GhosttyMouseFormat){GHOSTTY_MOUSE_FORMAT_SGR}); + + // Set terminal geometry so the encoder can map pixel positions to cells + ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_SIZE, + &(GhosttyMouseEncoderSize){ + .size = sizeof(GhosttyMouseEncoderSize), + .screen_width = 800, + .screen_height = 600, + .cell_width = 10, + .cell_height = 20, + .padding_top = 0, + .padding_bottom = 0, + .padding_right = 0, + .padding_left = 0, + }); + + // Create mouse event: left button press at pixel position (50, 40) + GhosttyMouseEvent event; + result = ghostty_mouse_event_new(NULL, &event); + assert(result == GHOSTTY_SUCCESS); + ghostty_mouse_event_set_action(event, GHOSTTY_MOUSE_ACTION_PRESS); + ghostty_mouse_event_set_button(event, GHOSTTY_MOUSE_BUTTON_LEFT); + ghostty_mouse_event_set_position(event, (GhosttyMousePosition){.x = 50.0f, .y = 40.0f}); + printf("Encoding event: left button press at (50, 40) in SGR format\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 enough space, the function will return an out of memory + // error. + size_t required = 0; + result = ghostty_mouse_encoder_encode(encoder, event, NULL, 0, &required); + assert(result == GHOSTTY_OUT_OF_MEMORY); + printf("Required buffer size: %zu bytes\n", required); + + // Encode the mouse 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_mouse_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_mouse_event_free(event); + ghostty_mouse_encoder_free(encoder); + return 0; +} diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index dd5eda989..9d22b647b 100644 --- a/include/ghostty/vt.h +++ b/include/ghostty/vt.h @@ -31,6 +31,7 @@ * - @ref terminal "Terminal" - Complete terminal emulator state and rendering * - @ref formatter "Formatter" - Format terminal content as plain text, VT sequences, or HTML * - @ref key "Key Encoding" - Encode key events into terminal sequences + * - @ref mouse "Mouse Encoding" - Encode mouse events into terminal sequences * - @ref osc "OSC Parser" - Parse OSC (Operating System Command) sequences * - @ref sgr "SGR Parser" - Parse SGR (Select Graphic Rendition) sequences * - @ref paste "Paste Utilities" - Validate paste data safety @@ -42,6 +43,7 @@ * Complete working examples: * - @ref c-vt/src/main.c - OSC parser example * - @ref c-vt-key-encode/src/main.c - Key encoding example + * - @ref c-vt-mouse-encode/src/main.c - Mouse encoding example * - @ref c-vt-paste/src/main.c - Paste safety check example * - @ref c-vt-sgr/src/main.c - SGR parser example * - @ref c-vt-formatter/src/main.c - Terminal formatter example @@ -58,6 +60,11 @@ * into terminal escape sequences using the Kitty keyboard protocol. */ +/** @example c-vt-mouse-encode/src/main.c + * This example demonstrates how to use the mouse encoder to convert mouse events + * into terminal escape sequences using the SGR mouse format. + */ + /** @example c-vt-paste/src/main.c * This example demonstrates how to use the paste utilities to check if * paste data is safe before sending it to the terminal. @@ -88,6 +95,7 @@ extern "C" { #include #include #include +#include #include #include diff --git a/include/ghostty/vt/mouse.h b/include/ghostty/vt/mouse.h new file mode 100644 index 000000000..f5f37ce7b --- /dev/null +++ b/include/ghostty/vt/mouse.h @@ -0,0 +1,119 @@ +/** + * @file mouse.h + * + * Mouse encoding module - encode mouse events into terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_MOUSE_H +#define GHOSTTY_VT_MOUSE_H + +/** @defgroup mouse Mouse Encoding + * + * Utilities for encoding mouse events into terminal escape sequences, + * supporting X10, UTF-8, SGR, URxvt, and SGR-Pixels mouse protocols. + * + * ## Basic Usage + * + * 1. Create an encoder instance with ghostty_mouse_encoder_new(). + * 2. Configure encoder options with ghostty_mouse_encoder_setopt() or + * ghostty_mouse_encoder_setopt_from_terminal(). + * 3. For each mouse event: + * - Create a mouse event with ghostty_mouse_event_new(). + * - Set event properties (action, button, modifiers, position). + * - Encode with ghostty_mouse_encoder_encode(). + * - Free the event with ghostty_mouse_event_free() or reuse it. + * 4. Free the encoder with ghostty_mouse_encoder_free() when done. + * + * For a complete working example, see example/c-vt-mouse-encode in the + * repository. + * + * ## Example + * + * @code{.c} + * #include + * #include + * #include + * + * int main() { + * // Create encoder + * GhosttyMouseEncoder encoder; + * GhosttyResult result = ghostty_mouse_encoder_new(NULL, &encoder); + * assert(result == GHOSTTY_SUCCESS); + * + * // Configure SGR format with normal tracking + * ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_EVENT, + * &(GhosttyMouseTrackingMode){GHOSTTY_MOUSE_TRACKING_NORMAL}); + * ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_FORMAT, + * &(GhosttyMouseFormat){GHOSTTY_MOUSE_FORMAT_SGR}); + * + * // Set terminal geometry for coordinate mapping + * ghostty_mouse_encoder_setopt(encoder, GHOSTTY_MOUSE_ENCODER_OPT_SIZE, + * &(GhosttyMouseEncoderSize){ + * .size = sizeof(GhosttyMouseEncoderSize), + * .screen_width = 800, .screen_height = 600, + * .cell_width = 10, .cell_height = 20, + * }); + * + * // Create and configure a left button press event + * GhosttyMouseEvent event; + * result = ghostty_mouse_event_new(NULL, &event); + * assert(result == GHOSTTY_SUCCESS); + * ghostty_mouse_event_set_action(event, GHOSTTY_MOUSE_ACTION_PRESS); + * ghostty_mouse_event_set_button(event, GHOSTTY_MOUSE_BUTTON_LEFT); + * ghostty_mouse_event_set_position(event, + * (GhosttyMousePosition){.x = 50.0f, .y = 40.0f}); + * + * // Encode the mouse event + * char buf[128]; + * size_t written = 0; + * result = ghostty_mouse_encoder_encode(encoder, event, + * buf, sizeof(buf), &written); + * assert(result == GHOSTTY_SUCCESS); + * + * // Use the encoded sequence (e.g., write to terminal) + * fwrite(buf, 1, written, stdout); + * + * // Cleanup + * ghostty_mouse_event_free(event); + * ghostty_mouse_encoder_free(encoder); + * return 0; + * } + * @endcode + * + * ## Example: Encoding with Terminal State + * + * When you have a GhosttyTerminal, you can sync its tracking mode and + * output format into the encoder automatically: + * + * @code{.c} + * // Create a terminal and feed it some VT data that enables mouse tracking + * GhosttyTerminal terminal; + * ghostty_terminal_new(NULL, &terminal, + * (GhosttyTerminalOptions){.cols = 80, .rows = 24, .max_scrollback = 0}); + * + * // Application might write data that enables mouse reporting, etc. + * ghostty_terminal_vt_write(terminal, vt_data, vt_len); + * + * // Create an encoder and sync its options from the terminal + * GhosttyMouseEncoder encoder; + * ghostty_mouse_encoder_new(NULL, &encoder); + * ghostty_mouse_encoder_setopt_from_terminal(encoder, terminal); + * + * // Encode a mouse event using the terminal-derived options + * char buf[128]; + * size_t written = 0; + * ghostty_mouse_encoder_encode(encoder, event, buf, sizeof(buf), &written); + * + * ghostty_mouse_encoder_free(encoder); + * ghostty_terminal_free(terminal); + * @endcode + * + * @{ + */ + +#include +#include + +/** @} */ + +#endif /* GHOSTTY_VT_MOUSE_H */ diff --git a/include/ghostty/vt/mouse/encoder.h b/include/ghostty/vt/mouse/encoder.h new file mode 100644 index 000000000..c418ed8ec --- /dev/null +++ b/include/ghostty/vt/mouse/encoder.h @@ -0,0 +1,211 @@ +/** + * @file encoder.h + * + * Mouse event encoding to terminal escape sequences. + */ + +#ifndef GHOSTTY_VT_MOUSE_ENCODER_H +#define GHOSTTY_VT_MOUSE_ENCODER_H + +#include +#include +#include +#include +#include +#include +#include + +/** + * Opaque handle to a mouse encoder instance. + * + * This handle represents a mouse encoder that converts normalized + * mouse events into terminal escape sequences. + * + * @ingroup mouse + */ +typedef struct GhosttyMouseEncoder *GhosttyMouseEncoder; + +/** + * Mouse tracking mode. + * + * @ingroup mouse + */ +typedef enum { + /** Mouse reporting disabled. */ + GHOSTTY_MOUSE_TRACKING_NONE = 0, + + /** X10 mouse mode. */ + GHOSTTY_MOUSE_TRACKING_X10 = 1, + + /** Normal mouse mode (button press/release only). */ + GHOSTTY_MOUSE_TRACKING_NORMAL = 2, + + /** Button-event tracking mode. */ + GHOSTTY_MOUSE_TRACKING_BUTTON = 3, + + /** Any-event tracking mode. */ + GHOSTTY_MOUSE_TRACKING_ANY = 4, +} GhosttyMouseTrackingMode; + +/** + * Mouse output format. + * + * @ingroup mouse + */ +typedef enum { + GHOSTTY_MOUSE_FORMAT_X10 = 0, + GHOSTTY_MOUSE_FORMAT_UTF8 = 1, + GHOSTTY_MOUSE_FORMAT_SGR = 2, + GHOSTTY_MOUSE_FORMAT_URXVT = 3, + GHOSTTY_MOUSE_FORMAT_SGR_PIXELS = 4, +} GhosttyMouseFormat; + +/** + * Mouse encoder size and geometry context. + * + * This describes the rendered terminal geometry used to convert + * surface-space positions into encoded coordinates. + * + * @ingroup mouse + */ +typedef struct { + /** Size of this struct in bytes. Must be set to sizeof(GhosttyMouseEncoderSize). */ + size_t size; + + /** Full screen width in pixels. */ + uint32_t screen_width; + + /** Full screen height in pixels. */ + uint32_t screen_height; + + /** Cell width in pixels. Must be non-zero. */ + uint32_t cell_width; + + /** Cell height in pixels. Must be non-zero. */ + uint32_t cell_height; + + /** Top padding in pixels. */ + uint32_t padding_top; + + /** Bottom padding in pixels. */ + uint32_t padding_bottom; + + /** Right padding in pixels. */ + uint32_t padding_right; + + /** Left padding in pixels. */ + uint32_t padding_left; +} GhosttyMouseEncoderSize; + +/** + * Mouse encoder option identifiers. + * + * These values are used with ghostty_mouse_encoder_setopt() to configure + * the behavior of the mouse encoder. + * + * @ingroup mouse + */ +typedef enum { + /** Mouse tracking mode (value: GhosttyMouseTrackingMode). */ + GHOSTTY_MOUSE_ENCODER_OPT_EVENT = 0, + + /** Mouse output format (value: GhosttyMouseFormat). */ + GHOSTTY_MOUSE_ENCODER_OPT_FORMAT = 1, + + /** Renderer size context (value: GhosttyMouseEncoderSize). */ + GHOSTTY_MOUSE_ENCODER_OPT_SIZE = 2, + + /** Whether any mouse button is currently pressed (value: bool). */ + GHOSTTY_MOUSE_ENCODER_OPT_ANY_BUTTON_PRESSED = 3, + + /** Whether to enable motion deduplication by last cell (value: bool). */ + GHOSTTY_MOUSE_ENCODER_OPT_TRACK_LAST_CELL = 4, +} GhosttyMouseEncoderOption; + +/** + * Create a new mouse encoder instance. + * + * @param allocator Pointer to allocator, 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 mouse + */ +GhosttyResult ghostty_mouse_encoder_new(const GhosttyAllocator *allocator, + GhosttyMouseEncoder *encoder); + +/** + * Free a mouse encoder instance. + * + * @param encoder The encoder handle to free (may be NULL) + * + * @ingroup mouse + */ +void ghostty_mouse_encoder_free(GhosttyMouseEncoder encoder); + +/** + * Set an option on the mouse encoder. + * + * A null pointer value does nothing. It does not reset to defaults. + * + * @param encoder The encoder handle, must not be NULL + * @param option The option to set + * @param value Pointer to option value (type depends on option) + * + * @ingroup mouse + */ +void ghostty_mouse_encoder_setopt(GhosttyMouseEncoder encoder, + GhosttyMouseEncoderOption option, + const void *value); + +/** + * Set encoder options from a terminal's current state. + * + * This sets tracking mode and output format from terminal state. + * It does not modify size or any-button state. + * + * @param encoder The encoder handle, must not be NULL + * @param terminal The terminal handle, must not be NULL + * + * @ingroup mouse + */ +void ghostty_mouse_encoder_setopt_from_terminal(GhosttyMouseEncoder encoder, + GhosttyTerminal terminal); + +/** + * Reset internal encoder state. + * + * This clears motion deduplication state (last tracked cell). + * + * @param encoder The encoder handle (may be NULL) + * + * @ingroup mouse + */ +void ghostty_mouse_encoder_reset(GhosttyMouseEncoder encoder); + +/** + * Encode a mouse event into a terminal escape sequence. + * + * Not all mouse events produce output. In such cases this returns + * GHOSTTY_SUCCESS with out_len set to 0. + * + * If the output buffer is too small, this returns GHOSTTY_OUT_OF_MEMORY + * and out_len contains the required size. + * + * @param encoder The encoder handle, must not be NULL + * @param event The mouse event to encode, must not be NULL + * @param out_buf Buffer to write encoded bytes to, or NULL to query required size + * @param out_buf_size Size of out_buf in bytes + * @param out_len Pointer to store bytes written (or required bytes on failure) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_OUT_OF_MEMORY if buffer is too small, + * or another error code + * + * @ingroup mouse + */ +GhosttyResult ghostty_mouse_encoder_encode(GhosttyMouseEncoder encoder, + GhosttyMouseEvent event, + char *out_buf, + size_t out_buf_size, + size_t *out_len); + +#endif /* GHOSTTY_VT_MOUSE_ENCODER_H */ diff --git a/include/ghostty/vt/mouse/event.h b/include/ghostty/vt/mouse/event.h new file mode 100644 index 000000000..44132097c --- /dev/null +++ b/include/ghostty/vt/mouse/event.h @@ -0,0 +1,193 @@ +/** + * @file event.h + * + * Mouse event representation and manipulation. + */ + +#ifndef GHOSTTY_VT_MOUSE_EVENT_H +#define GHOSTTY_VT_MOUSE_EVENT_H + +#include +#include +#include +#include + +/** + * Opaque handle to a mouse event. + * + * This handle represents a normalized mouse input event containing + * action, button, modifiers, and surface-space position. + * + * @ingroup mouse + */ +typedef struct GhosttyMouseEvent *GhosttyMouseEvent; + +/** + * Mouse event action type. + * + * @ingroup mouse + */ +typedef enum { + /** Mouse button was pressed. */ + GHOSTTY_MOUSE_ACTION_PRESS = 0, + + /** Mouse button was released. */ + GHOSTTY_MOUSE_ACTION_RELEASE = 1, + + /** Mouse moved. */ + GHOSTTY_MOUSE_ACTION_MOTION = 2, +} GhosttyMouseAction; + +/** + * Mouse button identity. + * + * @ingroup mouse + */ +typedef enum { + GHOSTTY_MOUSE_BUTTON_UNKNOWN = 0, + GHOSTTY_MOUSE_BUTTON_LEFT = 1, + GHOSTTY_MOUSE_BUTTON_RIGHT = 2, + GHOSTTY_MOUSE_BUTTON_MIDDLE = 3, + GHOSTTY_MOUSE_BUTTON_FOUR = 4, + GHOSTTY_MOUSE_BUTTON_FIVE = 5, + GHOSTTY_MOUSE_BUTTON_SIX = 6, + GHOSTTY_MOUSE_BUTTON_SEVEN = 7, + GHOSTTY_MOUSE_BUTTON_EIGHT = 8, + GHOSTTY_MOUSE_BUTTON_NINE = 9, + GHOSTTY_MOUSE_BUTTON_TEN = 10, + GHOSTTY_MOUSE_BUTTON_ELEVEN = 11, +} GhosttyMouseButton; + +/** + * Mouse position in surface-space pixels. + * + * @ingroup mouse + */ +typedef struct { + float x; + float y; +} GhosttyMousePosition; + +/** + * Create a new mouse event instance. + * + * @param allocator Pointer to allocator, or NULL to use the default allocator + * @param event Pointer to store the created event handle + * @return GHOSTTY_SUCCESS on success, or an error code on failure + * + * @ingroup mouse + */ +GhosttyResult ghostty_mouse_event_new(const GhosttyAllocator *allocator, + GhosttyMouseEvent *event); + +/** + * Free a mouse event instance. + * + * @param event The mouse event handle to free (may be NULL) + * + * @ingroup mouse + */ +void ghostty_mouse_event_free(GhosttyMouseEvent event); + +/** + * Set the event action. + * + * @param event The event handle, must not be NULL + * @param action The action to set + * + * @ingroup mouse + */ +void ghostty_mouse_event_set_action(GhosttyMouseEvent event, + GhosttyMouseAction action); + +/** + * Get the event action. + * + * @param event The event handle, must not be NULL + * @return The event action + * + * @ingroup mouse + */ +GhosttyMouseAction ghostty_mouse_event_get_action(GhosttyMouseEvent event); + +/** + * Set the event button. + * + * This sets a concrete button identity for the event. + * To represent "no button" (for motion events), use + * ghostty_mouse_event_clear_button(). + * + * @param event The event handle, must not be NULL + * @param button The button to set + * + * @ingroup mouse + */ +void ghostty_mouse_event_set_button(GhosttyMouseEvent event, + GhosttyMouseButton button); + +/** + * Clear the event button. + * + * This sets the event button to "none". + * + * @param event The event handle, must not be NULL + * + * @ingroup mouse + */ +void ghostty_mouse_event_clear_button(GhosttyMouseEvent event); + +/** + * Get the event button. + * + * @param event The event handle, must not be NULL + * @param out_button Output pointer for the button value (may be NULL) + * @return true if a button is set, false if no button is set + * + * @ingroup mouse + */ +bool ghostty_mouse_event_get_button(GhosttyMouseEvent event, + GhosttyMouseButton *out_button); + +/** + * Set keyboard modifiers held during the event. + * + * @param event The event handle, must not be NULL + * @param mods Modifier bitmask + * + * @ingroup mouse + */ +void ghostty_mouse_event_set_mods(GhosttyMouseEvent event, + GhosttyMods mods); + +/** + * Get keyboard modifiers held during the event. + * + * @param event The event handle, must not be NULL + * @return Modifier bitmask + * + * @ingroup mouse + */ +GhosttyMods ghostty_mouse_event_get_mods(GhosttyMouseEvent event); + +/** + * Set the event position in surface-space pixels. + * + * @param event The event handle, must not be NULL + * @param position The position to set + * + * @ingroup mouse + */ +void ghostty_mouse_event_set_position(GhosttyMouseEvent event, + GhosttyMousePosition position); + +/** + * Get the event position in surface-space pixels. + * + * @param event The event handle, must not be NULL + * @return The current event position + * + * @ingroup mouse + */ +GhosttyMousePosition ghostty_mouse_event_get_position(GhosttyMouseEvent event); + +#endif /* GHOSTTY_VT_MOUSE_EVENT_H */ diff --git a/src/input/mouse.zig b/src/input/mouse.zig index 8a769557f..fad3ed67a 100644 --- a/src/input/mouse.zig +++ b/src/input/mouse.zig @@ -4,7 +4,7 @@ const std = @import("std"); /// from ButtonState because button state is simply the current state /// of a mouse button but an action is something that triggers via /// an GUI event and supports more. -pub const Action = enum { press, release, motion }; +pub const Action = enum(c_int) { press, release, motion }; /// The state of a mouse button. /// diff --git a/src/input/mouse_encode.zig b/src/input/mouse_encode.zig index 2dfe084fb..13a8bd462 100644 --- a/src/input/mouse_encode.zig +++ b/src/input/mouse_encode.zig @@ -1,6 +1,7 @@ const std = @import("std"); const testing = std.testing; -const Terminal = @import("../terminal/Terminal.zig"); +const terminal = @import("../terminal/main.zig"); +const Terminal = terminal.Terminal; const renderer_size = @import("../renderer/size.zig"); const point = @import("../terminal/point.zig"); const key = @import("key.zig"); @@ -11,10 +12,10 @@ const log = std.log.scoped(.mouse_encode); /// Options that affect mouse encoding behavior and provide runtime context. pub const Options = struct { /// Terminal mouse reporting mode (X10, normal, button, any). - event: Terminal.MouseEvent = .none, + event: terminal.MouseEvent = .none, /// Terminal mouse reporting format. - format: Terminal.MouseFormat = .x10, + format: terminal.MouseFormat = .x10, /// Full renderer size used to convert surface-space pixel positions /// into grid cell coordinates (for most formats) and terminal-space @@ -68,7 +69,7 @@ pub const Event = struct { pos: Pos = .{}, /// Mouse position in surface-space pixels. - pub const Pos = struct { + pub const Pos = extern struct { x: f32 = 0, y: f32 = 0, }; @@ -92,7 +93,7 @@ pub fn encode( // If we don't have a motion-tracking event mode, do nothing, // because events outside the viewport are never reported in // such cases. - if (!opts.event.motion()) return; + if (!terminal.mouse.eventSendsMotion(opts.event)) return; // For motion modes, we only report if a button is currently pressed. // This lets a TUI detect a click over the surface + drag out diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 0fa808b59..44ffe7a3e 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -84,6 +84,7 @@ pub const input = struct { const paste = @import("input/paste.zig"); const key = @import("input/key.zig"); const key_encode = @import("input/key_encode.zig"); + const mouse_encode = @import("input/mouse_encode.zig"); // Paste-related APIs pub const PasteError = paste.Error; @@ -98,6 +99,13 @@ pub const input = struct { pub const KeyMods = key.Mods; pub const KeyEncodeOptions = key_encode.Options; pub const encodeKey = key_encode.encode; + + // Mouse encoding + pub const MouseAction = @import("input/mouse.zig").Action; + pub const MouseButton = @import("input/mouse.zig").Button; + pub const MouseEncodeOptions = mouse_encode.Options; + pub const MouseEncodeEvent = mouse_encode.Event; + pub const encodeMouse = mouse_encode.encode; }; comptime { @@ -126,6 +134,23 @@ comptime { @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.mouse_event_new, .{ .name = "ghostty_mouse_event_new" }); + @export(&c.mouse_event_free, .{ .name = "ghostty_mouse_event_free" }); + @export(&c.mouse_event_set_action, .{ .name = "ghostty_mouse_event_set_action" }); + @export(&c.mouse_event_get_action, .{ .name = "ghostty_mouse_event_get_action" }); + @export(&c.mouse_event_set_button, .{ .name = "ghostty_mouse_event_set_button" }); + @export(&c.mouse_event_clear_button, .{ .name = "ghostty_mouse_event_clear_button" }); + @export(&c.mouse_event_get_button, .{ .name = "ghostty_mouse_event_get_button" }); + @export(&c.mouse_event_set_mods, .{ .name = "ghostty_mouse_event_set_mods" }); + @export(&c.mouse_event_get_mods, .{ .name = "ghostty_mouse_event_get_mods" }); + @export(&c.mouse_event_set_position, .{ .name = "ghostty_mouse_event_set_position" }); + @export(&c.mouse_event_get_position, .{ .name = "ghostty_mouse_event_get_position" }); + @export(&c.mouse_encoder_new, .{ .name = "ghostty_mouse_encoder_new" }); + @export(&c.mouse_encoder_free, .{ .name = "ghostty_mouse_encoder_free" }); + @export(&c.mouse_encoder_setopt, .{ .name = "ghostty_mouse_encoder_setopt" }); + @export(&c.mouse_encoder_setopt_from_terminal, .{ .name = "ghostty_mouse_encoder_setopt_from_terminal" }); + @export(&c.mouse_encoder_reset, .{ .name = "ghostty_mouse_encoder_reset" }); + @export(&c.mouse_encoder_encode, .{ .name = "ghostty_mouse_encoder_encode" }); @export(&c.osc_new, .{ .name = "ghostty_osc_new" }); @export(&c.osc_free, .{ .name = "ghostty_osc_free" }); @export(&c.osc_next, .{ .name = "ghostty_osc_next" }); diff --git a/src/renderer/size.zig b/src/renderer/size.zig index e9754d1f3..565e95e2c 100644 --- a/src/renderer/size.zig +++ b/src/renderer/size.zig @@ -172,7 +172,7 @@ pub const Coordinate = union(enum) { /// /// The units for the width and height are in world space. They have to /// be normalized for any renderer implementation. -pub const CellSize = struct { +pub const CellSize = extern struct { width: u32, height: u32, }; @@ -180,7 +180,7 @@ pub const CellSize = struct { /// The dimensions of the screen that the grid is rendered to. This is the /// terminal screen, so it is likely a subset of the window size. The dimensions /// should be in pixels. -pub const ScreenSize = struct { +pub const ScreenSize = extern struct { width: u32, height: u32, @@ -224,7 +224,7 @@ pub const ScreenSize = struct { }; /// The dimensions of the grid itself, in rows/columns units. -pub const GridSize = struct { +pub const GridSize = extern struct { pub const Unit = terminal.size.CellCountInt; columns: Unit = 0, @@ -257,7 +257,7 @@ pub const GridSize = struct { }; /// The padding to add to a screen. -pub const Padding = struct { +pub const Padding = extern struct { top: u32 = 0, bottom: u32 = 0, right: u32 = 0, diff --git a/src/surface_mouse.zig b/src/surface_mouse.zig index 6d3c11394..8fa53d240 100644 --- a/src/surface_mouse.zig +++ b/src/surface_mouse.zig @@ -9,14 +9,14 @@ const std = @import("std"); const builtin = @import("builtin"); const input = @import("input.zig"); const terminal = @import("terminal/main.zig"); -const MouseShape = @import("terminal/mouse_shape.zig").MouseShape; +const MouseShape = terminal.MouseShape; /// For processing key events; the key that was physically pressed on the /// keyboard. physical_key: input.Key, /// The mouse event tracking mode, if any. -mouse_event: terminal.Terminal.MouseEvent, +mouse_event: terminal.MouseEvent, /// The current terminal's mouse shape. mouse_shape: MouseShape, diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 9e21ba97a..636a8f2ee 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -23,7 +23,7 @@ const point = @import("point.zig"); const sgr = @import("sgr.zig"); const Tabstops = @import("Tabstops.zig"); const color = @import("color.zig"); -const mouse_shape_pkg = @import("mouse_shape.zig"); +const mouse = @import("mouse.zig"); const ReadonlyHandler = @import("stream_readonly.zig").Handler; const ReadonlyStream = @import("stream_readonly.zig").Stream; @@ -79,7 +79,7 @@ previous_char: ?u21 = null, modes: modespkg.ModeState = .{}, /// The most recently set mouse shape for the terminal. -mouse_shape: mouse_shape_pkg.MouseShape = .text, +mouse_shape: mouse.Shape = .text, /// These are just a packed set of flags we may set on the terminal. flags: packed struct { @@ -96,8 +96,8 @@ flags: packed struct { /// set mode in modes. You can't get the right event/format to use /// based on modes alone because modes don't show you what order /// this was called so we have to track it separately. - mouse_event: MouseEvent = .none, - mouse_format: MouseFormat = .x10, + mouse_event: mouse.Event = .none, + mouse_format: mouse.Format = .x10, /// Set via the XTSHIFTESCAPE sequence. If true (XTSHIFTESCAPE = 1) /// then we want to capture the shift key for the mouse protocol @@ -167,31 +167,6 @@ pub const Dirty = packed struct { preedit: bool = false, }; -/// The event types that can be reported for mouse-related activities. -/// These are all mutually exclusive (hence in a single enum). -pub const MouseEvent = enum(u3) { - none = 0, - x10 = 1, // 9 - normal = 2, // 1000 - button = 3, // 1002 - any = 4, // 1003 - - /// Returns true if this event sends motion events. - pub fn motion(self: MouseEvent) bool { - return self == .button or self == .any; - } -}; - -/// The format of mouse events when enabled. -/// These are all mutually exclusive (hence in a single enum). -pub const MouseFormat = enum(u3) { - x10 = 0, - utf8 = 1, // 1005 - sgr = 2, // 1006 - urxvt = 3, // 1015 - sgr_pixels = 4, // 1016 -}; - /// Scrolling region is the area of the screen designated where scrolling /// occurs. When scrolling the screen, only this viewport is scrolled. pub const ScrollingRegion = struct { diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 7842866dc..5adc00251 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -3,6 +3,8 @@ pub const formatter = @import("formatter.zig"); pub const osc = @import("osc.zig"); pub const key_event = @import("key_event.zig"); pub const key_encode = @import("key_encode.zig"); +pub const mouse_event = @import("mouse_event.zig"); +pub const mouse_encode = @import("mouse_encode.zig"); pub const paste = @import("paste.zig"); pub const sgr = @import("sgr.zig"); pub const terminal = @import("terminal.zig"); @@ -58,6 +60,25 @@ 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 mouse_event_new = mouse_event.new; +pub const mouse_event_free = mouse_event.free; +pub const mouse_event_set_action = mouse_event.set_action; +pub const mouse_event_get_action = mouse_event.get_action; +pub const mouse_event_set_button = mouse_event.set_button; +pub const mouse_event_clear_button = mouse_event.clear_button; +pub const mouse_event_get_button = mouse_event.get_button; +pub const mouse_event_set_mods = mouse_event.set_mods; +pub const mouse_event_get_mods = mouse_event.get_mods; +pub const mouse_event_set_position = mouse_event.set_position; +pub const mouse_event_get_position = mouse_event.get_position; + +pub const mouse_encoder_new = mouse_encode.new; +pub const mouse_encoder_free = mouse_encode.free; +pub const mouse_encoder_setopt = mouse_encode.setopt; +pub const mouse_encoder_setopt_from_terminal = mouse_encode.setopt_from_terminal; +pub const mouse_encoder_reset = mouse_encode.reset; +pub const mouse_encoder_encode = mouse_encode.encode; + pub const paste_is_safe = paste.is_safe; pub const terminal_new = terminal.new; @@ -73,6 +94,8 @@ test { _ = osc; _ = key_event; _ = key_encode; + _ = mouse_event; + _ = mouse_encode; _ = paste; _ = sgr; _ = terminal; diff --git a/src/terminal/c/mouse_encode.zig b/src/terminal/c/mouse_encode.zig new file mode 100644 index 000000000..1a19b0511 --- /dev/null +++ b/src/terminal/c/mouse_encode.zig @@ -0,0 +1,529 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const testing = std.testing; +const lib_alloc = @import("../../lib/allocator.zig"); +const CAllocator = lib_alloc.Allocator; +const input_mouse_encode = @import("../../input/mouse_encode.zig"); +const renderer_size = @import("../../renderer/size.zig"); +const point = @import("../point.zig"); +const terminal_mouse = @import("../mouse.zig"); +const mouse_event = @import("mouse_event.zig"); +const Result = @import("result.zig").Result; +const Event = mouse_event.Event; +const Terminal = @import("terminal.zig").Terminal; + +const log = std.log.scoped(.mouse_encode); + +/// Wrapper around mouse encoding options that tracks the allocator for C API usage. +const MouseEncoderWrapper = struct { + opts: input_mouse_encode.Options, + track_last_cell: bool = false, + last_cell: ?point.Coordinate = null, + alloc: Allocator, +}; + +/// C: GhosttyMouseEncoder +pub const Encoder = ?*MouseEncoderWrapper; + +/// C: GhosttyMouseTrackingMode +pub const TrackingMode = terminal_mouse.Event; + +/// C: GhosttyMouseFormat +pub const Format = terminal_mouse.Format; + +/// C: GhosttyMouseEncoderSize +pub const Size = extern struct { + size: usize = @sizeOf(Size), + screen_width: u32, + screen_height: u32, + cell_width: u32, + cell_height: u32, + padding_top: u32, + padding_bottom: u32, + padding_right: u32, + padding_left: u32, + + fn toRenderer(self: Size) ?renderer_size.Size { + if (self.cell_width == 0 or self.cell_height == 0) return null; + return .{ + .screen = .{ + .width = self.screen_width, + .height = self.screen_height, + }, + .cell = .{ + .width = self.cell_width, + .height = self.cell_height, + }, + .padding = .{ + .top = self.padding_top, + .bottom = self.padding_bottom, + .right = self.padding_right, + .left = self.padding_left, + }, + }; + } +}; + +/// C: GhosttyMouseEncoderOption +pub const Option = enum(c_int) { + event = 0, + format = 1, + size = 2, + any_button_pressed = 3, + track_last_cell = 4, + + /// Input type expected for setting the option. + pub fn InType(comptime self: Option) type { + return switch (self) { + .event => TrackingMode, + .format => Format, + .size => Size, + .any_button_pressed, + .track_last_cell, + => bool, + }; + } +}; + +pub fn new( + alloc_: ?*const CAllocator, + result: *Encoder, +) callconv(.c) Result { + const alloc = lib_alloc.default(alloc_); + const ptr = alloc.create(MouseEncoderWrapper) catch + return .out_of_memory; + ptr.* = .{ + .opts = .{ .size = defaultSize() }, + .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); +} + +pub fn setopt( + encoder_: Encoder, + 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_, + comptime_option, + @ptrCast(@alignCast(value orelse return)), + ), + }; +} + +fn setoptTyped( + encoder_: Encoder, + comptime option: Option, + value: *const option.InType(), +) void { + const wrapper = encoder_.?; + switch (option) { + .event => { + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(TrackingMode, @intFromEnum(value.*)) catch { + log.warn("setopt invalid TrackingMode value={d}", .{@intFromEnum(value.*)}); + return; + }; + } + + if (wrapper.opts.event != value.*) wrapper.last_cell = null; + wrapper.opts.event = value.*; + }, + + .format => { + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(Format, @intFromEnum(value.*)) catch { + log.warn("setopt invalid Format value={d}", .{@intFromEnum(value.*)}); + return; + }; + } + + if (wrapper.opts.format != value.*) wrapper.last_cell = null; + wrapper.opts.format = value.*; + }, + + .size => { + if (value.size < @sizeOf(Size)) { + log.warn("setopt size struct too small size={d} expected>={d}", .{ + value.size, + @sizeOf(Size), + }); + return; + } + + wrapper.opts.size = value.toRenderer() orelse { + log.warn("setopt invalid size values (cell width and height must be non-zero)", .{}); + return; + }; + wrapper.last_cell = null; + }, + + .any_button_pressed => wrapper.opts.any_button_pressed = value.*, + + .track_last_cell => { + wrapper.track_last_cell = value.*; + if (!value.*) wrapper.last_cell = null; + }, + } +} + +pub fn setopt_from_terminal( + encoder_: Encoder, + terminal_: Terminal, +) callconv(.c) void { + const wrapper = encoder_ orelse return; + const t = terminal_ orelse return; + wrapper.opts.event = t.flags.mouse_event; + wrapper.opts.format = t.flags.mouse_format; + wrapper.last_cell = null; +} + +pub fn reset(encoder_: Encoder) callconv(.c) void { + const wrapper = encoder_ orelse return; + wrapper.last_cell = null; +} + +pub fn encode( + encoder_: Encoder, + event_: Event, + out_: ?[*]u8, + out_len: usize, + out_written: *usize, +) callconv(.c) Result { + const wrapper = encoder_ orelse return .invalid_value; + const event = event_ orelse return .invalid_value; + + const prev_last_cell = wrapper.last_cell; + + var opts = wrapper.opts; + opts.last_cell = if (wrapper.track_last_cell) &wrapper.last_cell else null; + + var writer: std.Io.Writer = .fixed(if (out_) |out| out[0..out_len] else &.{}); + input_mouse_encode.encode( + &writer, + event.event, + opts, + ) catch |err| switch (err) { + error.WriteFailed => { + // Failed writes should not mutate motion dedupe state because no + // complete sequence was produced. + wrapper.last_cell = prev_last_cell; + + // Use a discarding writer to count how much space we would have needed. + var count_last_cell = prev_last_cell; + var count_opts = wrapper.opts; + count_opts.last_cell = if (wrapper.track_last_cell) &count_last_cell else null; + + var discarding: std.Io.Writer.Discarding = .init(&.{}); + input_mouse_encode.encode( + &discarding.writer, + event.event, + count_opts, + ) catch unreachable; + + // Discarding always uses a u64. If we're on 32-bit systems + // we cast down. We should make this safer in the future. + out_written.* = @intCast(discarding.count); + return .out_of_memory; + }, + }; + + out_written.* = writer.end; + return .success; +} + +fn defaultSize() renderer_size.Size { + return .{ + .screen = .{ .width = 1, .height = 1 }, + .cell = .{ .width = 1, .height = 1 }, + .padding = .{}, + }; +} + +fn testSize() Size { + return .{ + .size = @sizeOf(Size), + .screen_width = 1_000, + .screen_height = 1_000, + .cell_width = 1, + .cell_height = 1, + .padding_top = 0, + .padding_bottom = 0, + .padding_right = 0, + .padding_left = 0, + }; +} + +test "alloc" { + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + free(e); +} + +test "setopt" { + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + const event_mode: TrackingMode = .any; + setopt(e, .event, &event_mode); + try testing.expectEqual(TrackingMode.any, e.?.opts.event); + + const format_mode: Format = .sgr; + setopt(e, .format, &format_mode); + try testing.expectEqual(Format.sgr, e.?.opts.format); + + const size = testSize(); + setopt(e, .size, &size); + try testing.expectEqual(size.screen_width, e.?.opts.size.screen.width); + try testing.expectEqual(size.screen_height, e.?.opts.size.screen.height); + + const any_button_pressed = true; + setopt(e, .any_button_pressed, &any_button_pressed); + try testing.expect(e.?.opts.any_button_pressed); + + const track_last_cell = true; + setopt(e, .track_last_cell, &track_last_cell); + try testing.expect(e.?.track_last_cell); +} + +test "setopt_from_terminal" { + const terminal_c = @import("terminal.zig"); + + var e: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + 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); + + const event_mode: TrackingMode = .any; + setopt(e, .event, &event_mode); + const format_mode: Format = .sgr; + setopt(e, .format, &format_mode); + + setopt_from_terminal(e, t); + try testing.expectEqual(TrackingMode.none, e.?.opts.event); + try testing.expectEqual(Format.x10, e.?.opts.format); +} + +test "setopt_from_terminal null" { + setopt_from_terminal(null, null); + + 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); + + 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: sgr press left" { + var encoder: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &encoder, + )); + defer free(encoder); + + const event_mode: TrackingMode = .any; + setopt(encoder, .event, &event_mode); + const format_mode: Format = .sgr; + setopt(encoder, .format, &format_mode); + const size = testSize(); + setopt(encoder, .size, &size); + + var event: Event = undefined; + try testing.expectEqual(Result.success, mouse_event.new( + &lib_alloc.test_allocator, + &event, + )); + defer mouse_event.free(event); + + mouse_event.set_action(event, .press); + mouse_event.set_button(event, .left); + mouse_event.set_position(event, .{ .x = 0, .y = 0 }); + + var required: usize = 0; + try testing.expectEqual(Result.out_of_memory, encode( + encoder, + event, + null, + 0, + &required, + )); + try testing.expectEqual(@as(usize, 9), required); + + var buf: [32]u8 = undefined; + var written: usize = 0; + try testing.expectEqual(Result.success, encode( + encoder, + event, + &buf, + buf.len, + &written, + )); + try testing.expectEqual(required, written); + try testing.expectEqualStrings("\x1b[<0;1;1M", buf[0..written]); +} + +test "encode: motion dedupe and reset" { + var encoder: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &encoder, + )); + defer free(encoder); + + const event_mode: TrackingMode = .any; + setopt(encoder, .event, &event_mode); + const format_mode: Format = .sgr; + setopt(encoder, .format, &format_mode); + const size = testSize(); + setopt(encoder, .size, &size); + const track_last_cell = true; + setopt(encoder, .track_last_cell, &track_last_cell); + + var event: Event = undefined; + try testing.expectEqual(Result.success, mouse_event.new( + &lib_alloc.test_allocator, + &event, + )); + defer mouse_event.free(event); + + mouse_event.set_action(event, .motion); + mouse_event.set_button(event, .left); + mouse_event.set_position(event, .{ .x = 5, .y = 6 }); + + { + var buf: [32]u8 = undefined; + var written: usize = 0; + try testing.expectEqual(Result.success, encode( + encoder, + event, + &buf, + buf.len, + &written, + )); + try testing.expect(written > 0); + } + + { + var buf: [32]u8 = undefined; + var written: usize = 0; + try testing.expectEqual(Result.success, encode( + encoder, + event, + &buf, + buf.len, + &written, + )); + try testing.expectEqual(@as(usize, 0), written); + } + + reset(encoder); + + { + var buf: [32]u8 = undefined; + var written: usize = 0; + try testing.expectEqual(Result.success, encode( + encoder, + event, + &buf, + buf.len, + &written, + )); + try testing.expect(written > 0); + } +} + +test "encode: querying required size doesn't update dedupe state" { + var encoder: Encoder = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &encoder, + )); + defer free(encoder); + + const event_mode: TrackingMode = .any; + setopt(encoder, .event, &event_mode); + const format_mode: Format = .sgr; + setopt(encoder, .format, &format_mode); + const size = testSize(); + setopt(encoder, .size, &size); + const track_last_cell = true; + setopt(encoder, .track_last_cell, &track_last_cell); + + var event: Event = undefined; + try testing.expectEqual(Result.success, mouse_event.new( + &lib_alloc.test_allocator, + &event, + )); + defer mouse_event.free(event); + + mouse_event.set_action(event, .motion); + mouse_event.set_button(event, .left); + mouse_event.set_position(event, .{ .x = 5, .y = 6 }); + + var required: usize = 0; + try testing.expectEqual(Result.out_of_memory, encode( + encoder, + event, + null, + 0, + &required, + )); + try testing.expect(required > 0); + + var buf: [32]u8 = undefined; + var written: usize = 0; + try testing.expectEqual(Result.success, encode( + encoder, + event, + &buf, + buf.len, + &written, + )); + try testing.expect(written > 0); +} diff --git a/src/terminal/c/mouse_event.zig b/src/terminal/c/mouse_event.zig new file mode 100644 index 000000000..91f4e2126 --- /dev/null +++ b/src/terminal/c/mouse_event.zig @@ -0,0 +1,155 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const testing = std.testing; +const lib_alloc = @import("../../lib/allocator.zig"); +const CAllocator = lib_alloc.Allocator; +const key = @import("../../input/key.zig"); +const mouse = @import("../../input/mouse.zig"); +const mouse_encode = @import("../../input/mouse_encode.zig"); +const Result = @import("result.zig").Result; + +const log = std.log.scoped(.mouse_event); + +/// Wrapper around mouse event that tracks the allocator for C API usage. +const MouseEventWrapper = struct { + event: mouse_encode.Event = .{}, + alloc: Allocator, +}; + +/// C: GhosttyMouseEvent +pub const Event = ?*MouseEventWrapper; + +/// C: GhosttyMouseAction +pub const Action = mouse.Action; + +/// C: GhosttyMouseButton +pub const Button = mouse.Button; + +/// C: GhosttyMousePosition +pub const Position = mouse_encode.Event.Pos; + +/// C: GhosttyMods +pub const Mods = key.Mods; + +pub fn new( + alloc_: ?*const CAllocator, + result: *Event, +) callconv(.c) Result { + const alloc = lib_alloc.default(alloc_); + const ptr = alloc.create(MouseEventWrapper) 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: Action) callconv(.c) void { + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(Action, @intFromEnum(action)) catch { + log.warn("set_action invalid action value={d}", .{@intFromEnum(action)}); + return; + }; + } + + event_.?.event.action = action; +} + +pub fn get_action(event_: Event) callconv(.c) Action { + return event_.?.event.action; +} + +pub fn set_button(event_: Event, button: Button) callconv(.c) void { + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(Button, @intFromEnum(button)) catch { + log.warn("set_button invalid button value={d}", .{@intFromEnum(button)}); + return; + }; + } + + event_.?.event.button = button; +} + +pub fn clear_button(event_: Event) callconv(.c) void { + event_.?.event.button = null; +} + +pub fn get_button(event_: Event, out: ?*Button) callconv(.c) bool { + if (event_.?.event.button) |button| { + if (out) |ptr| ptr.* = button; + return true; + } + + return false; +} + +pub fn set_mods(event_: Event, mods: Mods) callconv(.c) void { + event_.?.event.mods = mods; +} + +pub fn get_mods(event_: Event) callconv(.c) Mods { + return event_.?.event.mods; +} + +pub fn set_position(event_: Event, pos: Position) callconv(.c) void { + event_.?.event.pos = pos; +} + +pub fn get_position(event_: Event) callconv(.c) Position { + return event_.?.event.pos; +} + +test "alloc" { + var e: Event = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + free(e); +} + +test "free null" { + free(null); +} + +test "set/get" { + var e: Event = undefined; + try testing.expectEqual(Result.success, new( + &lib_alloc.test_allocator, + &e, + )); + defer free(e); + + // Action + set_action(e, .motion); + try testing.expectEqual(Action.motion, get_action(e)); + + // Button + set_button(e, .left); + var button: Button = .unknown; + try testing.expect(get_button(e, &button)); + try testing.expectEqual(Button.left, button); + try testing.expect(get_button(e, null)); + + clear_button(e); + try testing.expect(!get_button(e, &button)); + + // Mods + const mods: Mods = .{ .shift = true, .ctrl = true }; + set_mods(e, mods); + const got_mods = get_mods(e); + try testing.expect(got_mods.shift); + try testing.expect(got_mods.ctrl); + try testing.expect(!got_mods.alt); + + // Position + set_position(e, .{ .x = 12.5, .y = -4.0 }); + const pos = get_position(e); + try testing.expectEqual(@as(f32, 12.5), pos.x); + try testing.expectEqual(@as(f32, -4.0), pos.y); +} diff --git a/src/terminal/main.zig b/src/terminal/main.zig index 06c930014..a87d5ec87 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -31,7 +31,10 @@ pub const Cell = page.Cell; pub const Coordinate = point.Coordinate; pub const CSI = Parser.Action.CSI; pub const DCS = Parser.Action.DCS; -pub const MouseShape = @import("mouse_shape.zig").MouseShape; +pub const mouse = @import("mouse.zig"); +pub const MouseEvent = mouse.Event; +pub const MouseFormat = mouse.Format; +pub const MouseShape = mouse.Shape; pub const Page = page.Page; pub const PageList = @import("PageList.zig"); pub const Parser = @import("Parser.zig"); diff --git a/src/terminal/mouse_shape.zig b/src/terminal/mouse.zig similarity index 76% rename from src/terminal/mouse_shape.zig rename to src/terminal/mouse.zig index b5c6ac4d1..e72e166f7 100644 --- a/src/terminal/mouse_shape.zig +++ b/src/terminal/mouse.zig @@ -1,13 +1,39 @@ const std = @import("std"); const build_options = @import("terminal_options"); const lib = @import("../lib/main.zig"); +const lib_target: lib.Target = if (build_options.c_abi) .c else .zig; + +/// The event types that can be reported for mouse-related activities. +/// These are all mutually exclusive (hence in a single enum). +pub const Event = lib.Enum(lib_target, &.{ + "none", + "x10", // 9 + "normal", // 1000 + "button", // 1002 + "any", // 1003 +}); + +/// Returns true if this event sends motion events. +pub fn eventSendsMotion(event: Event) bool { + return event == .button or event == .any; +} + +/// The format of mouse events when enabled. +/// These are all mutually exclusive (hence in a single enum). +pub const Format = lib.Enum(lib_target, &.{ + "x10", + "utf8", // 1005 + "sgr", // 1006 + "urxvt", // 1015 + "sgr_pixels", // 1016 +}); /// The possible cursor shapes. Not all app runtimes support these shapes. /// The shapes are always based on the W3C supported cursor styles so we /// can have a cross platform list. // // Must be kept in sync with ghostty_cursor_shape_e -pub const MouseShape = enum(c_int) { +pub const Shape = enum(c_int) { default, context_menu, help, @@ -44,7 +70,7 @@ pub const MouseShape = enum(c_int) { zoom_out, /// Build cursor shape from string or null if its unknown. - pub fn fromString(v: []const u8) ?MouseShape { + pub fn fromString(v: []const u8) ?Shape { return string_map.get(v); } @@ -57,7 +83,7 @@ pub const MouseShape = enum(c_int) { break :gtk switch (@import("../build_config.zig").app_runtime) { .gtk => @import("gobject").ext.defineEnum( - MouseShape, + Shape, .{ .name = "GhosttyMouseShape" }, ), @@ -66,11 +92,11 @@ pub const MouseShape = enum(c_int) { }; test "ghostty.h MouseShape" { - try lib.checkGhosttyHEnum(MouseShape, "GHOSTTY_MOUSE_SHAPE_"); + try lib.checkGhosttyHEnum(Shape, "GHOSTTY_MOUSE_SHAPE_"); } }; -const string_map = std.StaticStringMap(MouseShape).initComptime(.{ +const string_map = std.StaticStringMap(Shape).initComptime(.{ // W3C .{ "default", .default }, .{ "context-menu", .context_menu }, @@ -134,7 +160,7 @@ const string_map = std.StaticStringMap(MouseShape).initComptime(.{ test "cursor shape from string" { const testing = std.testing; - try testing.expectEqual(MouseShape.default, MouseShape.fromString("default").?); + try testing.expectEqual(Shape.default, Shape.fromString("default").?); } test { diff --git a/src/terminal/point.zig b/src/terminal/point.zig index 5a3d4a6f8..c5983fcbc 100644 --- a/src/terminal/point.zig +++ b/src/terminal/point.zig @@ -66,7 +66,7 @@ pub const Point = union(Tag) { } }; -pub const Coordinate = struct { +pub const Coordinate = extern struct { /// x can use size.CellCountInt because the number of columns /// can't ever be more than a valid number of columns in a Page. x: size.CellCountInt = 0, diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 5f83128a9..f78bcc6f5 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -16,7 +16,7 @@ const modes = @import("modes.zig"); const osc = @import("osc.zig"); const sgr = @import("sgr.zig"); const UTF8Decoder = @import("UTF8Decoder.zig"); -const MouseShape = @import("mouse_shape.zig").MouseShape; +const MouseShape = @import("mouse.zig").Shape; const log = std.log.scoped(.stream);