diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-01 23:05:23 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-06-01 23:05:51 +0300 |
| commit | 5bd21b7ed468b5ff4d1a5b2ed7e211f401eb138a (patch) | |
| tree | 672fa4625cca0ecb3e9376fc7f8e33d7b0e9deef /vendor/libghostty-vt/src/terminal/c | |
| parent | 32abb3776668ee768200019c170956f67fe33a86 (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.zig | 225 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/c/main.zig | 31 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/c/render.zig | 329 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/c/selection.zig | 515 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/c/selection_gesture.zig | 1488 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/c/terminal.zig | 501 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/terminal/c/types.zig | 77 |
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", |
