diff --git a/include/ghostty/vt/selection.h b/include/ghostty/vt/selection.h index 053c3bc44..0270a3038 100644 --- a/include/ghostty/vt/selection.h +++ b/include/ghostty/vt/selection.h @@ -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. * diff --git a/src/lib_vt.zig b/src/lib_vt.zig index a7139476f..6d4406e88 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -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" }); diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 959edaabe..648bdbe51 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -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; diff --git a/src/terminal/c/selection_gesture.zig b/src/terminal/c/selection_gesture.zig index 8087a88a9..df823e62e 100644 --- a/src/terminal/c/selection_gesture.zig +++ b/src/terminal/c/selection_gesture.zig @@ -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); }