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.
This commit is contained in:
Mitchell Hashimoto
2026-03-15 20:02:39 -07:00
parent 79e023b65e
commit 9b35c2bb65
9 changed files with 1166 additions and 1 deletions

View File

@@ -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 <ghostty/vt/osc.h>
#include <ghostty/vt/sgr.h>
#include <ghostty/vt/key.h>
#include <ghostty/vt/mouse.h>
#include <ghostty/vt/paste.h>
#include <ghostty/vt/wasm.h>

View File

@@ -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 <ghostty/vt/mouse/event.h>
#include <ghostty/vt/mouse/encoder.h>
/** @} */
#endif /* GHOSTTY_VT_MOUSE_H */

View File

@@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/mouse/event.h>
#include <ghostty/vt/terminal.h>
#include <ghostty/vt/types.h>
/**
* 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 */

View File

@@ -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 <stdbool.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/key/event.h>
#include <ghostty/vt/types.h>
/**
* 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 */

View File

@@ -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.
///

View File

@@ -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" });

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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);
}