From 9b35c2bb65ef900c9eab5ab6920d582cb5333035 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 15 Mar 2026 20:02:39 -0700 Subject: [PATCH] vt: add mouse encoding C API Expose the internal mouse encoding functionality through the C API, following the same pattern as the existing key encoding API. This allows external consumers of libvt to encode mouse events into terminal escape sequences (X10, UTF-8, SGR, URxvt, SGR-Pixels). The API is split into two opaque handle types: GhosttyMouseEvent for building normalized mouse events (action, button, modifiers, position) and GhosttyMouseEncoder for converting those events into escape sequences. The encoder is configured via a setopt interface supporting tracking mode, output format, renderer geometry, button state, and optional motion deduplication by last cell. Encoder state can also be bulk-configured from a terminal handle via ghostty_mouse_encoder_setopt_from_terminal. Failed encodes due to insufficient buffer space report the required size without mutating deduplication state. --- include/ghostty/vt.h | 2 + include/ghostty/vt/mouse.h | 35 ++ include/ghostty/vt/mouse/encoder.h | 211 ++++++++++++ include/ghostty/vt/mouse/event.h | 193 +++++++++++ src/input/mouse.zig | 2 +- src/lib_vt.zig | 17 + src/terminal/c/main.zig | 23 ++ src/terminal/c/mouse_encode.zig | 529 +++++++++++++++++++++++++++++ src/terminal/c/mouse_event.zig | 155 +++++++++ 9 files changed, 1166 insertions(+), 1 deletion(-) create mode 100644 include/ghostty/vt/mouse.h create mode 100644 include/ghostty/vt/mouse/encoder.h create mode 100644 include/ghostty/vt/mouse/event.h create mode 100644 src/terminal/c/mouse_encode.zig create mode 100644 src/terminal/c/mouse_event.zig diff --git a/include/ghostty/vt.h b/include/ghostty/vt.h index dd5eda989..f3958ccc3 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 @@ -88,6 +89,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..4e44e15b5 --- /dev/null +++ b/include/ghostty/vt/mouse.h @@ -0,0 +1,35 @@ +/** + * @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. + * + * @{ + */ + +#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/lib_vt.zig b/src/lib_vt.zig index 0fa808b59..32fd14dc5 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -126,6 +126,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/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); +} -- 2.51.2