vt: replace ghostty_terminal_cell with GhosttyGridRef API

This commit is contained in:
Mitchell Hashimoto
2026-03-19 15:35:06 -07:00
parent 0400de28b4
commit df8813bf1b
7 changed files with 203 additions and 34 deletions

View File

@@ -96,6 +96,7 @@ extern "C" {
#include <ghostty/vt/focus.h>
#include <ghostty/vt/formatter.h>
#include <ghostty/vt/terminal.h>
#include <ghostty/vt/grid_ref.h>
#include <ghostty/vt/osc.h>
#include <ghostty/vt/sgr.h>
#include <ghostty/vt/style.h>

View File

@@ -0,0 +1,86 @@
/**
* @file grid_ref.h
*
* Terminal grid reference type for referencing a resolved position in the
* terminal grid.
*/
#ifndef GHOSTTY_VT_GRID_REF_H
#define GHOSTTY_VT_GRID_REF_H
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/types.h>
#include <ghostty/vt/screen.h>
#ifdef __cplusplus
extern "C" {
#endif
/** @defgroup grid_ref Grid Reference
*
* A grid reference is a resolved reference to a specific cell position in the
* terminal's internal page structure. Obtain a grid reference from
* ghostty_terminal_grid_ref(), then extract the cell or row via
* ghostty_grid_ref_cell() and ghostty_grid_ref_row().
*
* A grid reference is only valid until the next update to the terminal
* instance. There is no guarantee that a grid reference will remain
* valid after ANY operation, even if a seemingly unrelated part of
* the grid is changed, so any information related to the grid reference
* should be read and cached immediately after obtaining the grid reference.
*
* This API is not meant to be used as the core of render loop. It isn't
* built to sustain the framerates needed for rendering large screens.
* Use the render state API for that.
*
* @{
*/
/**
* A resolved reference to a terminal cell position.
*
* This is a sized struct. Use GHOSTTY_INIT_SIZED() to initialize it.
*
* @ingroup grid_ref
*/
typedef struct {
size_t size;
void *node;
uint16_t x;
uint16_t y;
} GhosttyGridRef;
/**
* Get the cell from a grid reference.
*
* @param ref Pointer to the grid reference
* @param[out] out_cell On success, set to the cell at the ref's position (may be NULL)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's
* node is NULL
*
* @ingroup grid_ref
*/
GhosttyResult ghostty_grid_ref_cell(const GhosttyGridRef *ref,
GhosttyCell *out_cell);
/**
* Get the row from a grid reference.
*
* @param ref Pointer to the grid reference
* @param[out] out_row On success, set to the row at the ref's position (may be NULL)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's
* node is NULL
*
* @ingroup grid_ref
*/
GhosttyResult ghostty_grid_ref_row(const GhosttyGridRef *ref,
GhosttyRow *out_row);
/** @} */
#ifdef __cplusplus
}
#endif
#endif /* GHOSTTY_VT_GRID_REF_H */

View File

@@ -13,6 +13,7 @@
#include <ghostty/vt/types.h>
#include <ghostty/vt/allocator.h>
#include <ghostty/vt/modes.h>
#include <ghostty/vt/grid_ref.h>
#include <ghostty/vt/screen.h>
#include <ghostty/vt/point.h>
#include <ghostty/vt/style.h>
@@ -377,11 +378,12 @@ GhosttyResult ghostty_terminal_get(GhosttyTerminal terminal,
void *out);
/**
* Get the cell and row at a given point in the terminal.
* Resolve a point in the terminal grid to a grid reference.
*
* Looks up the cell at the specified point in the terminal grid. On success,
* the output parameters are set to the opaque cell and row values, which can
* be queried further with ghostty_cell_get() and ghostty_row_get().
* Resolves the given point (which can be in active, viewport, screen,
* or history coordinates) to a grid reference for that location. Use
* ghostty_grid_ref_cell() and ghostty_grid_ref_row() to extract the cell
* and row.
*
* Lookups using the `active` and `viewport` tags are fast. The `screen`
* and `history` tags may require traversing the full scrollback page list
@@ -395,17 +397,15 @@ GhosttyResult ghostty_terminal_get(GhosttyTerminal terminal,
*
* @param terminal The terminal handle (NULL returns GHOSTTY_INVALID_VALUE)
* @param point The point specifying which cell to look up
* @param[out] out_cell On success, set to the cell at the given point (may be NULL)
* @param[out] out_row On success, set to the row containing the cell (may be NULL)
* @param[out] out_ref On success, set to the grid reference at the given point (may be NULL)
* @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the terminal
* is NULL or the point is out of bounds
*
* @ingroup terminal
*/
GhosttyResult ghostty_terminal_cell(GhosttyTerminal terminal,
GhosttyPoint point,
GhosttyCell *out_cell,
GhosttyRow *out_row);
GhosttyResult ghostty_terminal_grid_ref(GhosttyTerminal terminal,
GhosttyPoint point,
GhosttyGridRef *out_ref);
/** @} */

View File

@@ -196,7 +196,9 @@ comptime {
@export(&c.terminal_mode_get, .{ .name = "ghostty_terminal_mode_get" });
@export(&c.terminal_mode_set, .{ .name = "ghostty_terminal_mode_set" });
@export(&c.terminal_get, .{ .name = "ghostty_terminal_get" });
@export(&c.terminal_cell, .{ .name = "ghostty_terminal_cell" });
@export(&c.terminal_grid_ref, .{ .name = "ghostty_terminal_grid_ref" });
@export(&c.grid_ref_cell, .{ .name = "ghostty_grid_ref_cell" });
@export(&c.grid_ref_row, .{ .name = "ghostty_grid_ref_row" });
// On Wasm we need to export our allocator convenience functions.
if (builtin.target.cpu.arch.isWasm()) {

View File

@@ -0,0 +1,76 @@
const std = @import("std");
const testing = std.testing;
const page = @import("../page.zig");
const PageList = @import("../PageList.zig");
const size = @import("../size.zig");
const cell_c = @import("cell.zig");
const row_c = @import("row.zig");
const Result = @import("result.zig").Result;
/// C: GhosttyGridRef
///
/// A sized struct that holds a reference to a position in the terminal grid.
/// The ref points to a specific cell position within the terminal's
/// internal page structure.
pub const CGridRef = extern struct {
size: usize = @sizeOf(CGridRef),
node: ?*PageList.List.Node = null,
x: size.CellCountInt = 0,
y: size.CellCountInt = 0,
pub fn fromPin(pin: PageList.Pin) CGridRef {
return .{
.node = pin.node,
.x = pin.x,
.y = pin.y,
};
}
fn toPin(self: CGridRef) ?PageList.Pin {
return .{
.node = self.node orelse return null,
.x = self.x,
.y = self.y,
};
}
};
pub fn grid_ref_cell(
ref: *const CGridRef,
out: ?*cell_c.CCell,
) callconv(.c) Result {
const p = ref.toPin() orelse return .invalid_value;
if (out) |o| o.* = @bitCast(p.rowAndCell().cell.*);
return .success;
}
pub fn grid_ref_row(
ref: *const CGridRef,
out: ?*row_c.CRow,
) callconv(.c) Result {
const p = ref.toPin() orelse return .invalid_value;
if (out) |o| o.* = @bitCast(p.rowAndCell().row.*);
return .success;
}
test "grid_ref_cell null node" {
const ref = CGridRef{};
var out: cell_c.CCell = undefined;
try testing.expectEqual(Result.invalid_value, grid_ref_cell(&ref, &out));
}
test "grid_ref_row null node" {
const ref = CGridRef{};
var out: row_c.CRow = undefined;
try testing.expectEqual(Result.invalid_value, grid_ref_row(&ref, &out));
}
test "grid_ref_cell null out" {
const ref = CGridRef{};
try testing.expectEqual(Result.invalid_value, grid_ref_cell(&ref, null));
}
test "grid_ref_row null out" {
const ref = CGridRef{};
try testing.expectEqual(Result.invalid_value, grid_ref_row(&ref, null));
}

View File

@@ -109,11 +109,16 @@ pub const terminal_scroll_viewport = terminal.scroll_viewport;
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_cell = terminal.cell;
pub const terminal_grid_ref = terminal.grid_ref;
const grid_ref = @import("grid_ref.zig");
pub const grid_ref_cell = grid_ref.grid_ref_cell;
pub const grid_ref_row = grid_ref.grid_ref_row;
test {
_ = cell;
_ = color;
_ = grid_ref;
_ = row;
_ = focus;
_ = formatter;

View File

@@ -11,6 +11,7 @@ const point = @import("../point.zig");
const size = @import("../size.zig");
const cell_c = @import("cell.zig");
const row_c = @import("row.zig");
const grid_ref_c = @import("grid_ref.zig");
const style_c = @import("style.zig");
const Result = @import("result.zig").Result;
@@ -210,11 +211,10 @@ fn getTyped(
return .success;
}
pub fn cell(
pub fn grid_ref(
terminal_: Terminal,
pt: point.Point.C,
out_cell: ?*cell_c.CCell,
out_row: ?*row_c.CRow,
out_ref: ?*grid_ref_c.CGridRef,
) callconv(.c) Result {
const t = terminal_ orelse return .invalid_value;
const zig_pt: point.Point = switch (pt.tag) {
@@ -223,10 +223,9 @@ pub fn cell(
.screen => .{ .screen = pt.value.screen },
.history => .{ .history = pt.value.history },
};
const result = t.screens.active.pages.getCell(zig_pt) orelse
const p = t.screens.active.pages.pin(zig_pt) orelse
return .invalid_value;
if (out_cell) |p| p.* = @bitCast(result.cell.*);
if (out_row) |p| p.* = @bitCast(result.row.*);
if (out_ref) |out| out.* = grid_ref_c.CGridRef.fromPin(p);
return .success;
}
@@ -636,7 +635,7 @@ test "get invalid" {
try testing.expectEqual(Result.invalid_value, get(t, .invalid, null));
}
test "cell" {
test "grid_ref" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
@@ -651,29 +650,30 @@ test "cell" {
vt_write(t, "Hello", 5);
var out_cell: cell_c.CCell = undefined;
var out_row: row_c.CRow = undefined;
try testing.expectEqual(Result.success, cell(t, .{
var out_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.success, grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 0, .y = 0 } },
}, &out_cell, &out_row));
}, &out_ref));
// Extract cell from grid ref and verify it contains 'H'
var out_cell: cell_c.CCell = undefined;
try testing.expectEqual(Result.success, grid_ref_c.grid_ref_cell(&out_ref, &out_cell));
// Verify the cell contains 'H'
var cp: u32 = 0;
try testing.expectEqual(Result.success, cell_c.get(out_cell, .codepoint, @ptrCast(&cp)));
try testing.expectEqual(@as(u32, 'H'), cp);
}
test "cell null terminal" {
var out_cell: cell_c.CCell = undefined;
var out_row: row_c.CRow = undefined;
try testing.expectEqual(Result.invalid_value, cell(null, .{
test "grid_ref null terminal" {
var out_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.invalid_value, grid_ref(null, .{
.tag = .active,
.value = .{ .active = .{ .x = 0, .y = 0 } },
}, &out_cell, &out_row));
}, &out_ref));
}
test "cell out of bounds" {
test "grid_ref out of bounds" {
var t: Terminal = null;
try testing.expectEqual(Result.success, new(
&lib_alloc.test_allocator,
@@ -686,10 +686,9 @@ test "cell out of bounds" {
));
defer free(t);
var out_cell: cell_c.CCell = undefined;
var out_row: row_c.CRow = undefined;
try testing.expectEqual(Result.invalid_value, cell(t, .{
var out_ref: grid_ref_c.CGridRef = .{};
try testing.expectEqual(Result.invalid_value, grid_ref(t, .{
.tag = .active,
.value = .{ .active = .{ .x = 100, .y = 0 } },
}, &out_cell, &out_row));
}, &out_ref));
}