From 0400de28b40bc47b0fcd0f5a78a908413cb86be6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Mar 2026 14:07:12 -0700 Subject: [PATCH] vt: add ghostty_terminal_cell for point-based cell lookup Add a new C API function ghostty_terminal_cell that retrieves the opaque cell and row values at a given point in the terminal grid. The point is a tagged union supporting active, viewport, screen, and history coordinate systems. --- include/ghostty/vt/terminal.h | 36 +++++++++++++++- src/lib_vt.zig | 1 + src/terminal/c/main.zig | 1 + src/terminal/c/terminal.zig | 81 +++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 2 deletions(-) diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h index d0b14977f..65a6d5389 100644 --- a/include/ghostty/vt/terminal.h +++ b/include/ghostty/vt/terminal.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #ifdef __cplusplus @@ -372,8 +373,39 @@ GhosttyResult ghostty_terminal_mode_set(GhosttyTerminal terminal, * @ingroup terminal */ GhosttyResult ghostty_terminal_get(GhosttyTerminal terminal, - GhosttyTerminalData data, - void *out); + GhosttyTerminalData data, + void *out); + +/** + * Get the cell and row at a given point in the terminal. + * + * 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(). + * + * Lookups using the `active` and `viewport` tags are fast. The `screen` + * and `history` tags may require traversing the full scrollback page list + * to resolve the y coordinate, so they can be expensive for large + * scrollback buffers. + * + * This function isn't 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. This API is instead meant for less + * strictly performance-sensitive use cases. + * + * @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) + * @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); /** @} */ diff --git a/src/lib_vt.zig b/src/lib_vt.zig index 5494019c4..d82a3f3de 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -196,6 +196,7 @@ 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" }); // On Wasm we need to export our allocator convenience functions. if (builtin.target.cpu.arch.isWasm()) { diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 584f04f43..b1b2c92ef 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -109,6 +109,7 @@ 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; test { _ = cell; diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig index 1d31d5ae2..fe3a5e149 100644 --- a/src/terminal/c/terminal.zig +++ b/src/terminal/c/terminal.zig @@ -7,7 +7,10 @@ const ScreenSet = @import("../ScreenSet.zig"); const PageList = @import("../PageList.zig"); const kitty = @import("../kitty/key.zig"); const modes = @import("../modes.zig"); +const point = @import("../point.zig"); const size = @import("../size.zig"); +const cell_c = @import("cell.zig"); +const row_c = @import("row.zig"); const style_c = @import("style.zig"); const Result = @import("result.zig").Result; @@ -207,6 +210,26 @@ fn getTyped( return .success; } +pub fn cell( + terminal_: Terminal, + pt: point.Point.C, + out_cell: ?*cell_c.CCell, + out_row: ?*row_c.CRow, +) callconv(.c) Result { + const t = terminal_ orelse return .invalid_value; + 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 result = t.screens.active.pages.getCell(zig_pt) orelse + return .invalid_value; + if (out_cell) |p| p.* = @bitCast(result.cell.*); + if (out_row) |p| p.* = @bitCast(result.row.*); + return .success; +} + pub fn free(terminal_: Terminal) callconv(.c) void { const t = terminal_ orelse return; @@ -612,3 +635,61 @@ test "get invalid" { try testing.expectEqual(Result.invalid_value, get(t, .invalid, null)); } + +test "cell" { + 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 out_cell: cell_c.CCell = undefined; + var out_row: row_c.CRow = undefined; + try testing.expectEqual(Result.success, cell(t, .{ + .tag = .active, + .value = .{ .active = .{ .x = 0, .y = 0 } }, + }, &out_cell, &out_row)); + + // 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, .{ + .tag = .active, + .value = .{ .active = .{ .x = 0, .y = 0 } }, + }, &out_cell, &out_row)); +} + +test "cell out of bounds" { + 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 out_cell: cell_c.CCell = undefined; + var out_row: row_c.CRow = undefined; + try testing.expectEqual(Result.invalid_value, cell(t, .{ + .tag = .active, + .value = .{ .active = .{ .x = 100, .y = 0 } }, + }, &out_cell, &out_row)); +}