aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/terminal/c
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-01 23:05:23 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-01 23:05:51 +0300
commit5bd21b7ed468b5ff4d1a5b2ed7e211f401eb138a (patch)
tree672fa4625cca0ecb3e9376fc7f8e33d7b0e9deef /vendor/libghostty-vt/src/terminal/c
parent32abb3776668ee768200019c170956f67fe33a86 (diff)
chore: update libghostty vendor
refs #276
Diffstat (limited to 'vendor/libghostty-vt/src/terminal/c')
-rw-r--r--vendor/libghostty-vt/src/terminal/c/grid_ref_tracked.zig225
-rw-r--r--vendor/libghostty-vt/src/terminal/c/main.zig31
-rw-r--r--vendor/libghostty-vt/src/terminal/c/render.zig329
-rw-r--r--vendor/libghostty-vt/src/terminal/c/selection.zig515
-rw-r--r--vendor/libghostty-vt/src/terminal/c/selection_gesture.zig1488
-rw-r--r--vendor/libghostty-vt/src/terminal/c/terminal.zig501
-rw-r--r--vendor/libghostty-vt/src/terminal/c/types.zig77
7 files changed, 3133 insertions, 33 deletions
diff --git a/vendor/libghostty-vt/src/terminal/c/grid_ref_tracked.zig b/vendor/libghostty-vt/src/terminal/c/grid_ref_tracked.zig
new file mode 100644
index 00000000..60091715
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/grid_ref_tracked.zig
@@ -0,0 +1,225 @@
+const std = @import("std");
+const testing = std.testing;
+const lib = @import("../lib.zig");
+const PageList = @import("../PageList.zig");
+const point = @import("../point.zig");
+const grid_ref_c = @import("grid_ref.zig");
+const terminal_c = @import("terminal.zig");
+const Result = @import("result.zig").Result;
+
+/// C: GhosttyTrackedGridRef
+///
+/// An owned tracked reference to a position in the terminal grid. The
+/// underlying PageList pin is automatically updated as the PageList changes.
+pub const CTrackedGridRef = ?*TrackedGridRef;
+
+pub const TrackedGridRef = struct {
+ alloc: std.mem.Allocator,
+ terminal: terminal_c.Terminal,
+ screen_key: terminal_c.TerminalScreen,
+ screen_generation: usize,
+ pin: *PageList.Pin,
+
+ /// Return the PageList that owns this tracked ref's pin, or null if the
+ /// owning screen has been removed/reinitialized since the ref was created.
+ fn pageList(ref: *const TrackedGridRef) ?*PageList {
+ const wrapper = ref.terminal orelse return null;
+ const t = wrapper.terminal;
+ if (t.screens.generation(ref.screen_key) != ref.screen_generation) return null;
+ const screen = t.screens.get(ref.screen_key) orelse return null;
+ return &screen.pages;
+ }
+};
+
+pub fn tracked_grid_ref_free(ref_: CTrackedGridRef) callconv(lib.calling_conv) void {
+ const ref = ref_ orelse return;
+ if (ref.terminal) |wrapper| {
+ _ = wrapper.tracked_grid_refs.swapRemove(ref);
+ }
+ if (ref.pageList()) |list| list.untrackPin(ref.pin);
+ ref.alloc.destroy(ref);
+}
+
+pub fn tracked_grid_ref_has_value(ref_: CTrackedGridRef) callconv(lib.calling_conv) bool {
+ const ref = ref_ orelse return false;
+ _ = ref.pageList() orelse return false;
+ return !ref.pin.garbage;
+}
+
+pub fn tracked_grid_ref_snapshot(
+ ref_: CTrackedGridRef,
+ out_ref: ?*grid_ref_c.CGridRef,
+) callconv(lib.calling_conv) Result {
+ const ref = ref_ orelse return .invalid_value;
+ _ = ref.pageList() orelse return .no_value;
+ if (ref.pin.garbage) return .no_value;
+ if (out_ref) |out| out.* = grid_ref_c.CGridRef.fromPin(ref.pin.*);
+ return .success;
+}
+
+pub fn tracked_grid_ref_point(
+ ref_: CTrackedGridRef,
+ tag: point.Tag,
+ out: ?*point.Coordinate,
+) callconv(lib.calling_conv) Result {
+ const ref = ref_ orelse return .invalid_value;
+ const list = ref.pageList() orelse return .no_value;
+ if (ref.pin.garbage) return .no_value;
+ const pt = list.pointFromPin(tag, ref.pin.*) orelse return .no_value;
+ if (out) |o| o.* = pt.coord();
+ return .success;
+}
+
+pub fn tracked_grid_ref_set(
+ ref_: CTrackedGridRef,
+ terminal_: terminal_c.Terminal,
+ pt: point.Point.C,
+) callconv(lib.calling_conv) Result {
+ const ref = ref_ orelse return .invalid_value;
+ const wrapper = terminal_ orelse return .invalid_value;
+ if (ref.terminal != terminal_) return .invalid_value;
+
+ const t = wrapper.terminal;
+ const list = &t.screens.active.pages;
+ const p = list.pin(point.Point.fromC(pt)) orelse return .invalid_value;
+ const tracked_pin = list.trackPin(p) catch return .out_of_memory;
+
+ if (ref.pageList()) |old_list| old_list.untrackPin(ref.pin);
+ ref.screen_key = t.screens.active_key;
+ ref.screen_generation = t.screens.generation(ref.screen_key);
+ ref.pin = tracked_pin;
+ return .success;
+}
+
+test "tracked_grid_ref snapshots after terminal scroll" {
+ 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);
+
+ terminal_c.vt_write(terminal, "A", 1);
+
+ var ref: CTrackedGridRef = null;
+ try testing.expectEqual(Result.success, terminal_c.grid_ref_track(
+ terminal,
+ point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }),
+ &ref,
+ ));
+ defer tracked_grid_ref_free(ref);
+
+ terminal_c.vt_write(terminal, "\r\nB\r\nC", 6);
+ try testing.expect(tracked_grid_ref_has_value(ref));
+
+ var snapshot: grid_ref_c.CGridRef = undefined;
+ try testing.expectEqual(Result.success, tracked_grid_ref_snapshot(ref, &snapshot));
+
+ var buf: [1]u32 = undefined;
+ var len: usize = undefined;
+ try testing.expectEqual(Result.success, grid_ref_c.grid_ref_graphemes(&snapshot, &buf, buf.len, &len));
+ try testing.expectEqual(@as(usize, 1), len);
+ try testing.expectEqual(@as(u32, 'A'), buf[0]);
+}
+
+test "tracked_grid_ref reports no value after reset" {
+ 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);
+
+ terminal_c.vt_write(terminal, "A", 1);
+
+ var ref: CTrackedGridRef = null;
+ try testing.expectEqual(Result.success, terminal_c.grid_ref_track(
+ terminal,
+ point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }),
+ &ref,
+ ));
+ defer tracked_grid_ref_free(ref);
+
+ terminal_c.reset(terminal);
+ try testing.expect(!tracked_grid_ref_has_value(ref));
+
+ var snapshot: grid_ref_c.CGridRef = undefined;
+ try testing.expectEqual(Result.no_value, tracked_grid_ref_snapshot(ref, &snapshot));
+}
+
+test "tracked_grid_ref reports no value after alternate screen reset" {
+ 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);
+
+ terminal_c.vt_write(terminal, "\x1b[?1049hA", 9);
+
+ var ref: CTrackedGridRef = null;
+ try testing.expectEqual(Result.success, terminal_c.grid_ref_track(
+ terminal,
+ point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }),
+ &ref,
+ ));
+ defer tracked_grid_ref_free(ref);
+
+ terminal_c.vt_write(terminal, "\x1bc", 2);
+ try testing.expect(!tracked_grid_ref_has_value(ref));
+
+ var snapshot: grid_ref_c.CGridRef = undefined;
+ try testing.expectEqual(Result.no_value, tracked_grid_ref_snapshot(ref, &snapshot));
+
+ var coord: point.Coordinate = undefined;
+ try testing.expectEqual(Result.no_value, tracked_grid_ref_point(ref, .active, &coord));
+
+ terminal_c.vt_write(terminal, "\x1b[?1049h", 8);
+ try testing.expect(!tracked_grid_ref_has_value(ref));
+
+ try testing.expectEqual(Result.success, tracked_grid_ref_set(
+ ref,
+ terminal,
+ point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }),
+ ));
+ try testing.expect(tracked_grid_ref_has_value(ref));
+ try testing.expectEqual(Result.success, tracked_grid_ref_snapshot(ref, &snapshot));
+}
+
+test "tracked_grid_ref reports no value after terminal free" {
+ 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 },
+ ));
+
+ terminal_c.vt_write(terminal, "A", 1);
+
+ var ref: CTrackedGridRef = null;
+ try testing.expectEqual(Result.success, terminal_c.grid_ref_track(
+ terminal,
+ point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }),
+ &ref,
+ ));
+
+ terminal_c.free(terminal);
+ try testing.expect(!tracked_grid_ref_has_value(ref));
+
+ var snapshot: grid_ref_c.CGridRef = undefined;
+ try testing.expectEqual(Result.no_value, tracked_grid_ref_snapshot(ref, &snapshot));
+
+ var coord: point.Coordinate = undefined;
+ try testing.expectEqual(Result.no_value, tracked_grid_ref_point(ref, .active, &coord));
+
+ try testing.expectEqual(Result.invalid_value, tracked_grid_ref_set(
+ ref,
+ terminal,
+ point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }),
+ ));
+
+ tracked_grid_ref_free(ref);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/main.zig b/vendor/libghostty-vt/src/terminal/c/main.zig
index 126b6640..648bdbe5 100644
--- a/vendor/libghostty-vt/src/terminal/c/main.zig
+++ b/vendor/libghostty-vt/src/terminal/c/main.zig
@@ -8,6 +8,7 @@ pub const color = @import("color.zig");
pub const focus = @import("focus.zig");
pub const formatter = @import("formatter.zig");
pub const grid_ref = @import("grid_ref.zig");
+pub const grid_ref_tracked = @import("grid_ref_tracked.zig");
pub const kitty_graphics = @import("kitty_graphics.zig");
pub const kitty_graphics_get = kitty_graphics.get;
pub const kitty_graphics_image = kitty_graphics.image_get_handle;
@@ -30,6 +31,7 @@ pub const modes = @import("modes.zig");
pub const osc = @import("osc.zig");
pub const render = @import("render.zig");
pub const selection = @import("selection.zig");
+pub const selection_gesture = @import("selection_gesture.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");
@@ -169,7 +171,29 @@ 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_select_word = selection.word;
+pub const terminal_select_word_between = selection.word_between;
+pub const terminal_select_line = selection.line;
+pub const terminal_select_all = selection.all;
+pub const terminal_select_output = selection.output;
+pub const terminal_selection_format_buf = selection.format_buf;
+pub const terminal_selection_format_alloc = selection.format_alloc;
+pub const terminal_selection_adjust = selection.adjust;
+pub const terminal_selection_order = selection.order;
+pub const terminal_selection_ordered = selection.ordered;
+pub const terminal_selection_contains = selection.contains;
+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;
+pub const selection_gesture_event_free = selection_gesture.event_free;
+pub const selection_gesture_event_set = selection_gesture.event_set;
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;
pub const type_json = types.get_json;
@@ -179,6 +203,11 @@ pub const grid_ref_row = grid_ref.grid_ref_row;
pub const grid_ref_graphemes = grid_ref.grid_ref_graphemes;
pub const grid_ref_hyperlink_uri = grid_ref.grid_ref_hyperlink_uri;
pub const grid_ref_style = grid_ref.grid_ref_style;
+pub const tracked_grid_ref_free = grid_ref_tracked.tracked_grid_ref_free;
+pub const tracked_grid_ref_has_value = grid_ref_tracked.tracked_grid_ref_has_value;
+pub const tracked_grid_ref_point = grid_ref_tracked.tracked_grid_ref_point;
+pub const tracked_grid_ref_set = grid_ref_tracked.tracked_grid_ref_set;
+pub const tracked_grid_ref_snapshot = grid_ref_tracked.tracked_grid_ref_snapshot;
test {
_ = allocator;
@@ -186,6 +215,7 @@ test {
_ = cell;
_ = color;
_ = grid_ref;
+ _ = grid_ref_tracked;
_ = kitty_graphics;
_ = row;
_ = focus;
@@ -194,6 +224,7 @@ test {
_ = osc;
_ = render;
_ = selection;
+ _ = selection_gesture;
_ = key_event;
_ = key_encode;
_ = mouse_event;
diff --git a/vendor/libghostty-vt/src/terminal/c/render.zig b/vendor/libghostty-vt/src/terminal/c/render.zig
index af82ddfa..0f3b2e78 100644
--- a/vendor/libghostty-vt/src/terminal/c/render.zig
+++ b/vendor/libghostty-vt/src/terminal/c/render.zig
@@ -31,6 +31,7 @@ const RowIteratorWrapper = struct {
/// These are the raw pointers into the render state data.
raws: []const page.Row,
cells: []const std.MultiArrayList(renderpkg.RenderState.Cell),
+ selection: []const ?[2]size.CellCountInt,
dirty: []bool,
/// The color palette from the render state, needed to resolve
@@ -44,6 +45,7 @@ const RowCellsWrapper = struct {
raws: []const page.Cell,
graphemes: []const []const u21,
styles: []const Style,
+ selection: ?[2]size.CellCountInt,
/// The color palette, needed to resolve palette-indexed background colors.
palette: *const colorpkg.Palette,
@@ -61,6 +63,13 @@ pub const RowCells = ?*RowCellsWrapper;
/// C: GhosttyRenderStateDirty
pub const Dirty = renderpkg.RenderState.Dirty;
+/// C: GhosttyRenderStateRowSelection
+pub const RowSelection = extern struct {
+ size: usize = @sizeOf(RowSelection),
+ start_x: u16 = 0,
+ end_x: u16 = 0,
+};
+
/// C: GhosttyRenderStateCursorVisualStyle
pub const CursorVisualStyle = enum(c_int) {
bar = 0,
@@ -241,6 +250,7 @@ fn getTyped(
.y = null,
.raws = row_data.items(.raw),
.cells = row_data.items(.cells),
+ .selection = row_data.items(.selection),
.dirty = row_data.items(.dirty),
.palette = &state.state.colors.palette,
};
@@ -381,6 +391,7 @@ pub fn row_iterator_new(
.y = undefined,
.raws = undefined,
.cells = undefined,
+ .selection = undefined,
.dirty = undefined,
.palette = undefined,
};
@@ -417,6 +428,7 @@ pub fn row_cells_new(
.raws = undefined,
.graphemes = undefined,
.styles = undefined,
+ .selection = undefined,
.palette = undefined,
};
result.* = ptr;
@@ -453,6 +465,9 @@ pub const RowCellsData = enum(c_int) {
graphemes_buf = 4,
bg_color = 5,
fg_color = 6,
+ selected = 7,
+ has_styling = 8,
+ graphemes_utf8 = 9,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: RowCellsData) type {
@@ -463,6 +478,8 @@ pub const RowCellsData = enum(c_int) {
.graphemes_len => u32,
.graphemes_buf => u32,
.bg_color, .fg_color => colorpkg.RGB.C,
+ .selected, .has_styling => bool,
+ .graphemes_utf8 => lib.Buffer,
};
}
};
@@ -478,6 +495,7 @@ pub fn row_cells_get(
return .invalid_value;
};
}
+ if (out == null) return .invalid_value;
return switch (data) {
.invalid => .invalid_value,
@@ -553,17 +571,56 @@ fn rowCellsGetTyped(
const fg = s.fg(.{ .default = .{}, .palette = cells.palette });
out.* = fg.cval();
},
+ .selected => out.* = if (cells.selection) |sel|
+ x >= sel[0] and x <= sel[1]
+ else
+ false,
+ .has_styling => out.* = cell.hasStyling(),
+ .graphemes_utf8 => return rowCellsGetGraphemesUtf8(cell, if (cell.hasGrapheme()) cells.graphemes[x] else &.{}, out),
}
return .success;
}
+fn rowCellsGetGraphemesUtf8(
+ cell: page.Cell,
+ extra: []const u21,
+ out: *lib.Buffer,
+) Result {
+ out.len = 0;
+
+ if (!cell.hasText()) return .success;
+
+ var needed = std.unicode.utf8CodepointSequenceLength(cell.codepoint()) catch
+ return .invalid_value;
+ for (extra) |cp| {
+ needed += std.unicode.utf8CodepointSequenceLength(cp) catch
+ return .invalid_value;
+ }
+ out.len = needed;
+
+ if (out.ptr == null or out.cap < needed) return .out_of_space;
+
+ const buf = out.ptr.?[0..out.cap];
+ var i: usize = 0;
+ i += std.unicode.utf8Encode(cell.codepoint(), buf[i..]) catch
+ return .invalid_value;
+ for (extra) |cp| {
+ i += std.unicode.utf8Encode(cp, buf[i..]) catch
+ return .invalid_value;
+ }
+
+ out.len = i;
+ return .success;
+}
+
/// C: GhosttyRenderStateRowData
pub const RowData = enum(c_int) {
invalid = 0,
dirty = 1,
raw = 2,
cells = 3,
+ selection = 4,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: RowData) type {
@@ -572,6 +629,7 @@ pub const RowData = enum(c_int) {
.dirty => bool,
.raw => row.CRow,
.cells => RowCells,
+ .selection => RowSelection,
};
}
};
@@ -651,9 +709,18 @@ fn rowGetTyped(
.raws = cell_data.items(.raw),
.graphemes = cell_data.items(.grapheme),
.styles = cell_data.items(.style),
+ .selection = it.selection[y],
.palette = it.palette,
};
},
+ .selection => {
+ const out_size = out.size;
+ if (out_size < @sizeOf(RowSelection)) return .invalid_value;
+
+ const sel = it.selection[y] orelse return .no_value;
+ out.start_x = sel[0];
+ out.end_x = sel[1];
+ },
}
return .success;
@@ -845,6 +912,7 @@ test "render: row iterator new/free" {
try testing.expectEqual(@as(?size.CellCountInt, null), iterator_ptr.y);
try testing.expectEqual(row_data.items(.raw).len, iterator_ptr.raws.len);
try testing.expectEqual(row_data.items(.cells).len, iterator_ptr.cells.len);
+ try testing.expectEqual(row_data.items(.selection).len, iterator_ptr.selection.len);
try testing.expectEqual(row_data.items(.dirty).len, iterator_ptr.dirty.len);
}
@@ -1026,6 +1094,267 @@ test "render: row get/set dirty" {
try testing.expect(!dirty);
}
+test "render: row get selection" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 10,
+ .rows = 3,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ const t = terminal.?.terminal;
+ const screen = t.screens.active;
+ try screen.select(.init(
+ screen.pages.pin(.{ .active = .{ .x = 2, .y = 1 } }).?,
+ screen.pages.pin(.{ .active = .{ .x = 4, .y = 1 } }).?,
+ false,
+ ));
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var it: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &it,
+ ));
+ defer row_iterator_free(it);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
+
+ var sel: RowSelection = .{};
+ try testing.expect(row_iterator_next(it));
+ try testing.expectEqual(Result.no_value, row_get(it, .selection, @ptrCast(&sel)));
+
+ try testing.expect(row_iterator_next(it));
+ sel = .{};
+ try testing.expectEqual(Result.success, row_get(it, .selection, @ptrCast(&sel)));
+ try testing.expectEqual(@as(u16, 2), sel.start_x);
+ try testing.expectEqual(@as(u16, 4), sel.end_x);
+
+ try testing.expect(row_iterator_next(it));
+ sel = .{};
+ try testing.expectEqual(Result.no_value, row_get(it, .selection, @ptrCast(&sel)));
+}
+
+test "render: row cells get selected" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 10,
+ .rows = 3,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ const t = terminal.?.terminal;
+ const screen = t.screens.active;
+ try screen.select(.init(
+ screen.pages.pin(.{ .active = .{ .x = 2, .y = 1 } }).?,
+ screen.pages.pin(.{ .active = .{ .x = 4, .y = 1 } }).?,
+ false,
+ ));
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var it: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &it,
+ ));
+ defer row_iterator_free(it);
+
+ var cells: RowCells = null;
+ try testing.expectEqual(Result.success, row_cells_new(
+ &lib.alloc.test_allocator,
+ &cells,
+ ));
+ defer row_cells_free(cells);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
+
+ try testing.expect(row_iterator_next(it));
+ try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
+
+ var selected: bool = true;
+ try testing.expectEqual(Result.success, row_cells_select(cells, 0));
+ try testing.expectEqual(Result.success, row_cells_get(cells, .selected, @ptrCast(&selected)));
+ try testing.expect(!selected);
+
+ try testing.expect(row_iterator_next(it));
+ try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
+
+ try testing.expectEqual(Result.success, row_cells_select(cells, 1));
+ try testing.expectEqual(Result.success, row_cells_get(cells, .selected, @ptrCast(&selected)));
+ try testing.expect(!selected);
+
+ try testing.expectEqual(Result.success, row_cells_select(cells, 2));
+ try testing.expectEqual(Result.success, row_cells_get(cells, .selected, @ptrCast(&selected)));
+ try testing.expect(selected);
+
+ try testing.expectEqual(Result.success, row_cells_select(cells, 4));
+ try testing.expectEqual(Result.success, row_cells_get(cells, .selected, @ptrCast(&selected)));
+ try testing.expect(selected);
+
+ try testing.expectEqual(Result.success, row_cells_select(cells, 5));
+ try testing.expectEqual(Result.success, row_cells_get(cells, .selected, @ptrCast(&selected)));
+ try testing.expect(!selected);
+
+ try testing.expectEqual(Result.success, row_cells_select(cells, 3));
+ selected = false;
+ var written: usize = 0;
+ const keys = [_]RowCellsData{.selected};
+ var values = [_]?*anyopaque{@ptrCast(&selected)};
+ try testing.expectEqual(Result.success, row_cells_get_multi(cells, keys.len, &keys, &values, &written));
+ try testing.expectEqual(keys.len, written);
+ try testing.expect(selected);
+}
+
+test "render: row cells get has_styling" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{
+ .cols = 10,
+ .rows = 3,
+ .max_scrollback = 10_000,
+ },
+ ));
+ defer terminal_c.free(terminal);
+
+ const input = "A\x1b[31mB";
+ terminal_c.vt_write(terminal, input, input.len);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var it: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &it,
+ ));
+ defer row_iterator_free(it);
+
+ var cells: RowCells = null;
+ try testing.expectEqual(Result.success, row_cells_new(
+ &lib.alloc.test_allocator,
+ &cells,
+ ));
+ defer row_cells_free(cells);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
+ try testing.expect(row_iterator_next(it));
+ try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
+
+ var has_styling = true;
+ try testing.expectEqual(Result.success, row_cells_select(cells, 0));
+ try testing.expectEqual(Result.success, row_cells_get(cells, .has_styling, @ptrCast(&has_styling)));
+ try testing.expect(!has_styling);
+
+ try testing.expectEqual(Result.success, row_cells_select(cells, 1));
+ try testing.expectEqual(Result.success, row_cells_get(cells, .has_styling, @ptrCast(&has_styling)));
+ try testing.expect(has_styling);
+
+ has_styling = false;
+ var written: usize = 0;
+ const keys = [_]RowCellsData{.has_styling};
+ var values = [_]?*anyopaque{@ptrCast(&has_styling)};
+ try testing.expectEqual(Result.success, row_cells_get_multi(cells, keys.len, &keys, &values, &written));
+ try testing.expectEqual(keys.len, written);
+ try testing.expect(has_styling);
+}
+
+test "render: row cells get graphemes utf8" {
+ var terminal: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &terminal,
+ .{ .cols = 10, .rows = 3, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(terminal);
+
+ const input = "e\u{301}";
+ terminal_c.vt_write(terminal, input, input.len);
+
+ var state: RenderState = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &state,
+ ));
+ defer free(state);
+
+ try testing.expectEqual(Result.success, update(state, terminal));
+
+ var it: RowIterator = null;
+ try testing.expectEqual(Result.success, row_iterator_new(
+ &lib.alloc.test_allocator,
+ &it,
+ ));
+ defer row_iterator_free(it);
+
+ var cells: RowCells = null;
+ try testing.expectEqual(Result.success, row_cells_new(
+ &lib.alloc.test_allocator,
+ &cells,
+ ));
+ defer row_cells_free(cells);
+
+ try testing.expectEqual(Result.success, get(state, .row_iterator, @ptrCast(&it)));
+ try testing.expect(row_iterator_next(it));
+ try testing.expectEqual(Result.success, row_get(it, .cells, @ptrCast(&cells)));
+
+ try testing.expectEqual(Result.success, row_cells_select(cells, 0));
+
+ var text: lib.Buffer = .{};
+ try testing.expectEqual(Result.out_of_space, row_cells_get(cells, .graphemes_utf8, @ptrCast(&text)));
+ try testing.expectEqual(@as(usize, input.len), text.len);
+
+ var small = [_]u8{ 'x', 'x' };
+ text = .{ .ptr = &small, .cap = small.len };
+ try testing.expectEqual(Result.out_of_space, row_cells_get(cells, .graphemes_utf8, @ptrCast(&text)));
+ try testing.expectEqual(@as(usize, input.len), text.len);
+ try testing.expectEqualSlices(u8, &.{ 'x', 'x' }, &small);
+
+ var buf: [8]u8 = undefined;
+ text = .{ .ptr = &buf, .cap = buf.len };
+ try testing.expectEqual(Result.success, row_cells_get(cells, .graphemes_utf8, @ptrCast(&text)));
+ try testing.expectEqual(input.len, text.len);
+ try testing.expectEqualStrings(input, buf[0..text.len]);
+
+ try testing.expectEqual(Result.success, row_cells_select(cells, 1));
+ text = .{ .ptr = &buf, .cap = buf.len };
+ try testing.expectEqual(Result.success, row_cells_get(cells, .graphemes_utf8, @ptrCast(&text)));
+ try testing.expectEqual(@as(usize, 0), text.len);
+}
+
test "render: row iterator next" {
var terminal: terminal_c.Terminal = null;
try testing.expectEqual(Result.success, terminal_c.new(
diff --git a/vendor/libghostty-vt/src/terminal/c/selection.zig b/vendor/libghostty-vt/src/terminal/c/selection.zig
index 74e96598..cb574ecc 100644
--- a/vendor/libghostty-vt/src/terminal/c/selection.zig
+++ b/vendor/libghostty-vt/src/terminal/c/selection.zig
@@ -1,5 +1,20 @@
+const std = @import("std");
+const testing = std.testing;
+const lib = @import("../lib.zig");
+const CAllocator = lib.alloc.Allocator;
+const formatterpkg = @import("../formatter.zig");
const grid_ref = @import("grid_ref.zig");
+const point = @import("../point.zig");
+const selection_codepoints = @import("../selection_codepoints.zig");
const Selection = @import("../Selection.zig");
+const Result = @import("result.zig").Result;
+const terminal_c = @import("terminal.zig");
+
+const log = std.log.scoped(.selection_c);
+
+pub const Adjustment = Selection.Adjustment;
+pub const Order = Selection.Order;
+pub const Format = formatterpkg.Format;
/// C: GhosttySelection
pub const CSelection = extern struct {
@@ -13,4 +28,504 @@ 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,
+ };
+ }
+};
+
+/// C: GhosttyTerminalSelectWordOptions
+pub const SelectWordOptions = extern struct {
+ size: usize = @sizeOf(SelectWordOptions),
+ ref: grid_ref.CGridRef,
+ boundary_codepoints: ?[*]const u32 = null,
+ boundary_codepoints_len: usize = 0,
+};
+
+/// C: GhosttyTerminalSelectWordBetweenOptions
+pub const SelectWordBetweenOptions = extern struct {
+ size: usize = @sizeOf(SelectWordBetweenOptions),
+ start: grid_ref.CGridRef,
+ end: grid_ref.CGridRef,
+ boundary_codepoints: ?[*]const u32 = null,
+ boundary_codepoints_len: usize = 0,
};
+
+/// C: GhosttyTerminalSelectLineOptions
+pub const SelectLineOptions = extern struct {
+ size: usize = @sizeOf(SelectLineOptions),
+ ref: grid_ref.CGridRef,
+ whitespace: ?[*]const u32 = null,
+ whitespace_len: usize = 0,
+ semantic_prompt_boundary: bool = false,
+};
+
+/// C: GhosttyTerminalSelectionFormatOptions
+pub const FormatOptions = extern struct {
+ size: usize = @sizeOf(FormatOptions),
+ emit: Format,
+ unwrap: bool,
+ trim: bool,
+ selection: ?*const CSelection = null,
+};
+
+pub fn word(
+ terminal: terminal_c.Terminal,
+ options: ?*const SelectWordOptions,
+ out_selection: ?*CSelection,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const opts = options orelse return .invalid_value;
+ if (opts.size < @sizeOf(SelectWordOptions)) return .invalid_value;
+ const out = out_selection orelse return .invalid_value;
+
+ const boundary_codepoints = codepointSlice(
+ opts.boundary_codepoints,
+ opts.boundary_codepoints_len,
+ ) catch return .invalid_value;
+
+ const screen = t.screens.active;
+ const pin = opts.ref.toPin() orelse return .invalid_value;
+ out.* = .fromZig(screen.selectWord(
+ pin,
+ boundary_codepoints orelse &selection_codepoints.default_word_boundaries,
+ ) orelse
+ return .no_value);
+ return .success;
+}
+
+pub fn word_between(
+ terminal: terminal_c.Terminal,
+ options: ?*const SelectWordBetweenOptions,
+ out_selection: ?*CSelection,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const opts = options orelse return .invalid_value;
+ if (opts.size < @sizeOf(SelectWordBetweenOptions)) return .invalid_value;
+ const out = out_selection orelse return .invalid_value;
+
+ const boundary_codepoints = codepointSlice(
+ opts.boundary_codepoints,
+ opts.boundary_codepoints_len,
+ ) catch return .invalid_value;
+
+ const screen = t.screens.active;
+ const start = opts.start.toPin() orelse return .invalid_value;
+ const end = opts.end.toPin() orelse return .invalid_value;
+ out.* = .fromZig(screen.selectWordBetween(
+ start,
+ end,
+ boundary_codepoints orelse &selection_codepoints.default_word_boundaries,
+ ) orelse
+ return .no_value);
+ return .success;
+}
+
+pub fn line(
+ terminal: terminal_c.Terminal,
+ options: ?*const SelectLineOptions,
+ out_selection: ?*CSelection,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const opts = options orelse return .invalid_value;
+ if (opts.size < @sizeOf(SelectLineOptions)) return .invalid_value;
+ const out = out_selection orelse return .invalid_value;
+
+ const whitespace = codepointSlice(
+ opts.whitespace,
+ opts.whitespace_len,
+ ) catch return .invalid_value;
+
+ const screen = t.screens.active;
+ const pin = opts.ref.toPin() orelse return .invalid_value;
+ out.* = .fromZig(screen.selectLine(.{
+ .pin = pin,
+ .whitespace = whitespace orelse &selection_codepoints.default_line_whitespace,
+ .semantic_prompt_boundary = opts.semantic_prompt_boundary,
+ }) orelse return .no_value);
+ return .success;
+}
+
+pub fn all(
+ terminal: terminal_c.Terminal,
+ out_selection: ?*CSelection,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const out = out_selection orelse return .invalid_value;
+
+ out.* = .fromZig(t.screens.active.selectAll() orelse return .no_value);
+ return .success;
+}
+
+pub fn output(
+ terminal: terminal_c.Terminal,
+ ref: grid_ref.CGridRef,
+ out_selection: ?*CSelection,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const out = out_selection orelse return .invalid_value;
+
+ const screen = t.screens.active;
+ const pin = ref.toPin() orelse return .invalid_value;
+ out.* = .fromZig(screen.selectOutput(pin) orelse return .no_value);
+ return .success;
+}
+
+pub fn format_buf(
+ terminal: terminal_c.Terminal,
+ opts: FormatOptions,
+ out_: ?[*]u8,
+ out_len: usize,
+ out_written: *usize,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+
+ if (out_ == null) {
+ var discarding: std.Io.Writer.Discarding = .init(&.{});
+ formatSelection(t, opts, &discarding.writer) catch |err| return switch (err) {
+ error.InvalidValue => .invalid_value,
+ error.NoValue => .no_value,
+ error.WriteFailed => unreachable,
+ };
+ out_written.* = @intCast(discarding.count);
+ return .out_of_space;
+ }
+
+ var writer: std.Io.Writer = .fixed(out_.?[0..out_len]);
+ formatSelection(t, opts, &writer) catch |err| switch (err) {
+ error.InvalidValue => return .invalid_value,
+ error.NoValue => return .no_value,
+ error.WriteFailed => {
+ var discarding: std.Io.Writer.Discarding = .init(&.{});
+ formatSelection(t, opts, &discarding.writer) catch unreachable;
+ out_written.* = @intCast(discarding.count);
+ return .out_of_space;
+ },
+ };
+
+ out_written.* = writer.end;
+ return .success;
+}
+
+pub fn format_alloc(
+ terminal: terminal_c.Terminal,
+ alloc_: ?*const CAllocator,
+ opts: FormatOptions,
+ out_ptr: *?[*]u8,
+ out_len: *usize,
+) callconv(lib.calling_conv) Result {
+ out_ptr.* = null;
+ out_len.* = 0;
+
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const alloc = lib.alloc.default(alloc_);
+
+ var aw: std.Io.Writer.Allocating = .init(alloc);
+ defer aw.deinit();
+
+ formatSelection(t, opts, &aw.writer) catch |err| return switch (err) {
+ error.InvalidValue => .invalid_value,
+ error.NoValue => .no_value,
+ error.WriteFailed => .out_of_memory,
+ };
+
+ const buf = aw.toOwnedSlice() catch return .out_of_memory;
+ out_ptr.* = buf.ptr;
+ out_len.* = buf.len;
+ return .success;
+}
+
+fn formatSelection(
+ t: *terminal_c.ZigTerminal,
+ opts: FormatOptions,
+ writer: *std.Io.Writer,
+) error{ InvalidValue, NoValue, WriteFailed }!void {
+ var formatter = selectionFormatter(t, opts) catch |err| return err;
+ try formatter.format(writer);
+}
+
+fn selectionFormatter(
+ t: *terminal_c.ZigTerminal,
+ opts: FormatOptions,
+) error{ InvalidValue, NoValue }!formatterpkg.TerminalFormatter {
+ if (opts.size < @sizeOf(FormatOptions)) return error.InvalidValue;
+ _ = std.meta.intToEnum(Format, @intFromEnum(opts.emit)) catch
+ return error.InvalidValue;
+
+ const sel = if (opts.selection) |sel|
+ sel.toZig() orelse return error.InvalidValue
+ else
+ t.screens.active.selection orelse return error.NoValue;
+
+ var formatter: formatterpkg.TerminalFormatter = .init(t, .{
+ .emit = opts.emit,
+ .unwrap = opts.unwrap,
+ .trim = opts.trim,
+ });
+ formatter.content = .{ .selection = sel };
+ return formatter;
+}
+
+/// Return the borrowed C array of `uint32_t` codepoints as a `[]const u21`.
+///
+/// `NULL + len 0` returns null, which callers treat as “use the API default
+/// set.” A non-null pointer with `len 0` returns an empty slice, meaning “use an
+/// explicitly empty set.” A non-zero length requires a non-null pointer.
+///
+/// This is intentionally zero-copy. In the C ABI, codepoints are `uint32_t`,
+/// but selection internals use Zig's `u21` to represent valid Unicode scalar
+/// values. Zig currently stores `u21` in the same size and alignment as `u32`,
+/// so we assert that layout relationship and reinterpret the borrowed slice.
+/// If Zig ever changes that representation, these comptime assertions fail
+/// loudly rather than silently making this cast wrong.
+fn codepointSlice(
+ ptr: ?[*]const u32,
+ len: usize,
+) error{InvalidValue}!?[]const u21 {
+ comptime {
+ std.debug.assert(@sizeOf(u21) == @sizeOf(u32));
+ std.debug.assert(@alignOf(u21) == @alignOf(u32));
+ }
+
+ if (len == 0) {
+ const p = ptr orelse return null;
+ _ = p;
+ return &.{};
+ }
+
+ const p = ptr orelse return error.InvalidValue;
+ const cps: [*]const u21 = @ptrCast(p);
+ return cps[0..len];
+}
+
+pub fn adjust(
+ terminal: terminal_c.Terminal,
+ selection: ?*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 = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ 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;
+}
+
+pub fn order(
+ terminal: terminal_c.Terminal,
+ selection: ?*const CSelection,
+ out_order: ?*Selection.Order,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const sel = (selection orelse return .invalid_value).toZig() orelse
+ return .invalid_value;
+ const out = out_order orelse return .invalid_value;
+
+ out.* = sel.order(t.screens.active);
+ return .success;
+}
+
+pub fn ordered(
+ terminal: terminal_c.Terminal,
+ selection: ?*const CSelection,
+ desired: Selection.Order,
+ out_selection: ?*CSelection,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(Selection.Order, @intFromEnum(desired)) catch {
+ log.warn("terminal_selection_ordered invalid desired value={d}", .{@intFromEnum(desired)});
+ return .invalid_value;
+ };
+ }
+
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const sel = (selection orelse return .invalid_value).toZig() orelse
+ return .invalid_value;
+ const out = out_selection orelse return .invalid_value;
+
+ out.* = .fromZig(sel.ordered(t.screens.active, desired));
+ return .success;
+}
+
+pub fn contains(
+ terminal: terminal_c.Terminal,
+ selection: ?*const CSelection,
+ pt: point.Point.C,
+ out_contains: ?*bool,
+) callconv(lib.calling_conv) Result {
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const sel = (selection orelse return .invalid_value).toZig() orelse
+ return .invalid_value;
+ const out = out_contains orelse return .invalid_value;
+
+ const screen = t.screens.active;
+ const pin = screen.pages.pin(.fromC(pt)) orelse return .invalid_value;
+ out.* = sel.contains(screen, pin);
+ return .success;
+}
+
+pub fn equal(
+ terminal: terminal_c.Terminal,
+ a: ?*const CSelection,
+ b: ?*const CSelection,
+ out_equal: ?*bool,
+) callconv(lib.calling_conv) Result {
+ _ = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+ const sel_a = (a orelse return .invalid_value).toZig() orelse
+ return .invalid_value;
+ const sel_b = (b orelse return .invalid_value).toZig() orelse
+ return .invalid_value;
+ const out = out_equal orelse return .invalid_value;
+
+ out.* = sel_a.eql(sel_b);
+ return .success;
+}
+
+test "selection_format_alloc uses active selection" {
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(t);
+
+ terminal_c.vt_write(t, "Hello World", 11);
+
+ var start_ref: grid_ref.CGridRef = .{};
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 6, .y = 0 } },
+ }, &start_ref));
+
+ var end_ref: grid_ref.CGridRef = .{};
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 10, .y = 0 } },
+ }, &end_ref));
+
+ const sel: CSelection = .{
+ .start = start_ref,
+ .end = end_ref,
+ };
+ try testing.expectEqual(Result.success, terminal_c.set(t, .selection, @ptrCast(&sel)));
+
+ const opts: FormatOptions = .{
+ .emit = .plain,
+ .unwrap = true,
+ .trim = true,
+ };
+
+ var required: usize = 0;
+ try testing.expectEqual(Result.out_of_space, format_buf(
+ t,
+ opts,
+ null,
+ 0,
+ &required,
+ ));
+ try testing.expectEqual(@as(usize, 5), required);
+
+ var out_ptr: ?[*]u8 = null;
+ var out_len: usize = 0;
+ try testing.expectEqual(Result.success, format_alloc(
+ t,
+ &lib.alloc.test_allocator,
+ opts,
+ &out_ptr,
+ &out_len,
+ ));
+ const ptr = out_ptr orelse return error.TestExpectedEqual;
+ defer lib.alloc.default(&lib.alloc.test_allocator).free(ptr[0..out_len]);
+
+ try testing.expectEqualStrings("World", ptr[0..out_len]);
+}
+
+test "selection_format_buf uses provided selection" {
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(t);
+
+ terminal_c.vt_write(t, "Hello World", 11);
+
+ var start_ref: grid_ref.CGridRef = .{};
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 0, .y = 0 } },
+ }, &start_ref));
+
+ var end_ref: grid_ref.CGridRef = .{};
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 4, .y = 0 } },
+ }, &end_ref));
+
+ const sel: CSelection = .{
+ .start = start_ref,
+ .end = end_ref,
+ };
+ const opts: FormatOptions = .{
+ .emit = .plain,
+ .unwrap = true,
+ .trim = true,
+ .selection = &sel,
+ };
+
+ var small: [2]u8 = undefined;
+ var written: usize = 0;
+ try testing.expectEqual(Result.out_of_space, format_buf(
+ t,
+ opts,
+ &small,
+ small.len,
+ &written,
+ ));
+ try testing.expectEqual(@as(usize, 5), written);
+
+ var buf: [32]u8 = undefined;
+ try testing.expectEqual(Result.success, format_buf(
+ t,
+ opts,
+ &buf,
+ buf.len,
+ &written,
+ ));
+ try testing.expectEqualStrings("Hello", buf[0..written]);
+}
+
+test "selection_format_alloc returns no_value without active selection" {
+ var t: terminal_c.Terminal = null;
+ try testing.expectEqual(Result.success, terminal_c.new(
+ &lib.alloc.test_allocator,
+ &t,
+ .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 },
+ ));
+ defer terminal_c.free(t);
+
+ var out_ptr: ?[*]u8 = @ptrFromInt(1);
+ var out_len: usize = 123;
+ try testing.expectEqual(Result.no_value, format_alloc(
+ t,
+ &lib.alloc.test_allocator,
+ .{ .emit = .plain, .unwrap = true, .trim = true },
+ &out_ptr,
+ &out_len,
+ ));
+ try testing.expect(out_ptr == null);
+ try testing.expectEqual(@as(usize, 0), out_len);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/selection_gesture.zig b/vendor/libghostty-vt/src/terminal/c/selection_gesture.zig
new file mode 100644
index 00000000..cc56da3c
--- /dev/null
+++ b/vendor/libghostty-vt/src/terminal/c/selection_gesture.zig
@@ -0,0 +1,1488 @@
+const std = @import("std");
+const testing = std.testing;
+const builtin = @import("builtin");
+const lib = @import("../lib.zig");
+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 point = @import("../point.zig");
+const selection_c = @import("selection.zig");
+const terminal_c = @import("terminal.zig");
+const types = @import("types.zig");
+const Result = @import("result.zig").Result;
+
+const log = std.log.scoped(.selection_gesture_c);
+
+/// C: GhosttySelectionGesture
+pub const Gesture = ?*GestureWrapper;
+
+/// C: GhosttySelectionGestureEvent
+pub const Event = ?*EventWrapper;
+
+const GestureWrapper = struct {
+ alloc: std.mem.Allocator,
+ gesture: SelectionGesture = .init,
+};
+
+const EventWrapper = struct {
+ alloc: std.mem.Allocator,
+ event: union(EventType) {
+ press: SelectionGesture.Press,
+ release: SelectionGesture.Release,
+ drag: SelectionGesture.Drag,
+ autoscroll_tick: SelectionGesture.AutoscrollTick,
+ deep_press: SelectionGesture.DeepPress,
+ },
+
+ // Validation sidecar for required event fields that don't have safe
+ // sentinels in the real SelectionGesture payloads. For example, PageList.Pin
+ // contains a non-null node pointer and Geometry has no meaningful zero
+ // value. Keep these as one-bit flags so dispatch can reject incomplete C
+ // events instead of using undefined placeholder data.
+ event_validation: packed struct {
+ press_pin_set: bool = false,
+ drag_pin_set: bool = false,
+ drag_geometry_set: bool = false,
+ autoscroll_tick_viewport_set: bool = false,
+ autoscroll_tick_geometry_set: bool = false,
+ } = .{},
+
+ // Backing storage for Press/Drag/AutoscrollTick.word_boundary_codepoints.
+ // The C API receives codepoints as borrowed uint32_t values, but
+ // SelectionGesture stores a []const u21 slice. We copy/convert into
+ // event-owned storage so the real payload can safely point at it until the
+ // event is changed or freed.
+ word_boundary_codepoints: ?[]u21 = null,
+
+ // Backing storage for Press.behaviors. The C API sets behaviors as a value
+ // struct, but SelectionGesture.Press stores a pointer to a [3]Behavior.
+ // Keep the array on the event wrapper so the Press payload can point at a
+ // stable location for the lifetime of the event.
+ behaviors: [3]Behavior = SelectionGesture.default_behaviors,
+
+ fn init(self: *EventWrapper, event_type: EventType) void {
+ self.event = switch (event_type) {
+ .press => .{ .press = self.defaultPress() },
+ .release => .{ .release = self.defaultRelease() },
+ .drag => .{ .drag = self.defaultDrag() },
+ .autoscroll_tick => .{ .autoscroll_tick = self.defaultAutoscrollTick() },
+ .deep_press => .{ .deep_press = self.defaultDeepPress() },
+ };
+ }
+
+ fn defaultPress(self: *EventWrapper) SelectionGesture.Press {
+ return .{
+ .time = null,
+ .pin = undefined,
+ .xpos = 0,
+ .ypos = 0,
+ .max_distance = 0,
+ .repeat_interval = 0,
+ .word_boundary_codepoints = &selection_codepoints.default_word_boundaries,
+ .behaviors = &self.behaviors,
+ };
+ }
+
+ fn defaultRelease(self: *EventWrapper) SelectionGesture.Release {
+ _ = self;
+ return .{ .pin = null };
+ }
+
+ fn defaultDrag(self: *EventWrapper) SelectionGesture.Drag {
+ _ = self;
+ return .{
+ .pin = undefined,
+ .xpos = 0,
+ .ypos = 0,
+ .rectangle = false,
+ .word_boundary_codepoints = &selection_codepoints.default_word_boundaries,
+ .geometry = undefined,
+ };
+ }
+
+ fn defaultAutoscrollTick(self: *EventWrapper) SelectionGesture.AutoscrollTick {
+ _ = self;
+ return .{
+ .viewport = undefined,
+ .xpos = 0,
+ .ypos = 0,
+ .rectangle = false,
+ .word_boundary_codepoints = &selection_codepoints.default_word_boundaries,
+ .geometry = undefined,
+ };
+ }
+
+ fn defaultDeepPress(self: *EventWrapper) SelectionGesture.DeepPress {
+ _ = self;
+ return .{
+ .word_boundary_codepoints = &selection_codepoints.default_word_boundaries,
+ };
+ }
+
+ fn deinit(self: *EventWrapper) void {
+ if (self.word_boundary_codepoints) |cps| {
+ if (cps.len > 0) self.alloc.free(cps);
+ }
+ }
+};
+
+/// C: GhosttySelectionGestureBehavior
+pub const Behavior = SelectionGesture.Behavior;
+
+/// C: GhosttySelectionGestureAutoscroll
+pub const Autoscroll = SelectionGesture.Autoscroll;
+
+/// C: GhosttySelectionGestureBehaviors
+pub const Behaviors = extern struct {
+ single_click: Behavior,
+ double_click: Behavior,
+ triple_click: Behavior,
+};
+
+/// C: GhosttySelectionGestureData
+pub const Data = enum(c_int) {
+ click_count = 0,
+ dragged = 1,
+ autoscroll = 2,
+ behavior = 3,
+ anchor = 4,
+
+ pub fn OutType(comptime self: Data) type {
+ return switch (self) {
+ .click_count => u8,
+ .dragged => bool,
+ .autoscroll => Autoscroll,
+ .behavior => Behavior,
+ .anchor => grid_ref.CGridRef,
+ };
+ }
+};
+
+/// C: GhosttySelectionGestureEventType
+pub const EventType = enum(c_int) {
+ press = 0,
+ release = 1,
+ drag = 2,
+ autoscroll_tick = 3,
+ deep_press = 4,
+};
+
+/// C: GhosttySelectionGestureEventOption
+pub const EventOption = enum(c_int) {
+ ref = 0,
+ position = 1,
+ repeat_distance = 2,
+ time_ns = 3,
+ repeat_interval_ns = 4,
+ word_boundary_codepoints = 5,
+ behaviors = 6,
+ rectangle = 7,
+ geometry = 8,
+ viewport = 9,
+
+ pub fn Type(comptime self: EventOption) type {
+ return switch (self) {
+ .ref => grid_ref.CGridRef,
+ .position => types.SurfacePosition,
+ .repeat_distance => f64,
+ .time_ns => u64,
+ .repeat_interval_ns => u64,
+ .word_boundary_codepoints => types.Codepoints,
+ .behaviors => Behaviors,
+ .rectangle => bool,
+ .geometry => Geometry,
+ .viewport => point.Coordinate,
+ };
+ }
+};
+
+/// C: GhosttySelectionGestureGeometry
+pub const Geometry = extern struct {
+ columns: u32,
+ cell_width: u32,
+ padding_left: u32,
+ screen_height: u32,
+
+ fn toZig(self: Geometry) ?SelectionGesture.Drag.Geometry {
+ if (self.columns == 0) return null;
+ if (self.cell_width == 0) return null;
+ if (self.screen_height == 0) return null;
+ return .{
+ .columns = self.columns,
+ .cell_width = self.cell_width,
+ .padding_left = self.padding_left,
+ .screen_height = self.screen_height,
+ };
+ }
+};
+
+pub fn new(
+ alloc_: ?*const CAllocator,
+ out_gesture: ?*Gesture,
+) callconv(lib.calling_conv) Result {
+ const out = out_gesture orelse return .invalid_value;
+
+ const alloc = lib.alloc.default(alloc_);
+ const gesture = alloc.create(GestureWrapper) catch {
+ out.* = null;
+ return .out_of_memory;
+ };
+ gesture.* = .{
+ .alloc = alloc,
+ };
+ out.* = gesture;
+ return .success;
+}
+
+pub fn event_new(
+ alloc_: ?*const CAllocator,
+ out_event: ?*Event,
+ event_type: EventType,
+) callconv(lib.calling_conv) Result {
+ const out = out_event orelse return .invalid_value;
+ _ = std.meta.intToEnum(EventType, @intFromEnum(event_type)) catch
+ return .invalid_value;
+
+ const alloc = lib.alloc.default(alloc_);
+ const event = alloc.create(EventWrapper) catch {
+ out.* = null;
+ return .out_of_memory;
+ };
+ event.* = .{
+ .alloc = alloc,
+ .event = undefined,
+ };
+ event.init(event_type);
+ out.* = event;
+ return .success;
+}
+
+pub fn free(
+ gesture_: Gesture,
+ terminal: terminal_c.Terminal,
+) callconv(lib.calling_conv) void {
+ const wrapper = gesture_ orelse return;
+ if (terminal_c.zigTerminal(terminal)) |t| {
+ wrapper.gesture.deinit(t);
+ }
+ const alloc = wrapper.alloc;
+ alloc.destroy(wrapper);
+}
+
+pub fn event_free(event_: Event) callconv(lib.calling_conv) void {
+ const event = event_ orelse return;
+ event.deinit();
+ const alloc = event.alloc;
+ alloc.destroy(event);
+}
+
+pub fn reset(
+ gesture_: Gesture,
+ terminal: terminal_c.Terminal,
+) callconv(lib.calling_conv) void {
+ const wrapper = gesture_ orelse return;
+ const t = terminal_c.zigTerminal(terminal) orelse return;
+ 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.event_validation.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;
+ },
+ .release => |release| {
+ wrapper.gesture.release(t, release);
+ return .no_value;
+ },
+ .drag => |drag| {
+ if (!event_wrapper.event_validation.drag_pin_set) return .invalid_value;
+ if (!event_wrapper.event_validation.drag_geometry_set) return .invalid_value;
+ const sel = wrapper.gesture.drag(t, drag);
+ if (out_selection) |out| {
+ out.* = selection_c.CSelection.fromZig(sel orelse return .no_value);
+ } else if (sel == null) return .no_value;
+ return .success;
+ },
+ .autoscroll_tick => |tick| {
+ if (!event_wrapper.event_validation.autoscroll_tick_viewport_set) return .invalid_value;
+ if (!event_wrapper.event_validation.autoscroll_tick_geometry_set) return .invalid_value;
+ const sel = wrapper.gesture.autoscrollTick(t, tick);
+ if (out_selection) |out| {
+ out.* = selection_c.CSelection.fromZig(sel orelse return .no_value);
+ } else if (sel == null) return .no_value;
+ return .success;
+ },
+ .deep_press => |deep_press| {
+ const sel = wrapper.gesture.deepPress(t, deep_press);
+ 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,
+ value: ?*const anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(EventOption, @intFromEnum(option)) catch {
+ log.warn("selection_gesture_event_set invalid option value={d}", .{@intFromEnum(option)});
+ return .invalid_value;
+ };
+ }
+
+ return switch (option) {
+ inline else => |comptime_option| eventSetTyped(
+ event_,
+ comptime_option,
+ if (value) |ptr| @ptrCast(@alignCast(ptr)) else null,
+ ),
+ };
+}
+
+pub fn get(
+ gesture_: Gesture,
+ terminal: terminal_c.Terminal,
+ data: Data,
+ out: ?*anyopaque,
+) callconv(lib.calling_conv) Result {
+ if (comptime std.debug.runtime_safety) {
+ _ = std.meta.intToEnum(Data, @intFromEnum(data)) catch {
+ log.warn("selection_gesture_get invalid data value={d}", .{@intFromEnum(data)});
+ return .invalid_value;
+ };
+ }
+
+ const out_ptr = out orelse return .invalid_value;
+ return switch (data) {
+ inline else => |comptime_data| getTyped(
+ gesture_,
+ terminal,
+ comptime_data,
+ @ptrCast(@alignCast(out_ptr)),
+ ),
+ };
+}
+
+pub fn get_multi(
+ gesture_: Gesture,
+ terminal: terminal_c.Terminal,
+ count: usize,
+ keys: ?[*]const Data,
+ values: ?[*]?*anyopaque,
+ out_written: ?*usize,
+) callconv(lib.calling_conv) Result {
+ const k = keys orelse return .invalid_value;
+ const v = values orelse return .invalid_value;
+
+ for (0..count) |i| {
+ const result = get(gesture_, terminal, k[i], v[i]);
+ if (result != .success) {
+ if (out_written) |w| w.* = i;
+ return result;
+ }
+ }
+ if (out_written) |w| w.* = count;
+ return .success;
+}
+
+fn getTyped(
+ gesture_: Gesture,
+ terminal: terminal_c.Terminal,
+ comptime data: Data,
+ out: *data.OutType(),
+) Result {
+ const wrapper = gesture_ orelse return .invalid_value;
+ const t = terminal_c.zigTerminal(terminal) orelse return .invalid_value;
+
+ switch (data) {
+ .click_count => out.* = wrapper.gesture.left_click_count,
+ .dragged => out.* = wrapper.gesture.left_click_dragged,
+ .autoscroll => out.* = wrapper.gesture.left_drag_autoscroll,
+ .behavior => out.* = wrapper.gesture.left_click_behavior,
+ .anchor => {
+ const pin = wrapper.gesture.validatedLeftClickPin(&t.screens) orelse
+ return .no_value;
+ out.* = .fromPin(pin.*);
+ },
+ }
+
+ return .success;
+}
+
+fn eventSetTyped(
+ event_: Event,
+ comptime option: EventOption,
+ value: ?*const option.Type(),
+) Result {
+ const event = event_ orelse return .invalid_value;
+ return switch (event.event) {
+ .press => |*press| pressSetTyped(event, press, option, value),
+ .release => |*release| releaseSetTyped(release, option, value),
+ .drag => |*drag| dragSetTyped(event, drag, option, value),
+ .autoscroll_tick => |*tick| autoscrollTickSetTyped(event, tick, option, value),
+ .deep_press => |*deep_press| deepPressSetTyped(event, deep_press, option, value),
+ };
+}
+
+fn pressSetTyped(
+ event: *EventWrapper,
+ press: *SelectionGesture.Press,
+ comptime option: EventOption,
+ value: ?*const option.Type(),
+) Result {
+ const v = value orelse {
+ switch (option) {
+ .ref => event.event_validation.press_pin_set = false,
+ .position => {
+ press.xpos = 0;
+ press.ypos = 0;
+ },
+ .repeat_distance => press.max_distance = 0,
+ .time_ns => press.time = null,
+ .repeat_interval_ns => press.repeat_interval = 0,
+ .word_boundary_codepoints => clearWordBoundaryCodepoints(
+ event,
+ &press.word_boundary_codepoints,
+ ),
+ .behaviors => {
+ event.behaviors = SelectionGesture.default_behaviors;
+ press.behaviors = &event.behaviors;
+ },
+ .rectangle,
+ .geometry,
+ .viewport,
+ => return .invalid_value,
+ }
+ return .success;
+ };
+
+ switch (option) {
+ .ref => {
+ press.pin = v.toPin() orelse return .invalid_value;
+ event.event_validation.press_pin_set = true;
+ },
+ .position => {
+ press.xpos = v.x;
+ press.ypos = v.y;
+ },
+ .repeat_distance => press.max_distance = v.*,
+ .time_ns => press.time = instantFromNs(v.*),
+ .repeat_interval_ns => press.repeat_interval = v.*,
+ .word_boundary_codepoints => return trySetWordBoundaryCodepoints(
+ event,
+ &press.word_boundary_codepoints,
+ v,
+ ),
+ .behaviors => {
+ if (!validBehavior(v.single_click) or
+ !validBehavior(v.double_click) or
+ !validBehavior(v.triple_click)) return .invalid_value;
+ event.behaviors = .{ v.single_click, v.double_click, v.triple_click };
+ press.behaviors = &event.behaviors;
+ },
+ .rectangle,
+ .geometry,
+ .viewport,
+ => return .invalid_value,
+ }
+
+ return .success;
+}
+
+fn releaseSetTyped(
+ release: *SelectionGesture.Release,
+ comptime option: EventOption,
+ value: ?*const option.Type(),
+) Result {
+ switch (option) {
+ .ref => {
+ const v = value orelse {
+ release.pin = null;
+ return .success;
+ };
+ release.pin = v.toPin() orelse return .invalid_value;
+ },
+
+ .position,
+ .repeat_distance,
+ .time_ns,
+ .repeat_interval_ns,
+ .word_boundary_codepoints,
+ .behaviors,
+ .rectangle,
+ .geometry,
+ .viewport,
+ => return .invalid_value,
+ }
+
+ return .success;
+}
+
+fn dragSetTyped(
+ event: *EventWrapper,
+ drag: *SelectionGesture.Drag,
+ comptime option: EventOption,
+ value: ?*const option.Type(),
+) Result {
+ const v = value orelse {
+ switch (option) {
+ .ref => event.event_validation.drag_pin_set = false,
+ .position => {
+ drag.xpos = 0;
+ drag.ypos = 0;
+ },
+ .word_boundary_codepoints => clearWordBoundaryCodepoints(
+ event,
+ &drag.word_boundary_codepoints,
+ ),
+ .rectangle => drag.rectangle = false,
+ .geometry => event.event_validation.drag_geometry_set = false,
+ .viewport => return .invalid_value,
+
+ .repeat_distance,
+ .time_ns,
+ .repeat_interval_ns,
+ .behaviors,
+ => return .invalid_value,
+ }
+ return .success;
+ };
+
+ switch (option) {
+ .ref => {
+ drag.pin = v.toPin() orelse return .invalid_value;
+ event.event_validation.drag_pin_set = true;
+ },
+ .position => {
+ drag.xpos = v.x;
+ drag.ypos = v.y;
+ },
+ .word_boundary_codepoints => return trySetWordBoundaryCodepoints(
+ event,
+ &drag.word_boundary_codepoints,
+ v,
+ ),
+ .rectangle => drag.rectangle = v.*,
+ .geometry => {
+ drag.geometry = v.toZig() orelse return .invalid_value;
+ event.event_validation.drag_geometry_set = true;
+ },
+ .viewport => return .invalid_value,
+
+ .repeat_distance,
+ .time_ns,
+ .repeat_interval_ns,
+ .behaviors,
+ => return .invalid_value,
+ }
+
+ return .success;
+}
+
+fn autoscrollTickSetTyped(
+ event: *EventWrapper,
+ tick: *SelectionGesture.AutoscrollTick,
+ comptime option: EventOption,
+ value: ?*const option.Type(),
+) Result {
+ const v = value orelse {
+ switch (option) {
+ .viewport => event.event_validation.autoscroll_tick_viewport_set = false,
+ .position => {
+ tick.xpos = 0;
+ tick.ypos = 0;
+ },
+ .word_boundary_codepoints => clearWordBoundaryCodepoints(
+ event,
+ &tick.word_boundary_codepoints,
+ ),
+ .rectangle => tick.rectangle = false,
+ .geometry => event.event_validation.autoscroll_tick_geometry_set = false,
+
+ .ref,
+ .repeat_distance,
+ .time_ns,
+ .repeat_interval_ns,
+ .behaviors,
+ => return .invalid_value,
+ }
+ return .success;
+ };
+
+ switch (option) {
+ .viewport => {
+ tick.viewport = v.*;
+ event.event_validation.autoscroll_tick_viewport_set = true;
+ },
+ .position => {
+ tick.xpos = v.x;
+ tick.ypos = v.y;
+ },
+ .word_boundary_codepoints => return trySetWordBoundaryCodepoints(
+ event,
+ &tick.word_boundary_codepoints,
+ v,
+ ),
+ .rectangle => tick.rectangle = v.*,
+ .geometry => {
+ tick.geometry = v.toZig() orelse return .invalid_value;
+ event.event_validation.autoscroll_tick_geometry_set = true;
+ },
+
+ .ref,
+ .repeat_distance,
+ .time_ns,
+ .repeat_interval_ns,
+ .behaviors,
+ => return .invalid_value,
+ }
+
+ return .success;
+}
+
+fn deepPressSetTyped(
+ event: *EventWrapper,
+ deep_press: *SelectionGesture.DeepPress,
+ comptime option: EventOption,
+ value: ?*const option.Type(),
+) Result {
+ const v = value orelse {
+ switch (option) {
+ .word_boundary_codepoints => clearWordBoundaryCodepoints(
+ event,
+ &deep_press.word_boundary_codepoints,
+ ),
+
+ .ref,
+ .position,
+ .repeat_distance,
+ .time_ns,
+ .repeat_interval_ns,
+ .behaviors,
+ .rectangle,
+ .geometry,
+ .viewport,
+ => return .invalid_value,
+ }
+ return .success;
+ };
+
+ switch (option) {
+ .word_boundary_codepoints => return trySetWordBoundaryCodepoints(
+ event,
+ &deep_press.word_boundary_codepoints,
+ v,
+ ),
+
+ .ref,
+ .position,
+ .repeat_distance,
+ .time_ns,
+ .repeat_interval_ns,
+ .behaviors,
+ .rectangle,
+ .geometry,
+ .viewport,
+ => return .invalid_value,
+ }
+
+ return .success;
+}
+
+fn trySetWordBoundaryCodepoints(
+ event: *EventWrapper,
+ target: *[]const u21,
+ value: *const types.Codepoints,
+) Result {
+ if (value.len > 0 and value.ptr == null) return .invalid_value;
+ clearWordBoundaryCodepoints(event, target);
+ const ptr = value.ptr orelse {
+ event.word_boundary_codepoints = &.{};
+ target.* = event.word_boundary_codepoints.?;
+ return .success;
+ };
+ const copy = event.alloc.alloc(u21, value.len) catch return .out_of_memory;
+ errdefer event.alloc.free(copy);
+ for (copy, ptr[0..value.len]) |*dst, cp| {
+ dst.* = std.math.cast(u21, cp) orelse return .invalid_value;
+ }
+ event.word_boundary_codepoints = copy;
+ target.* = copy;
+ return .success;
+}
+
+fn clearWordBoundaryCodepoints(event: *EventWrapper, target: *[]const u21) void {
+ if (event.word_boundary_codepoints) |cps| {
+ if (cps.len > 0) event.alloc.free(cps);
+ }
+ event.word_boundary_codepoints = null;
+ target.* = &selection_codepoints.default_word_boundaries;
+}
+
+fn instantFromNs(ns: u64) SelectionGesture.Time {
+ if (comptime builtin.target.cpu.arch == .wasm32 and
+ builtin.target.os.tag == .freestanding)
+ {
+ return ns;
+ }
+
+ return switch (builtin.os.tag) {
+ .windows, .uefi, .wasi => .{ .timestamp = ns },
+ else => .{ .timestamp = .{
+ .sec = @intCast(ns / std.time.ns_per_s),
+ .nsec = @intCast(ns % std.time.ns_per_s),
+ } },
+ };
+}
+
+fn validBehavior(behavior: Behavior) bool {
+ _ = std.meta.intToEnum(Behavior, @intFromEnum(behavior)) catch return false;
+ return true;
+}
+
+test "selection gesture lifecycle and get" {
+ 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 click_count: u8 = 255;
+ try testing.expectEqual(Result.success, get(gesture, terminal, .click_count, &click_count));
+ try testing.expectEqual(@as(u8, 0), click_count);
+
+ var dragged = true;
+ try testing.expectEqual(Result.success, get(gesture, terminal, .dragged, &dragged));
+ try testing.expect(!dragged);
+
+ var autoscroll: Autoscroll = .up;
+ try testing.expectEqual(Result.success, get(gesture, terminal, .autoscroll, &autoscroll));
+ try testing.expectEqual(Autoscroll.none, autoscroll);
+
+ var behavior: Behavior = .word;
+ try testing.expectEqual(Result.success, get(gesture, terminal, .behavior, &behavior));
+ try testing.expectEqual(Behavior.cell, behavior);
+
+ var anchor: grid_ref.CGridRef = undefined;
+ try testing.expectEqual(Result.no_value, get(gesture, terminal, .anchor, &anchor));
+}
+
+test "selection gesture get_multi" {
+ 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);
+
+ const keys = [_]Data{ .click_count, .dragged, .autoscroll, .behavior };
+ var click_count: u8 = 255;
+ var dragged = true;
+ var autoscroll: Autoscroll = .up;
+ var behavior: Behavior = .word;
+ var values = [_]?*anyopaque{
+ &click_count,
+ &dragged,
+ &autoscroll,
+ &behavior,
+ };
+ var written: usize = 0;
+
+ try testing.expectEqual(Result.success, get_multi(
+ gesture,
+ terminal,
+ keys.len,
+ &keys,
+ &values,
+ &written,
+ ));
+ try testing.expectEqual(keys.len, written);
+ try testing.expectEqual(@as(u8, 0), click_count);
+ try testing.expect(!dragged);
+ try testing.expectEqual(Autoscroll.none, autoscroll);
+ try testing.expectEqual(Behavior.cell, behavior);
+}
+
+test "selection gesture get_multi returns first failing index" {
+ 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);
+
+ const keys = [_]Data{ .click_count, .anchor, .dragged };
+ var click_count: u8 = 255;
+ var anchor: grid_ref.CGridRef = undefined;
+ var dragged = true;
+ var values = [_]?*anyopaque{ &click_count, &anchor, &dragged };
+ var written: usize = 0;
+
+ try testing.expectEqual(Result.no_value, get_multi(
+ gesture,
+ terminal,
+ keys.len,
+ &keys,
+ &values,
+ &written,
+ ));
+ try testing.expectEqual(@as(usize, 1), written);
+ try testing.expectEqual(@as(u8, 0), click_count);
+ try testing.expect(dragged);
+}
+
+test "selection gesture event set clear and free" {
+ var event: Event = null;
+ try testing.expectEqual(Result.success, event_new(
+ &lib.alloc.test_allocator,
+ &event,
+ .press,
+ ));
+ defer event_free(event);
+
+ const in_pos: types.SurfacePosition = .{ .x = 12.5, .y = -3.25 };
+ try testing.expectEqual(Result.success, event_set(event, .position, &in_pos));
+ try testing.expectEqual(@as(f64, 12.5), event.?.event.press.xpos);
+ try testing.expectEqual(@as(f64, -3.25), event.?.event.press.ypos);
+
+ try testing.expectEqual(Result.success, event_set(event, .position, null));
+ try testing.expectEqual(@as(f64, 0), event.?.event.press.xpos);
+ try testing.expectEqual(@as(f64, 0), event.?.event.press.ypos);
+
+ const repeat_distance: f64 = 4.0;
+ try testing.expectEqual(Result.success, event_set(event, .repeat_distance, &repeat_distance));
+ try testing.expectEqual(repeat_distance, event.?.event.press.max_distance);
+}
+
+test "selection gesture event copies clears and frees codepoints" {
+ var event: Event = null;
+ try testing.expectEqual(Result.success, event_new(
+ &lib.alloc.test_allocator,
+ &event,
+ .press,
+ ));
+ defer event_free(event);
+
+ var values = [_]u32{ ' ', '\t' };
+ const in: types.Codepoints = .{ .ptr = &values, .len = values.len };
+ try testing.expectEqual(Result.success, event_set(event, .word_boundary_codepoints, &in));
+
+ values[0] = 'x';
+
+ try testing.expectEqual(@as(usize, 2), event.?.event.press.word_boundary_codepoints.len);
+ try testing.expectEqual(@as(u21, ' '), event.?.event.press.word_boundary_codepoints[0]);
+ try testing.expectEqual(@as(u21, '\t'), event.?.event.press.word_boundary_codepoints[1]);
+
+ const invalid: types.Codepoints = .{ .ptr = null, .len = 1 };
+ try testing.expectEqual(Result.invalid_value, event_set(event, .word_boundary_codepoints, &invalid));
+
+ try testing.expectEqual(Result.success, event_set(event, .word_boundary_codepoints, null));
+ try testing.expectEqual(
+ selection_codepoints.default_word_boundaries.len,
+ event.?.event.press.word_boundary_codepoints.len,
+ );
+
+ const empty: types.Codepoints = .{ .ptr = null, .len = 0 };
+ try testing.expectEqual(Result.success, event_set(event, .word_boundary_codepoints, &empty));
+ try testing.expectEqual(@as(usize, 0), event.?.event.press.word_boundary_codepoints.len);
+}
+
+test "selection gesture event behaviors" {
+ var event: Event = null;
+ try testing.expectEqual(Result.success, event_new(
+ &lib.alloc.test_allocator,
+ &event,
+ .press,
+ ));
+ defer event_free(event);
+
+ const in: Behaviors = .{
+ .single_click = .cell,
+ .double_click = .word,
+ .triple_click = .line,
+ };
+ try testing.expectEqual(Result.success, event_set(event, .behaviors, &in));
+ try testing.expectEqual(Behavior.cell, event.?.event.press.behaviors[0]);
+ try testing.expectEqual(Behavior.word, event.?.event.press.behaviors[1]);
+ 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 event applies release" {
+ 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 release_event: Event = null;
+ try testing.expectEqual(Result.success, event_new(
+ &lib.alloc.test_allocator,
+ &release_event,
+ .release,
+ ));
+ defer event_free(release_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.success, event_set(release_event, .ref, &ref));
+
+ try testing.expectEqual(Result.no_value, handle_event(gesture, terminal, press_event, null));
+ try testing.expectEqual(Result.no_value, handle_event(gesture, terminal, release_event, null));
+
+ var dragged = true;
+ try testing.expectEqual(Result.success, get(gesture, terminal, .dragged, &dragged));
+ try testing.expect(!dragged);
+
+ const pos: types.SurfacePosition = .{ .x = 0, .y = 0 };
+ try testing.expectEqual(Result.invalid_value, event_set(release_event, .position, &pos));
+}
+
+test "selection gesture release without ref marks dragged" {
+ 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 release_event: Event = null;
+ try testing.expectEqual(Result.success, event_new(
+ &lib.alloc.test_allocator,
+ &release_event,
+ .release,
+ ));
+ defer event_free(release_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));
+ try testing.expectEqual(Result.no_value, handle_event(gesture, terminal, release_event, null));
+
+ var dragged = false;
+ try testing.expectEqual(Result.success, get(gesture, terminal, .dragged, &dragged));
+ try testing.expect(dragged);
+}
+
+test "selection gesture event applies drag" {
+ 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);
+
+ terminal_c.vt_write(terminal, "abcde", 5);
+
+ 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 drag_event: Event = null;
+ try testing.expectEqual(Result.success, event_new(
+ &lib.alloc.test_allocator,
+ &drag_event,
+ .drag,
+ ));
+ defer event_free(drag_event);
+
+ var press_ref: grid_ref.CGridRef = undefined;
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(terminal, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 1, .y = 0 } },
+ }, &press_ref));
+ try testing.expectEqual(Result.success, event_set(press_event, .ref, &press_ref));
+
+ const press_pos: types.SurfacePosition = .{ .x = 10, .y = 10 };
+ try testing.expectEqual(Result.success, event_set(press_event, .position, &press_pos));
+ try testing.expectEqual(Result.no_value, handle_event(gesture, terminal, press_event, null));
+
+ var drag_ref: grid_ref.CGridRef = undefined;
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(terminal, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 3, .y = 0 } },
+ }, &drag_ref));
+ try testing.expectEqual(Result.success, event_set(drag_event, .ref, &drag_ref));
+
+ const drag_pos: types.SurfacePosition = .{ .x = 36, .y = 10 };
+ try testing.expectEqual(Result.success, event_set(drag_event, .position, &drag_pos));
+ const geometry: Geometry = .{
+ .columns = 5,
+ .cell_width = 10,
+ .padding_left = 0,
+ .screen_height = 20,
+ };
+ try testing.expectEqual(Result.success, event_set(drag_event, .geometry, &geometry));
+
+ var sel: selection_c.CSelection = undefined;
+ try testing.expectEqual(Result.success, handle_event(gesture, terminal, drag_event, &sel));
+ try testing.expectEqual(@as(u16, 1), sel.start.toPin().?.x);
+ try testing.expectEqual(@as(u16, 3), sel.end.toPin().?.x);
+
+ var dragged = false;
+ try testing.expectEqual(Result.success, get(gesture, terminal, .dragged, &dragged));
+ try testing.expect(dragged);
+}
+
+test "selection gesture drag requires ref and geometry" {
+ 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 drag_event: Event = null;
+ try testing.expectEqual(Result.success, event_new(
+ &lib.alloc.test_allocator,
+ &drag_event,
+ .drag,
+ ));
+ defer event_free(drag_event);
+
+ var sel: selection_c.CSelection = undefined;
+ try testing.expectEqual(Result.invalid_value, handle_event(gesture, terminal, drag_event, &sel));
+
+ 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(drag_event, .ref, &ref));
+ try testing.expectEqual(Result.invalid_value, handle_event(gesture, terminal, drag_event, &sel));
+
+ const invalid_geometry: Geometry = .{
+ .columns = 5,
+ .cell_width = 0,
+ .padding_left = 0,
+ .screen_height = 20,
+ };
+ try testing.expectEqual(Result.invalid_value, event_set(drag_event, .geometry, &invalid_geometry));
+}
+
+test "selection gesture event applies autoscroll tick" {
+ 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);
+
+ terminal_c.vt_write(terminal, "abcde\r\nfghij", 12);
+
+ 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 drag_event: Event = null;
+ try testing.expectEqual(Result.success, event_new(
+ &lib.alloc.test_allocator,
+ &drag_event,
+ .drag,
+ ));
+ defer event_free(drag_event);
+
+ var tick_event: Event = null;
+ try testing.expectEqual(Result.success, event_new(
+ &lib.alloc.test_allocator,
+ &tick_event,
+ .autoscroll_tick,
+ ));
+ defer event_free(tick_event);
+
+ const geometry: Geometry = .{
+ .columns = 5,
+ .cell_width = 10,
+ .padding_left = 0,
+ .screen_height = 20,
+ };
+
+ var press_ref: grid_ref.CGridRef = undefined;
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(terminal, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 1, .y = 0 } },
+ }, &press_ref));
+ try testing.expectEqual(Result.success, event_set(press_event, .ref, &press_ref));
+ const press_pos: types.SurfacePosition = .{ .x = 10, .y = 10 };
+ try testing.expectEqual(Result.success, event_set(press_event, .position, &press_pos));
+ try testing.expectEqual(Result.no_value, handle_event(gesture, terminal, press_event, null));
+
+ var drag_ref: grid_ref.CGridRef = undefined;
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(terminal, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 3, .y = 1 } },
+ }, &drag_ref));
+ try testing.expectEqual(Result.success, event_set(drag_event, .ref, &drag_ref));
+ const drag_pos: types.SurfacePosition = .{ .x = 36, .y = 20 };
+ try testing.expectEqual(Result.success, event_set(drag_event, .position, &drag_pos));
+ try testing.expectEqual(Result.success, event_set(drag_event, .geometry, &geometry));
+ var sel: selection_c.CSelection = undefined;
+ try testing.expectEqual(Result.success, handle_event(gesture, terminal, drag_event, &sel));
+
+ var autoscroll: Autoscroll = .none;
+ try testing.expectEqual(Result.success, get(gesture, terminal, .autoscroll, &autoscroll));
+ try testing.expectEqual(Autoscroll.down, autoscroll);
+
+ const viewport: point.Coordinate = .{ .x = 3, .y = 1 };
+ try testing.expectEqual(Result.success, event_set(tick_event, .viewport, &viewport));
+ try testing.expectEqual(Result.success, event_set(tick_event, .position, &drag_pos));
+ try testing.expectEqual(Result.success, event_set(tick_event, .geometry, &geometry));
+
+ try testing.expectEqual(Result.success, handle_event(gesture, terminal, tick_event, &sel));
+}
+
+test "selection gesture autoscroll tick requires viewport and geometry" {
+ 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 tick_event: Event = null;
+ try testing.expectEqual(Result.success, event_new(
+ &lib.alloc.test_allocator,
+ &tick_event,
+ .autoscroll_tick,
+ ));
+ defer event_free(tick_event);
+
+ var sel: selection_c.CSelection = undefined;
+ try testing.expectEqual(Result.invalid_value, handle_event(gesture, terminal, tick_event, &sel));
+
+ const viewport: point.Coordinate = .{ .x = 1, .y = 0 };
+ try testing.expectEqual(Result.success, event_set(tick_event, .viewport, &viewport));
+ try testing.expectEqual(Result.invalid_value, handle_event(gesture, terminal, tick_event, &sel));
+
+ 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.invalid_value, event_set(tick_event, .ref, &ref));
+}
+
+test "selection gesture event applies deep 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);
+
+ terminal_c.vt_write(terminal, "abcde", 5);
+
+ 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 deep_press_event: Event = null;
+ try testing.expectEqual(Result.success, event_new(
+ &lib.alloc.test_allocator,
+ &deep_press_event,
+ .deep_press,
+ ));
+ defer event_free(deep_press_event);
+
+ var ref: grid_ref.CGridRef = undefined;
+ try testing.expectEqual(Result.success, terminal_c.grid_ref(terminal, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 2, .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));
+
+ var sel: selection_c.CSelection = undefined;
+ try testing.expectEqual(Result.success, handle_event(gesture, terminal, deep_press_event, &sel));
+ try testing.expectEqual(@as(u16, 0), sel.start.toPin().?.x);
+ try testing.expectEqual(@as(u16, 4), sel.end.toPin().?.x);
+
+ var dragged = false;
+ try testing.expectEqual(Result.success, get(gesture, terminal, .dragged, &dragged));
+ try testing.expect(dragged);
+
+ const pos: types.SurfacePosition = .{ .x = 0, .y = 0 };
+ try testing.expectEqual(Result.invalid_value, event_set(deep_press_event, .position, &pos));
+}
+
+test "selection gesture deep press without active anchor returns no value" {
+ 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 deep_press_event: Event = null;
+ try testing.expectEqual(Result.success, event_new(
+ &lib.alloc.test_allocator,
+ &deep_press_event,
+ .deep_press,
+ ));
+ defer event_free(deep_press_event);
+
+ var sel: selection_c.CSelection = undefined;
+ try testing.expectEqual(Result.no_value, handle_event(gesture, terminal, deep_press_event, &sel));
+}
+
+test "selection gesture free null" {
+ free(null, null);
+}
+
+test "selection gesture event free null" {
+ event_free(null);
+}
diff --git a/vendor/libghostty-vt/src/terminal/c/terminal.zig b/vendor/libghostty-vt/src/terminal/c/terminal.zig
index d3293f3b..5a5db2d6 100644
--- a/vendor/libghostty-vt/src/terminal/c/terminal.zig
+++ b/vendor/libghostty-vt/src/terminal/c/terminal.zig
@@ -3,7 +3,7 @@ const testing = std.testing;
const build_options = @import("terminal_options");
const lib = @import("../lib.zig");
const CAllocator = lib.alloc.Allocator;
-const ZigTerminal = @import("../Terminal.zig");
+pub const ZigTerminal = @import("../Terminal.zig");
const Stream = @import("../stream_terminal.zig").Stream;
const ScreenSet = @import("../ScreenSet.zig");
const PageList = @import("../PageList.zig");
@@ -19,6 +19,8 @@ const size_report = @import("../size_report.zig");
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;
@@ -34,6 +36,7 @@ const TerminalWrapper = struct {
terminal: *ZigTerminal,
stream: Stream,
effects: Effects = .{},
+ tracked_grid_refs: std.AutoArrayHashMapUnmanaged(*grid_ref_tracked_c.TrackedGridRef, void) = .{},
};
/// C callback state for terminal effects. Trampolines are always
@@ -208,6 +211,10 @@ const Effects = struct {
/// C: GhosttyTerminal
pub const Terminal = ?*TerminalWrapper;
+pub fn zigTerminal(terminal_: Terminal) ?*ZigTerminal {
+ return (terminal_ orelse return null).terminal;
+}
+
/// C: GhosttyTerminalOptions
pub const Options = extern struct {
cols: size.CellCountInt,
@@ -259,6 +266,11 @@ fn new_(
});
errdefer t.deinit(alloc);
+ // libghostty-vt embedders don't necessarily install Ghostty's shell
+ // integration, so don't assume OSC 133 prompts can be redrawn on resize.
+ // Shells can still opt in with OSC 133;A;redraw=1.
+ t.flags.shell_redraws_prompt = .false;
+
// Setup our stream with trampolines always installed so that
// setting C callbacks at any time takes effect immediately.
var handler: Stream.Handler = t.vtHandler();
@@ -313,6 +325,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 {
@@ -335,6 +348,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,
};
}
};
@@ -442,6 +456,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;
}
@@ -575,13 +597,15 @@ pub const TerminalData = enum(c_int) {
kitty_image_medium_temp_file = 28,
kitty_image_medium_shared_mem = 29,
kitty_graphics = 30,
+ selection = 31,
+ viewport_active = 32,
/// Output type expected for querying the data of the given kind.
pub fn OutType(comptime self: TerminalData) type {
return switch (self) {
.invalid => void,
.cols, .rows, .cursor_x, .cursor_y => size.CellCountInt,
- .cursor_pending_wrap, .cursor_visible, .mouse_tracking => bool,
+ .cursor_pending_wrap, .cursor_visible, .mouse_tracking, .viewport_active => bool,
.active_screen => TerminalScreen,
.kitty_keyboard_flags => u8,
.scrollbar => TerminalScrollbar,
@@ -603,6 +627,7 @@ pub const TerminalData = enum(c_int) {
.kitty_image_medium_shared_mem,
=> bool,
.kitty_graphics => KittyGraphics,
+ .selection => selection_c.CSelection,
};
}
};
@@ -712,6 +737,10 @@ 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,
+ ),
+ .viewport_active => out.* = t.screens.active.pages.viewport == .active,
}
return .success;
@@ -723,18 +752,56 @@ pub fn grid_ref(
out_ref: ?*grid_ref_c.CGridRef,
) callconv(lib.calling_conv) Result {
const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal;
- const zig_pt: point.Point = switch (pt.tag) {
- .active => .{ .active = pt.value.active },
- .viewport => .{ .viewport = pt.value.viewport },
- .screen => .{ .screen = pt.value.screen },
- .history => .{ .history = pt.value.history },
- };
+ const zig_pt: point.Point = .fromC(pt);
const p = t.screens.active.pages.pin(zig_pt) orelse
return .invalid_value;
if (out_ref) |out| out.* = grid_ref_c.CGridRef.fromPin(p);
return .success;
}
+pub fn grid_ref_track(
+ terminal_: Terminal,
+ pt: point.Point.C,
+ out_ref: ?*grid_ref_tracked_c.CTrackedGridRef,
+) callconv(lib.calling_conv) Result {
+ const wrapper = terminal_ orelse return .invalid_value;
+ const out = out_ref orelse return .invalid_value;
+ out.* = null;
+
+ const t: *ZigTerminal = wrapper.terminal;
+ const list = &t.screens.active.pages;
+ const p = list.pin(.fromC(pt)) orelse return .invalid_value;
+ const tracked_pin = list.trackPin(p) catch return .out_of_memory;
+
+ const alloc = t.gpa();
+ const ref = alloc.create(grid_ref_tracked_c.TrackedGridRef) catch {
+ list.untrackPin(tracked_pin);
+ return .out_of_memory;
+ };
+ ref.* = .{
+ .alloc = alloc,
+ .terminal = wrapper,
+ .screen_key = t.screens.active_key,
+ .screen_generation = t.screens.generation(t.screens.active_key),
+ .pin = tracked_pin,
+ };
+
+ // Store the tracked ref in the terminal so that when we free
+ // the terminal the tracked ref can be detached safely.
+ wrapper.tracked_grid_refs.putNoClobber(
+ alloc,
+ ref,
+ {},
+ ) catch {
+ list.untrackPin(tracked_pin);
+ alloc.destroy(ref);
+ return .out_of_memory;
+ };
+
+ out.* = ref;
+ return .success;
+}
+
pub fn point_from_grid_ref(
terminal_: Terminal,
ref: *const grid_ref_c.CGridRef,
@@ -752,9 +819,11 @@ pub fn point_from_grid_ref(
pub fn free(terminal_: Terminal) callconv(lib.calling_conv) void {
const wrapper = terminal_ orelse return;
const t = wrapper.terminal;
+ const alloc = t.gpa();
+ for (wrapper.tracked_grid_refs.keys()) |ref| ref.terminal = null;
+ wrapper.tracked_grid_refs.deinit(alloc);
wrapper.stream.deinit();
- const alloc = t.gpa();
t.deinit(alloc);
alloc.destroy(t);
alloc.destroy(wrapper);
@@ -821,6 +890,10 @@ test "scroll_viewport" {
const zt = t.?.terminal;
+ var viewport_active: bool = false;
+ try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active)));
+ try testing.expect(viewport_active);
+
// Write "hello" on the first line
vt_write(t, "hello", 5);
@@ -835,6 +908,8 @@ test "scroll_viewport" {
// Scroll to top: "hello" should be visible again
scroll_viewport(t, .{ .tag = .top, .value = undefined });
+ try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active)));
+ try testing.expect(!viewport_active);
{
const str = try zt.plainString(testing.allocator);
defer testing.allocator.free(str);
@@ -843,6 +918,8 @@ test "scroll_viewport" {
// Scroll to bottom: viewport should be empty again
scroll_viewport(t, .{ .tag = .bottom, .value = undefined });
+ try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active)));
+ try testing.expect(viewport_active);
{
const str = try zt.plainString(testing.allocator);
defer testing.allocator.free(str);
@@ -851,6 +928,8 @@ test "scroll_viewport" {
// Scroll up by delta to bring "hello" back into view
scroll_viewport(t, .{ .tag = .delta, .value = .{ .delta = -3 } });
+ try testing.expectEqual(Result.success, get(t, .viewport_active, @ptrCast(&viewport_active)));
+ try testing.expect(!viewport_active);
{
const str = try zt.plainString(testing.allocator);
defer testing.allocator.free(str);
@@ -1298,6 +1377,410 @@ 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 "selection derivation helpers" {
+ 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 \r\nWorld", 16);
+
+ var out: selection_c.CSelection = undefined;
+
+ var word_ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.success, grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 3, .y = 0 } },
+ }, &word_ref));
+
+ var empty_ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.success, grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 20, .y = 0 } },
+ }, &empty_ref));
+
+ var line_ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.success, grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 0, .y = 0 } },
+ }, &line_ref));
+
+ var word_opts: selection_c.SelectWordOptions = .{
+ .ref = word_ref,
+ };
+ try testing.expectEqual(Result.success, selection_c.word(t, &word_opts, &out));
+ try testing.expectEqual(@as(u16, 2), out.start.toPin().?.x);
+ try testing.expectEqual(@as(u16, 6), out.end.toPin().?.x);
+
+ word_opts.ref = empty_ref;
+ try testing.expectEqual(Result.no_value, selection_c.word(t, &word_opts, &out));
+
+ var between_start_ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.success, grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 20, .y = 1 } },
+ }, &between_start_ref));
+
+ var between_end_ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.success, grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 0, .y = 1 } },
+ }, &between_end_ref));
+
+ var word_between_opts: selection_c.SelectWordBetweenOptions = .{
+ .start = between_start_ref,
+ .end = between_end_ref,
+ };
+ try testing.expectEqual(Result.success, selection_c.word_between(t, &word_between_opts, &out));
+ try testing.expectEqual(@as(u16, 0), out.start.toPin().?.x);
+ try testing.expectEqual(@as(u16, 1), out.start.toPin().?.y);
+ try testing.expectEqual(@as(u16, 4), out.end.toPin().?.x);
+ try testing.expectEqual(@as(u16, 1), out.end.toPin().?.y);
+
+ var line_opts: selection_c.SelectLineOptions = .{
+ .ref = line_ref,
+ };
+ try testing.expectEqual(Result.success, selection_c.line(t, &line_opts, &out));
+ try testing.expectEqual(@as(u16, 2), out.start.toPin().?.x);
+ try testing.expectEqual(@as(u16, 6), out.end.toPin().?.x);
+
+ try testing.expectEqual(Result.success, selection_c.all(t, &out));
+ try testing.expectEqual(@as(u16, 2), out.start.toPin().?.x);
+ try testing.expectEqual(@as(u16, 0), out.start.toPin().?.y);
+ try testing.expectEqual(@as(u16, 4), out.end.toPin().?.x);
+ try testing.expectEqual(@as(u16, 1), out.end.toPin().?.y);
+
+ try testing.expectEqual(Result.no_value, selection_c.output(t, line_ref, &out));
+
+ line_opts.size = @sizeOf(usize) - 1;
+ try testing.expectEqual(Result.invalid_value, selection_c.line(t, &line_opts, &out));
+ try testing.expectEqual(Result.invalid_value, selection_c.word(t, null, &out));
+ try testing.expectEqual(Result.invalid_value, selection_c.word(t, &word_opts, null));
+ try testing.expectEqual(Result.invalid_value, selection_c.word_between(t, null, &out));
+ try testing.expectEqual(Result.invalid_value, selection_c.word_between(t, &word_between_opts, null));
+}
+
+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_c.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_c.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_c.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 "selection_order and selection_ordered" {
+ 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\r\nWorld", 12);
+
+ var start_ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.success, grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 3, .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 = 1 } },
+ }, &end_ref));
+
+ const sel: selection_c.CSelection = .{
+ .start = start_ref,
+ .end = end_ref,
+ .rectangle = true,
+ };
+
+ var order: selection_c.Order = undefined;
+ try testing.expectEqual(Result.success, selection_c.order(t, &sel, &order));
+ try testing.expectEqual(selection_c.Order.mirrored_forward, order);
+
+ var out: selection_c.CSelection = undefined;
+ try testing.expectEqual(Result.success, selection_c.ordered(t, &sel, .forward, &out));
+ try testing.expectEqual(@as(u16, 1), out.start.toPin().?.x);
+ try testing.expectEqual(@as(u16, 0), out.start.toPin().?.y);
+ try testing.expectEqual(@as(u16, 3), out.end.toPin().?.x);
+ try testing.expectEqual(@as(u16, 1), out.end.toPin().?.y);
+ try testing.expect(out.rectangle);
+
+ try testing.expectEqual(Result.success, selection_c.ordered(t, &sel, .reverse, &out));
+ try testing.expectEqual(@as(u16, 3), out.start.toPin().?.x);
+ try testing.expectEqual(@as(u16, 1), out.start.toPin().?.y);
+ try testing.expectEqual(@as(u16, 1), out.end.toPin().?.x);
+ try testing.expectEqual(@as(u16, 0), out.end.toPin().?.y);
+ try testing.expect(out.rectangle);
+}
+
+test "selection_contains" {
+ 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\r\nWorld", 12);
+
+ var start_ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.success, grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 3, .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 = 1 } },
+ }, &end_ref));
+
+ const linear: selection_c.CSelection = .{
+ .start = start_ref,
+ .end = end_ref,
+ };
+
+ var contains: bool = undefined;
+ try testing.expectEqual(Result.success, selection_c.contains(t, &linear, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 4, .y = 0 } },
+ }, &contains));
+ try testing.expect(contains);
+
+ try testing.expectEqual(Result.success, selection_c.contains(t, &linear, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 2, .y = 0 } },
+ }, &contains));
+ try testing.expect(!contains);
+
+ const rectangle: selection_c.CSelection = .{
+ .start = start_ref,
+ .end = end_ref,
+ .rectangle = true,
+ };
+
+ try testing.expectEqual(Result.success, selection_c.contains(t, &rectangle, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 2, .y = 0 } },
+ }, &contains));
+ try testing.expect(contains);
+}
+
+test "selection_equal" {
+ 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);
+
+ var other_t: Terminal = null;
+ try testing.expectEqual(Result.success, new(
+ &lib.alloc.test_allocator,
+ &other_t,
+ .{
+ .cols = 80,
+ .rows = 24,
+ .max_scrollback = 0,
+ },
+ ));
+ defer free(other_t);
+
+ vt_write(t, "Hello", 5);
+ vt_write(other_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 other_end_ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.success, grid_ref(t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 2, .y = 0 } },
+ }, &other_end_ref));
+
+ var cross_terminal_ref: grid_ref_c.CGridRef = .{};
+ try testing.expectEqual(Result.success, grid_ref(other_t, .{
+ .tag = .active,
+ .value = .{ .active = .{ .x = 1, .y = 0 } },
+ }, &cross_terminal_ref));
+
+ const sel: selection_c.CSelection = .{
+ .start = start_ref,
+ .end = end_ref,
+ };
+ const equal_sel: selection_c.CSelection = .{
+ .start = start_ref,
+ .end = end_ref,
+ };
+ const different_endpoint: selection_c.CSelection = .{
+ .start = start_ref,
+ .end = other_end_ref,
+ };
+ const different_rectangle: selection_c.CSelection = .{
+ .start = start_ref,
+ .end = end_ref,
+ .rectangle = true,
+ };
+ const cross_terminal: selection_c.CSelection = .{
+ .start = start_ref,
+ .end = cross_terminal_ref,
+ };
+
+ var equal: bool = undefined;
+ try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &equal_sel, &equal));
+ try testing.expect(equal);
+
+ try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &different_endpoint, &equal));
+ try testing.expect(!equal);
+
+ try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &different_rectangle, &equal));
+ try testing.expect(!equal);
+
+ try testing.expectEqual(Result.success, selection_c.equal(t, &sel, &cross_terminal, &equal));
+ try testing.expect(!equal);
+ try testing.expectEqual(Result.invalid_value, selection_c.equal(t, &sel, &equal_sel, null));
+}
+
+test "selection_order invalid values" {
+ 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);
+
+ var order: selection_c.Order = undefined;
+ try testing.expectEqual(Result.invalid_value, selection_c.order(null, null, &order));
+ try testing.expectEqual(Result.invalid_value, selection_c.order(t, null, &order));
+}
+
test "grid_ref" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
diff --git a/vendor/libghostty-vt/src/terminal/c/types.zig b/vendor/libghostty-vt/src/terminal/c/types.zig
index 500809d9..de043a03 100644
--- a/vendor/libghostty-vt/src/terminal/c/types.zig
+++ b/vendor/libghostty-vt/src/terminal/c/types.zig
@@ -14,36 +14,60 @@ const size_report = @import("size_report.zig");
const terminal = @import("terminal.zig");
const formatter = @import("formatter.zig");
const selection = @import("selection.zig");
+const selection_gesture = @import("selection_gesture.zig");
const render = @import("render.zig");
const style_c = @import("style.zig");
const mouse_encode = @import("mouse_encode.zig");
const grid_ref = @import("grid_ref.zig");
+/// C: GhosttySurfacePosition
+pub const SurfacePosition = extern struct {
+ x: f64,
+ y: f64,
+};
+
+/// C: GhosttyCodepoints
+pub const Codepoints = extern struct {
+ ptr: ?[*]const u32 = null,
+ len: usize = 0,
+};
+
/// All C API structs and their Ghostty C names.
-pub const structs: std.StaticStringMap(StructInfo) = .initComptime(.{
- .{ "GhosttyColorRgb", StructInfo.init(color.RGB.C) },
- .{ "GhosttyDeviceAttributes", StructInfo.init(terminal.DeviceAttributes) },
- .{ "GhosttyDeviceAttributesPrimary", StructInfo.init(terminal.DeviceAttributes.Primary) },
- .{ "GhosttyDeviceAttributesSecondary", StructInfo.init(terminal.DeviceAttributes.Secondary) },
- .{ "GhosttyDeviceAttributesTertiary", StructInfo.init(terminal.DeviceAttributes.Tertiary) },
- .{ "GhosttyFormatterTerminalOptions", StructInfo.init(formatter.TerminalOptions) },
- .{ "GhosttySelection", StructInfo.init(selection.CSelection) },
- .{ "GhosttyFormatterTerminalExtra", StructInfo.init(formatter.TerminalOptions.Extra) },
- .{ "GhosttyFormatterScreenExtra", StructInfo.init(formatter.ScreenOptions.Extra) },
- .{ "GhosttyGridRef", StructInfo.init(grid_ref.CGridRef) },
- .{ "GhosttyMouseEncoderSize", StructInfo.init(mouse_encode.Size) },
- .{ "GhosttyMousePosition", StructInfo.init(mouse_event.Position) },
- .{ "GhosttyPoint", StructInfo.init(point.Point.C) },
- .{ "GhosttyPointCoordinate", StructInfo.init(point.Coordinate) },
- .{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) },
- .{ "GhosttySizeReportSize", StructInfo.init(size_report.Size) },
- .{ "GhosttyString", StructInfo.init(lib.String) },
- .{ "GhosttyStyle", StructInfo.init(style_c.Style) },
- .{ "GhosttyStyleColor", StructInfo.init(style_c.Color) },
- .{ "GhosttyTerminalOptions", StructInfo.init(terminal.Options) },
- .{ "GhosttyTerminalScrollbar", StructInfo.init(terminal.TerminalScrollbar) },
- .{ "GhosttyTerminalScrollViewport", StructInfo.init(terminal.ScrollViewport) },
-});
+pub const structs: std.StaticStringMap(StructInfo) = structs: {
+ @setEvalBranchQuota(10_000);
+ break :structs .initComptime(.{
+ .{ "GhosttyBuffer", StructInfo.init(lib.Buffer) },
+ .{ "GhosttyCodepoints", StructInfo.init(Codepoints) },
+ .{ "GhosttyColorRgb", StructInfo.init(color.RGB.C) },
+ .{ "GhosttyDeviceAttributes", StructInfo.init(terminal.DeviceAttributes) },
+ .{ "GhosttyDeviceAttributesPrimary", StructInfo.init(terminal.DeviceAttributes.Primary) },
+ .{ "GhosttyDeviceAttributesSecondary", StructInfo.init(terminal.DeviceAttributes.Secondary) },
+ .{ "GhosttyDeviceAttributesTertiary", StructInfo.init(terminal.DeviceAttributes.Tertiary) },
+ .{ "GhosttyFormatterTerminalOptions", StructInfo.init(formatter.TerminalOptions) },
+ .{ "GhosttySelection", StructInfo.init(selection.CSelection) },
+ .{ "GhosttyTerminalSelectWordOptions", StructInfo.init(selection.SelectWordOptions) },
+ .{ "GhosttyTerminalSelectWordBetweenOptions", StructInfo.init(selection.SelectWordBetweenOptions) },
+ .{ "GhosttyTerminalSelectLineOptions", StructInfo.init(selection.SelectLineOptions) },
+ .{ "GhosttyFormatterTerminalExtra", StructInfo.init(formatter.TerminalOptions.Extra) },
+ .{ "GhosttyFormatterScreenExtra", StructInfo.init(formatter.ScreenOptions.Extra) },
+ .{ "GhosttyGridRef", StructInfo.init(grid_ref.CGridRef) },
+ .{ "GhosttyMouseEncoderSize", StructInfo.init(mouse_encode.Size) },
+ .{ "GhosttyMousePosition", StructInfo.init(mouse_event.Position) },
+ .{ "GhosttyPoint", StructInfo.init(point.Point.C) },
+ .{ "GhosttyPointCoordinate", StructInfo.init(point.Coordinate) },
+ .{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) },
+ .{ "GhosttySelectionGestureBehaviors", StructInfo.init(selection_gesture.Behaviors) },
+ .{ "GhosttySelectionGestureGeometry", StructInfo.init(selection_gesture.Geometry) },
+ .{ "GhosttySizeReportSize", StructInfo.init(size_report.Size) },
+ .{ "GhosttyString", StructInfo.init(lib.String) },
+ .{ "GhosttySurfacePosition", StructInfo.init(SurfacePosition) },
+ .{ "GhosttyStyle", StructInfo.init(style_c.Style) },
+ .{ "GhosttyStyleColor", StructInfo.init(style_c.Color) },
+ .{ "GhosttyTerminalOptions", StructInfo.init(terminal.Options) },
+ .{ "GhosttyTerminalScrollbar", StructInfo.init(terminal.TerminalScrollbar) },
+ .{ "GhosttyTerminalScrollViewport", StructInfo.init(terminal.ScrollViewport) },
+ });
+};
/// The comptime-generated JSON string of all structs.
pub const json: [:0]const u8 = json: {
@@ -144,6 +168,11 @@ fn jsonWriteAll(writer: *std.Io.Writer) std.Io.Writer.Error!void {
fn typeName(comptime T: type) []const u8 {
return switch (@typeInfo(T)) {
.bool => "bool",
+ .float => |info| switch (info.bits) {
+ 32 => "f32",
+ 64 => "f64",
+ else => @compileError("unsupported float size"),
+ },
.int => |info| switch (info.signedness) {
.signed => switch (info.bits) {
8 => "i8",