libghostty: add ghostty_selection_gesture_event

This commit is contained in:
Mitchell Hashimoto
2026-05-27 10:44:40 -07:00
parent bbfa984aec
commit 5ac8e6569a
4 changed files with 174 additions and 2 deletions

View File

@@ -500,6 +500,35 @@ GHOSTTY_API GhosttyResult ghostty_selection_gesture_event_set(
GhosttySelectionGestureEventOption option,
const void* value);
/**
* Apply a selection gesture event and return the resulting selection snapshot.
*
* This dispatches to the gesture operation matching the event's fixed type.
* For GHOSTTY_SELECTION_GESTURE_EVENT_TYPE_PRESS, the event must have
* GHOSTTY_SELECTION_GESTURE_EVENT_OPT_REF set before calling this function.
* All other press options use their initialized defaults when unset or cleared.
*
* The returned selection is not installed as the terminal's current selection.
* It is a snapshot with the same lifetime rules as GhosttySelection.
*
* @param gesture Selection gesture handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param terminal Terminal used to interpret and update gesture state
* @param event Selection gesture event handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param[out] out_selection On success, receives the resulting selection. May
* be NULL to apply the event and discard the selection result.
* @return GHOSTTY_SUCCESS on success, GHOSTTY_NO_VALUE if the event does not
* currently produce a selection, GHOSTTY_OUT_OF_MEMORY if tracking
* gesture state fails, or GHOSTTY_INVALID_VALUE if gesture, terminal,
* event, or required event data is invalid
*
* @ingroup selection
*/
GHOSTTY_API GhosttyResult ghostty_selection_gesture_event(
GhosttySelectionGesture gesture,
GhosttyTerminal terminal,
GhosttySelectionGestureEvent event,
GhosttySelection* out_selection);
/**
* Create a selection gesture object.
*

View File

@@ -254,6 +254,7 @@ comptime {
@export(&c.selection_gesture_new, .{ .name = "ghostty_selection_gesture_new" });
@export(&c.selection_gesture_free, .{ .name = "ghostty_selection_gesture_free" });
@export(&c.selection_gesture_reset, .{ .name = "ghostty_selection_gesture_reset" });
@export(&c.selection_gesture_event, .{ .name = "ghostty_selection_gesture_event" });
@export(&c.selection_gesture_get, .{ .name = "ghostty_selection_gesture_get" });
@export(&c.selection_gesture_get_multi, .{ .name = "ghostty_selection_gesture_get_multi" });
@export(&c.selection_gesture_event_new, .{ .name = "ghostty_selection_gesture_event_new" });

View File

@@ -186,6 +186,7 @@ pub const terminal_selection_equal = selection.equal;
pub const selection_gesture_new = selection_gesture.new;
pub const selection_gesture_free = selection_gesture.free;
pub const selection_gesture_reset = selection_gesture.reset;
pub const selection_gesture_event = selection_gesture.handle_event;
pub const selection_gesture_get = selection_gesture.get;
pub const selection_gesture_get_multi = selection_gesture.get_multi;
pub const selection_gesture_event_new = selection_gesture.event_new;

View File

@@ -6,6 +6,7 @@ const CAllocator = lib.alloc.Allocator;
const SelectionGesture = @import("../SelectionGesture.zig");
const selection_codepoints = @import("../selection_codepoints.zig");
const grid_ref = @import("grid_ref.zig");
const selection_c = @import("selection.zig");
const terminal_c = @import("terminal.zig");
const types = @import("types.zig");
const Result = @import("result.zig").Result;
@@ -29,6 +30,12 @@ const EventWrapper = struct {
press: SelectionGesture.Press,
},
// Press.pin has no safe sentinel value: PageList.Pin contains a non-null
// node pointer and is undefined until the C caller provides a GhosttyGridRef.
// Track that separately so event execution can reject a press whose required
// ref option was never set, or was later cleared.
press_pin_set: bool = false,
// Backing storage for Press.word_boundary_codepoints. The C API receives
// codepoints as borrowed uint32_t values, but SelectionGesture.Press stores
// a []const u21 slice. We copy/convert into event-owned storage so the real
@@ -196,6 +203,28 @@ pub fn reset(
wrapper.gesture.reset(t);
}
pub fn handle_event(
gesture_: Gesture,
terminal: terminal_c.Terminal,
event_: Event,
out_selection: ?*selection_c.CSelection,
) callconv(lib.calling_conv) Result {
const wrapper = gesture_ orelse return .invalid_value;
const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
const event_wrapper = event_ orelse return .invalid_value;
return switch (event_wrapper.event) {
.press => |press| {
if (!event_wrapper.press_pin_set) return .invalid_value;
const sel = wrapper.gesture.press(t, press) catch return .out_of_memory;
if (out_selection) |out| {
out.* = selection_c.CSelection.fromZig(sel orelse return .no_value);
} else if (sel == null) return .no_value;
return .success;
},
};
}
pub fn event_set(
event_: Event,
option: EventOption,
@@ -306,7 +335,7 @@ fn pressSetTyped(
) Result {
const v = value orelse {
switch (option) {
.ref => {},
.ref => event.press_pin_set = false,
.position => {
press.xpos = 0;
press.ypos = 0;
@@ -324,7 +353,10 @@ fn pressSetTyped(
};
switch (option) {
.ref => press.pin = v.toPin() orelse return .invalid_value,
.ref => {
press.pin = v.toPin() orelse return .invalid_value;
event.press_pin_set = true;
},
.position => {
press.xpos = v.x;
press.ypos = v.y;
@@ -575,6 +607,115 @@ test "selection gesture event behaviors" {
try testing.expectEqual(Behavior.line, event.?.event.press.behaviors[2]);
}
test "selection gesture event applies press" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&terminal,
.{ .cols = 5, .rows = 2, .max_scrollback = 10_000 },
));
defer terminal_c.free(terminal);
var gesture: Gesture = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&gesture,
));
defer free(gesture, terminal);
var press_event: Event = null;
try testing.expectEqual(Result.success, event_new(
&lib.alloc.test_allocator,
&press_event,
.press,
));
defer event_free(press_event);
terminal_c.vt_write(terminal, "abc", 3);
var ref: grid_ref.CGridRef = undefined;
try testing.expectEqual(Result.success, terminal_c.grid_ref(terminal, .{
.tag = .active,
.value = .{ .active = .{ .x = 1, .y = 0 } },
}, &ref));
try testing.expectEqual(Result.success, event_set(press_event, .ref, &ref));
const behaviors: Behaviors = .{
.single_click = .word,
.double_click = .word,
.triple_click = .line,
};
try testing.expectEqual(Result.success, event_set(press_event, .behaviors, &behaviors));
var sel: selection_c.CSelection = undefined;
try testing.expectEqual(Result.success, handle_event(gesture, terminal, press_event, &sel));
try testing.expectEqual(@as(u16, 0), sel.start.toPin().?.x);
try testing.expectEqual(@as(u16, 2), sel.end.toPin().?.x);
try testing.expectEqual(Result.success, handle_event(gesture, terminal, press_event, null));
}
test "selection gesture event press requires ref" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&terminal,
.{ .cols = 5, .rows = 2, .max_scrollback = 10_000 },
));
defer terminal_c.free(terminal);
var gesture: Gesture = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&gesture,
));
defer free(gesture, terminal);
var press_event: Event = null;
try testing.expectEqual(Result.success, event_new(
&lib.alloc.test_allocator,
&press_event,
.press,
));
defer event_free(press_event);
var sel: selection_c.CSelection = undefined;
try testing.expectEqual(Result.invalid_value, handle_event(gesture, terminal, press_event, &sel));
}
test "selection gesture event null output still reports no selection" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
&lib.alloc.test_allocator,
&terminal,
.{ .cols = 5, .rows = 2, .max_scrollback = 10_000 },
));
defer terminal_c.free(terminal);
var gesture: Gesture = null;
try testing.expectEqual(Result.success, new(
&lib.alloc.test_allocator,
&gesture,
));
defer free(gesture, terminal);
var press_event: Event = null;
try testing.expectEqual(Result.success, event_new(
&lib.alloc.test_allocator,
&press_event,
.press,
));
defer event_free(press_event);
var ref: grid_ref.CGridRef = undefined;
try testing.expectEqual(Result.success, terminal_c.grid_ref(terminal, .{
.tag = .active,
.value = .{ .active = .{ .x = 1, .y = 0 } },
}, &ref));
try testing.expectEqual(Result.success, event_set(press_event, .ref, &ref));
try testing.expectEqual(Result.no_value, handle_event(gesture, terminal, press_event, null));
}
test "selection gesture free null" {
free(null, null);
}