diff --git a/include/ghostty/vt/selection.h b/include/ghostty/vt/selection.h index 3ba2f00db..cc20a0691 100644 --- a/include/ghostty/vt/selection.h +++ b/include/ghostty/vt/selection.h @@ -77,6 +77,61 @@ typedef struct { bool rectangle; } GhosttySelection; +/** + * Operation used to adjust a selection endpoint. + * + * Adjustment mutates the selection's logical end endpoint, not whichever + * endpoint is visually bottom/right. This preserves keyboard and drag + * behavior for both forward and reversed selections. + * + * @ingroup selection + */ +typedef enum GHOSTTY_ENUM_TYPED { + /** Move left to the previous non-empty cell, wrapping upward. */ + GHOSTTY_SELECTION_ADJUST_LEFT = 0, + + /** Move right to the next non-empty cell, wrapping downward. */ + GHOSTTY_SELECTION_ADJUST_RIGHT = 1, + + /** + * Move up one row at the current column, or to the beginning of the + * line if already at the top. + */ + GHOSTTY_SELECTION_ADJUST_UP = 2, + + /** + * Move down to the next non-blank row at the current column, or to the + * end of the line if none exists. + */ + GHOSTTY_SELECTION_ADJUST_DOWN = 3, + + /** Move to the top-left cell of the screen. */ + GHOSTTY_SELECTION_ADJUST_HOME = 4, + + /** Move to the right edge of the last non-blank row on the screen. */ + GHOSTTY_SELECTION_ADJUST_END = 5, + + /** + * Move up by one terminal page height, or to home if that would move + * past the top. + */ + GHOSTTY_SELECTION_ADJUST_PAGE_UP = 6, + + /** + * Move down by one terminal page height, or to end if that would move + * past the bottom. + */ + GHOSTTY_SELECTION_ADJUST_PAGE_DOWN = 7, + + /** Move to the left edge of the current line. */ + GHOSTTY_SELECTION_ADJUST_BEGINNING_OF_LINE = 8, + + /** Move to the right edge of the current line. */ + GHOSTTY_SELECTION_ADJUST_END_OF_LINE = 9, + + GHOSTTY_SELECTION_ADJUST_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, +} GhosttySelectionAdjust; + /** @} */ #ifdef __cplusplus diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 756698449..55886e395 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -1123,6 +1123,35 @@ GHOSTTY_API GhosttyResult ghostty_terminal_get_multi(GhosttyTerminal terminal, void** values, size_t* out_written); +/** + * Adjust a selection snapshot using terminal selection semantics. + * + * This mutates the caller-provided GhosttySelection in place. The logical end + * endpoint is always moved, regardless of whether the selection is forward or + * reversed visually. The input selection remains a snapshot: after adjustment, + * call ghostty_terminal_set() with GHOSTTY_TERMINAL_OPT_SELECTION to install it + * as the terminal-owned selection if desired. + * + * The selection's start and end grid refs must both be valid untracked + * snapshots for the given terminal's currently active screen. In practice, + * they must come from that terminal and screen, and no mutating terminal call + * may have occurred since the refs were produced or reconstructed from + * tracked refs. Passing refs from another terminal, another screen, or stale + * refs violates this precondition. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param selection Selection snapshot to adjust in place + * @param adjustment The adjustment operation to apply + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal, + * selection, selection references, or adjustment are invalid + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_selection_adjust( + GhosttyTerminal terminal, + GhosttySelection* selection, + GhosttySelectionAdjust adjustment); + /** * Resolve a point in the terminal grid to a grid reference. * diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 12aa66bfe..6276f707c 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -239,6 +239,7 @@ comptime { @export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" }); @export(&c.terminal_get, .{ .name = "ghostty_terminal_get" }); @export(&c.terminal_get_multi, .{ .name = "ghostty_terminal_get_multi" }); + @export(&c.terminal_selection_adjust, .{ .name = "ghostty_terminal_selection_adjust" }); @export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" }); @export(&c.terminal_grid_ref_track, .{ .name = "ghostty_terminal_grid_ref_track" }); @export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_grid_ref" }); diff --git a/src/terminal/Selection.zig b/src/terminal/Selection.zig index 8cb52816c..4d7d4a2fa 100644 --- a/src/terminal/Selection.zig +++ b/src/terminal/Selection.zig @@ -4,6 +4,7 @@ const Selection = @This(); const std = @import("std"); const assert = @import("../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; +const lib = @import("lib.zig"); const page = @import("page.zig"); const point = @import("point.zig"); const PageList = @import("PageList.zig"); @@ -389,18 +390,18 @@ pub fn containedRowCached( } /// Possible adjustments to the selection. -pub const Adjustment = enum { - left, - right, - up, - down, - home, - end, - page_up, - page_down, - beginning_of_line, - end_of_line, -}; +pub const Adjustment = lib.Enum(lib.target, &.{ + "left", + "right", + "up", + "down", + "home", + "end", + "page_up", + "page_down", + "beginning_of_line", + "end_of_line", +}); /// Adjust the selection by some given adjustment. An adjustment allows /// a selection to be expanded slightly left, right, up, down, etc. diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index ab6ab719b..35fb8b197 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -170,6 +170,7 @@ pub const terminal_mode_get = terminal.mode_get; pub const terminal_mode_set = terminal.mode_set; pub const terminal_get = terminal.get; pub const terminal_get_multi = terminal.get_multi; +pub const terminal_selection_adjust = terminal.selection_adjust; pub const terminal_grid_ref = terminal.grid_ref; pub const terminal_grid_ref_track = terminal.grid_ref_track; pub const terminal_point_from_grid_ref = terminal.point_from_grid_ref; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 2e88ea524..fef6fbad9 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -7,6 +7,7 @@ const ZigTerminal = @import("../Terminal.zig"); const Stream = @import("../stream_terminal.zig").Stream; const ScreenSet = @import("../ScreenSet.zig"); const PageList = @import("../PageList.zig"); +const Selection = @import("../Selection.zig"); const apc = @import("../apc.zig"); const kitty = @import("../kitty/key.zig"); const kitty_gfx_c = @import("kitty_graphics.zig"); @@ -664,6 +665,26 @@ pub fn get_multi( return .success; } +pub fn selection_adjust( + terminal_: Terminal, + selection: ?*selection_c.CSelection, + adjustment: Selection.Adjustment, +) callconv(lib.calling_conv) Result { + if (comptime std.debug.runtime_safety) { + _ = std.meta.intToEnum(Selection.Adjustment, @intFromEnum(adjustment)) catch { + log.warn("terminal_selection_adjust invalid adjustment value={d}", .{@intFromEnum(adjustment)}); + return .invalid_value; + }; + } + + const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal; + const sel_ptr = selection orelse return .invalid_value; + var sel = sel_ptr.toZig() orelse return .invalid_value; + sel.adjust(t.screens.active, adjustment); + sel_ptr.* = .fromZig(sel); + return .success; +} + fn getTyped( terminal_: Terminal, comptime data: TerminalData, @@ -1389,6 +1410,54 @@ test "set and get selection" { try testing.expectEqual(Result.no_value, get(t, .selection, @ptrCast(&out))); } +test "selection_adjust mutates snapshot end" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &t, + .{ + .cols = 80, + .rows = 24, + .max_scrollback = 0, + }, + )); + defer free(t); + + vt_write(t, "Hello", 5); + + var start_ref: grid_ref_c.CGridRef = .{}; + try testing.expectEqual(Result.success, grid_ref(t, .{ + .tag = .active, + .value = .{ .active = .{ .x = 0, .y = 0 } }, + }, &start_ref)); + + var end_ref: grid_ref_c.CGridRef = .{}; + try testing.expectEqual(Result.success, grid_ref(t, .{ + .tag = .active, + .value = .{ .active = .{ .x = 1, .y = 0 } }, + }, &end_ref)); + + var sel: selection_c.CSelection = .{ + .start = start_ref, + .end = end_ref, + }; + try testing.expectEqual(Result.success, selection_adjust(t, &sel, .right)); + 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, selection_adjust(t, &sel, .left)); + try testing.expectEqual(@as(u16, 0), sel.start.toPin().?.x); + try testing.expectEqual(@as(u16, 1), sel.end.toPin().?.x); + + sel = .{ + .start = end_ref, + .end = start_ref, + }; + try testing.expectEqual(Result.success, selection_adjust(t, &sel, .right)); + try testing.expectEqual(@as(u16, 1), sel.start.toPin().?.x); + try testing.expectEqual(@as(u16, 1), sel.end.toPin().?.x); +} + test "grid_ref" { var t: Terminal = null; try testing.expectEqual(Result.success, new(