From 6b94c2da26653cc8feeaee3ef90166b3ad1e3aee Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Apr 2026 10:45:03 -0700 Subject: [PATCH] libghostty: add ghostty_terminal_point_from_grid_ref Add the inverse of ghostty_terminal_grid_ref(), converting a grid reference back to coordinates in a requested coordinate system (active, viewport, screen, or history). This wraps the existing internal PageList.pointFromPin and is placed on the terminal API since it requires terminal-owned PageList state to resolve the top-left anchor for each coordinate system. Returns GHOSTTY_NO_VALUE when the ref falls outside the requested range, e.g. a scrollback ref cannot be expressed in active coordinates. --- include/ghostty/vt/terminal.h | 33 ++++++++++ src/lib_vt.zig | 1 + src/terminal/c/main.zig | 1 + src/terminal/c/terminal.zig | 110 ++++++++++++++++++++++++++++++++++ 4 files changed, 145 insertions(+) diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index 73db8d6d1..a229dd700 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -1064,6 +1064,39 @@ GHOSTTY_API GhosttyResult ghostty_terminal_grid_ref(GhosttyTerminal terminal, GhosttyPoint point, GhosttyGridRef *out_ref); +/** + * Convert a grid reference back to a point in the given coordinate system. + * + * This is the inverse of ghostty_terminal_grid_ref(): given a grid reference, + * it returns the x/y coordinates in the requested coordinate system (active, + * viewport, screen, or history). + * + * The grid reference must have been obtained from the same terminal instance. + * Like all grid references, it is only valid until the next mutating terminal + * call. + * + * Not every grid reference is representable in every coordinate system. For + * example, a cell in scrollback history cannot be expressed in active + * coordinates, and a cell that has scrolled off the visible area cannot be + * expressed in viewport coordinates. In these cases, the function returns + * GHOSTTY_NO_VALUE. + * + * @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE) + * @param ref Pointer to the grid reference to convert + * @param tag The target coordinate system + * @param[out] out On success, set to the coordinate in the requested system (may be NULL) + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal + * or ref is NULL/invalid, GHOSTTY_NO_VALUE if the ref falls outside + * the requested coordinate system + * + * @ingroup terminal + */ +GHOSTTY_API GhosttyResult ghostty_terminal_point_from_grid_ref( + GhosttyTerminal terminal, + const GhosttyGridRef *ref, + GhosttyPointTag tag, + GhosttyPointCoordinate *out); + /** @} */ #ifdef __cplusplus diff --git a/src/lib_vt.zig b/src/lib_vt.zig index ce2e4d5b6..3799fbe66 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -233,6 +233,7 @@ comptime { @export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" }); @export(&c.terminal_get, .{ .name = "ghostty_terminal_get" }); @export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" }); + @export(&c.terminal_point_from_grid_ref, .{ .name = "ghostty_terminal_point_from_grid_ref" }); @export(&c.kitty_graphics_get, .{ .name = "ghostty_kitty_graphics_get" }); @export(&c.kitty_graphics_image, .{ .name = "ghostty_kitty_graphics_image" }); @export(&c.kitty_graphics_image_get, .{ .name = "ghostty_kitty_graphics_image_get" }); diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index ebfe2571d..3f5f65f49 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -157,6 +157,7 @@ pub const terminal_mode_get = terminal.mode_get; pub const terminal_mode_set = terminal.mode_set; pub const terminal_get = terminal.get; pub const terminal_grid_ref = terminal.grid_ref; +pub const terminal_point_from_grid_ref = terminal.point_from_grid_ref; pub const type_json = types.get_json; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 32bc0311a..8a2a3d40b 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -697,6 +697,20 @@ pub fn grid_ref( return .success; } +pub fn point_from_grid_ref( + terminal_: Terminal, + ref: *const grid_ref_c.CGridRef, + tag: point.Tag, + out: ?*point.Coordinate, +) callconv(lib.calling_conv) Result { + const t: *ZigTerminal = (terminal_ orelse return .invalid_value).terminal; + const p = ref.toPin() orelse return .invalid_value; + const pt = t.screens.active.pages.pointFromPin(tag, p) orelse + return .no_value; + if (out) |o| o.* = pt.coord(); + return .success; +} + pub fn free(terminal_: Terminal) callconv(lib.calling_conv) void { const wrapper = terminal_ orelse return; const t = wrapper.terminal; @@ -1261,6 +1275,102 @@ test "grid_ref null terminal" { }, &out_ref)); } +test "point_from_grid_ref roundtrip active" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + )); + defer free(t); + + vt_write(t, "Hello", 5); + + // Get a grid ref at (2, 0) in active coords + var ref: grid_ref_c.CGridRef = .{}; + try testing.expectEqual(Result.success, grid_ref(t, .{ + .tag = .active, + .value = .{ .active = .{ .x = 2, .y = 0 } }, + }, &ref)); + + // Convert back to active coords + var coord: point.Coordinate = undefined; + try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .active, &coord)); + try testing.expectEqual(@as(size.CellCountInt, 2), coord.x); + try testing.expectEqual(@as(u32, 0), coord.y); +} + +test "point_from_grid_ref roundtrip viewport" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + )); + defer free(t); + + vt_write(t, "Hello", 5); + + var ref: grid_ref_c.CGridRef = .{}; + try testing.expectEqual(Result.success, grid_ref(t, .{ + .tag = .viewport, + .value = .{ .viewport = .{ .x = 0, .y = 0 } }, + }, &ref)); + + var coord: point.Coordinate = undefined; + try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .viewport, &coord)); + try testing.expectEqual(@as(size.CellCountInt, 0), coord.x); + try testing.expectEqual(@as(u32, 0), coord.y); +} + +test "point_from_grid_ref history ref to active returns no_value" { + var t: Terminal = null; + try testing.expectEqual(Result.success, new( + &lib.alloc.test_allocator, + &t, + .{ .cols = 80, .rows = 4, .max_scrollback = 10_000 }, + )); + defer free(t); + + // Write enough lines to push content into scrollback + for (0..10) |_| { + vt_write(t, "line\n", 5); + } + + // Get a ref to the first line (now in scrollback) + var ref: grid_ref_c.CGridRef = .{}; + try testing.expectEqual(Result.success, grid_ref(t, .{ + .tag = .screen, + .value = .{ .screen = .{ .x = 0, .y = 0 } }, + }, &ref)); + + // Should succeed for screen coords + var coord: point.Coordinate = undefined; + try testing.expectEqual(Result.success, point_from_grid_ref(t, &ref, .screen, &coord)); + try testing.expectEqual(@as(u32, 0), coord.y); + + // Should fail for active coords (it's in scrollback) + try testing.expectEqual(Result.no_value, point_from_grid_ref(t, &ref, .active, &coord)); +} + +test "point_from_grid_ref null terminal" { + var ref: grid_ref_c.CGridRef = .{}; + try testing.expectEqual(Result.invalid_value, point_from_grid_ref(null, &ref, .active, null)); +} + +test "point_from_grid_ref null node" { + 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); + + const ref: grid_ref_c.CGridRef = .{}; + try testing.expectEqual(Result.invalid_value, point_from_grid_ref(t, &ref, .active, null)); +} + test "set write_pty callback" { var t: Terminal = null; try testing.expectEqual(Result.success, new(