diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 1751aa126..756698449 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #ifdef __cplusplus @@ -592,6 +593,21 @@ typedef enum GHOSTTY_ENUM_TYPED { * Input type: size_t* */ GHOSTTY_TERMINAL_OPT_APC_MAX_BYTES_KITTY = 20, + + /** + * Set the active screen selection. + * + * The value must point to a GhosttySelection whose grid references are + * valid for this terminal's active screen at the time of the call. The + * terminal copies the selection immediately and converts it to + * terminal-owned tracked state, so the GhosttySelection struct and its + * untracked grid references do not need to outlive this call. + * + * Passing NULL clears the active screen selection. + * + * Input type: GhosttySelection* + */ + GHOSTTY_TERMINAL_OPT_SELECTION = 21, GHOSTTY_TERMINAL_OPT_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyTerminalOption; @@ -868,6 +884,23 @@ typedef enum GHOSTTY_ENUM_TYPED { * Output type: GhosttyKittyGraphics * */ GHOSTTY_TERMINAL_DATA_KITTY_GRAPHICS = 30, + + /** + * The active screen's current selection. + * + * On success, writes an untracked snapshot of the terminal-owned selection + * to the caller-provided GhosttySelection. The GhosttySelection struct is + * caller-owned and may be kept, but the grid references inside it are + * untracked borrowed references into the active screen. They are only valid + * until the next mutating terminal call, such as ghostty_terminal_set(), + * ghostty_terminal_vt_write(), ghostty_terminal_resize(), or + * ghostty_terminal_reset(). + * + * Returns GHOSTTY_NO_VALUE when there is no active selection. + * + * Output type: GhosttySelection * + */ + GHOSTTY_TERMINAL_DATA_SELECTION = 31, GHOSTTY_TERMINAL_DATA_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE, } GhosttyTerminalData; diff --git a/src/terminal/c/selection.zig b/src/terminal/c/selection.zig index 74e96598f..c4c6284e6 100644 --- a/src/terminal/c/selection.zig +++ b/src/terminal/c/selection.zig @@ -13,4 +13,12 @@ pub const CSelection = extern struct { const end_pin = self.end.toPin() orelse return null; return Selection.init(start_pin, end_pin, self.rectangle); } + + pub fn fromZig(sel: Selection) CSelection { + return .{ + .start = .fromPin(sel.start()), + .end = .fromPin(sel.end()), + .rectangle = sel.rectangle, + }; + } }; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 662a2ec03..2e88ea524 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -20,6 +20,7 @@ const cell_c = @import("cell.zig"); const row_c = @import("row.zig"); const grid_ref_c = @import("grid_ref.zig"); const grid_ref_tracked_c = @import("grid_ref_tracked.zig"); +const selection_c = @import("selection.zig"); const style_c = @import("style.zig"); const color = @import("../color.zig"); const Result = @import("result.zig").Result; @@ -314,6 +315,7 @@ pub const Option = enum(c_int) { kitty_image_medium_shared_mem = 18, apc_max_bytes = 19, apc_max_bytes_kitty = 20, + selection = 21, /// Input type expected for setting the option. pub fn InType(comptime self: Option) type { @@ -336,6 +338,7 @@ pub const Option = enum(c_int) { .kitty_image_medium_shared_mem, => ?*const bool, .apc_max_bytes, .apc_max_bytes_kitty => ?*const usize, + .selection => ?*const selection_c.CSelection, }; } }; @@ -443,6 +446,14 @@ fn setTyped( wrapper.stream.handler.apc_handler.max_bytes.remove(.kitty); } }, + .selection => { + if (value) |ptr| { + const sel = ptr.toZig() orelse return .invalid_value; + wrapper.terminal.screens.active.select(sel) catch return .out_of_memory; + } else { + wrapper.terminal.screens.active.clearSelection(); + } + }, } return .success; } @@ -576,6 +587,7 @@ pub const TerminalData = enum(c_int) { kitty_image_medium_temp_file = 28, kitty_image_medium_shared_mem = 29, kitty_graphics = 30, + selection = 31, /// Output type expected for querying the data of the given kind. pub fn OutType(comptime self: TerminalData) type { @@ -604,6 +616,7 @@ pub const TerminalData = enum(c_int) { .kitty_image_medium_shared_mem, => bool, .kitty_graphics => KittyGraphics, + .selection => selection_c.CSelection, }; } }; @@ -713,6 +726,9 @@ fn getTyped( if (comptime !build_options.kitty_graphics) return .no_value; out.* = &t.screens.active.kitty_images; }, + .selection => out.* = selection_c.CSelection.fromZig( + t.screens.active.selection orelse return .no_value, + ), } return .success; @@ -1325,6 +1341,54 @@ test "get invalid" { try testing.expectEqual(Result.invalid_value, get(t, .invalid, null)); } +test "set and get selection" { + 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 = 4, .y = 0 } }, + }, &end_ref)); + + var out: selection_c.CSelection = undefined; + try testing.expectEqual(Result.no_value, get(t, .selection, @ptrCast(&out))); + + const sel: selection_c.CSelection = .{ + .start = start_ref, + .end = end_ref, + .rectangle = true, + }; + try testing.expectEqual(Result.success, set(t, .selection, @ptrCast(&sel))); + try testing.expect(t.?.terminal.screens.active.selection.?.tracked()); + + try testing.expectEqual(Result.success, get(t, .selection, @ptrCast(&out))); + try testing.expect(out.start.toPin().?.eql(start_ref.toPin().?)); + try testing.expect(out.end.toPin().?.eql(end_ref.toPin().?)); + try testing.expect(out.rectangle); + + try testing.expectEqual(Result.success, set(t, .selection, null)); + try testing.expect(t.?.terminal.screens.active.selection == null); + try testing.expectEqual(Result.no_value, get(t, .selection, @ptrCast(&out))); +} + test "grid_ref" { var t: Terminal = null; try testing.expectEqual(Result.success, new(