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.
This commit is contained in:
Mitchell Hashimoto
2026-04-06 10:45:03 -07:00
parent 20b7fe0e1d
commit 6b94c2da26
4 changed files with 145 additions and 0 deletions

View File

@@ -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

View File

@@ -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" });

View File

@@ -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;

View File

@@ -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(