From b9a241d1e237fa97bf8b3b161f253cc2313100f2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 4 Apr 2026 20:23:08 -0700 Subject: [PATCH] libghostty: add hyperlink URI accessor to grid_ref API Add ghostty_grid_ref_hyperlink_uri to extract the OSC 8 hyperlink URI from a cell at a grid reference position. Follows the same buffer pattern as ghostty_grid_ref_graphemes: callers pass a buffer and get back the byte length, or GHOSTTY_OUT_OF_SPACE with the required size if the buffer is too small. Cells without a hyperlink return success with length 0. --- include/ghostty/vt/grid_ref.h | 26 ++++++++++ src/lib_vt.zig | 1 + src/terminal/c/grid_ref.zig | 94 +++++++++++++++++++++++++++++++++++ src/terminal/c/main.zig | 1 + 4 files changed, 122 insertions(+) diff --git a/include/ghostty/vt/grid_ref.h b/include/ghostty/vt/grid_ref.h index d3489ea73..1f9f52b9b 100644 --- a/include/ghostty/vt/grid_ref.h +++ b/include/ghostty/vt/grid_ref.h @@ -109,6 +109,32 @@ GHOSTTY_API GhosttyResult ghostty_grid_ref_graphemes(const GhosttyGridRef *ref, size_t buf_len, size_t *out_len); +/** + * Get the hyperlink URI for the cell at the grid reference's position. + * + * Writes the URI bytes into the provided buffer. If the cell has no + * hyperlink, out_len is set to 0 and GHOSTTY_SUCCESS is returned. + * + * If the buffer is too small (or NULL), the function returns + * GHOSTTY_OUT_OF_SPACE and writes the required number of bytes to + * out_len. The caller can then retry with a sufficiently sized buffer. + * + * @param ref Pointer to the grid reference + * @param buf Output buffer for the URI bytes (may be NULL) + * @param buf_len Size of the output buffer in bytes + * @param[out] out_len On success, the number of bytes written. On + * GHOSTTY_OUT_OF_SPACE, the required buffer size in bytes. + * @return GHOSTTY_SUCCESS on success, GHOSTTY_INVALID_VALUE if the ref's + * node is NULL, GHOSTTY_OUT_OF_SPACE if the buffer is too small + * + * @ingroup grid_ref + */ +GHOSTTY_API GhosttyResult ghostty_grid_ref_hyperlink_uri( + const GhosttyGridRef *ref, + uint8_t *buf, + size_t buf_len, + size_t *out_len); + /** * Get the style of the cell at the grid reference's position. * diff --git a/src/lib_vt.zig b/src/lib_vt.zig index adfb11478..3edef835a 100644 --- a/src/lib_vt.zig +++ b/src/lib_vt.zig @@ -217,6 +217,7 @@ comptime { @export(&c.grid_ref_cell, .{ .name = "ghostty_grid_ref_cell" }); @export(&c.grid_ref_row, .{ .name = "ghostty_grid_ref_row" }); @export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" }); + @export(&c.grid_ref_hyperlink_uri, .{ .name = "ghostty_grid_ref_hyperlink_uri" }); @export(&c.grid_ref_style, .{ .name = "ghostty_grid_ref_style" }); @export(&c.build_info, .{ .name = "ghostty_build_info" }); @export(&c.type_json, .{ .name = "ghostty_type_json" }); diff --git a/src/terminal/c/grid_ref.zig b/src/terminal/c/grid_ref.zig index d029c5951..c7e86c29f 100644 --- a/src/terminal/c/grid_ref.zig +++ b/src/terminal/c/grid_ref.zig @@ -3,11 +3,13 @@ const testing = std.testing; const lib = @import("../lib.zig"); const page = @import("../page.zig"); const PageList = @import("../PageList.zig"); +const point = @import("../point.zig"); const size = @import("../size.zig"); const stylepkg = @import("../style.zig"); const cell_c = @import("cell.zig"); const row_c = @import("row.zig"); const style_c = @import("style.zig"); +const terminal_c = @import("terminal.zig"); const Result = @import("result.zig").Result; /// C: GhosttyGridRef @@ -89,6 +91,38 @@ pub fn grid_ref_graphemes( return .success; } +pub fn grid_ref_hyperlink_uri( + ref: *const CGridRef, + out_buf: ?[*]u8, + buf_len: usize, + out_len: *usize, +) callconv(lib.calling_conv) Result { + const p = ref.toPin() orelse return .invalid_value; + const rac = p.node.data.getRowAndCell(p.x, p.y); + const cell = rac.cell; + + if (!cell.hyperlink) { + out_len.* = 0; + return .success; + } + + const link_id = p.node.data.lookupHyperlink(cell) orelse { + out_len.* = 0; + return .success; + }; + const entry = p.node.data.hyperlink_set.get(p.node.data.memory, link_id); + const uri = entry.uri.slice(p.node.data.memory); + + if (out_buf == null or buf_len < uri.len) { + out_len.* = uri.len; + return .out_of_space; + } + + @memcpy(out_buf.?[0..uri.len], uri); + out_len.* = uri.len; + return .success; +} + pub fn grid_ref_style( ref: *const CGridRef, out: ?*style_c.Style, @@ -154,3 +188,63 @@ test "grid_ref_style null out" { const ref = CGridRef{}; try testing.expectEqual(Result.invalid_value, grid_ref_style(&ref, null)); } + +test "grid_ref_hyperlink_uri null node" { + const ref = CGridRef{}; + var len: usize = undefined; + try testing.expectEqual(Result.invalid_value, grid_ref_hyperlink_uri(&ref, null, 0, &len)); +} + +test "grid_ref_hyperlink_uri no hyperlink" { + var terminal: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib.alloc.test_allocator, + &terminal, + .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + )); + defer terminal_c.free(terminal); + + terminal_c.vt_write(terminal, "hello", 5); + + var ref: CGridRef = undefined; + try testing.expectEqual(Result.success, terminal_c.grid_ref( + terminal, + point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }), + &ref, + )); + + var len: usize = undefined; + try testing.expectEqual(Result.success, grid_ref_hyperlink_uri(&ref, null, 0, &len)); + try testing.expectEqual(@as(usize, 0), len); +} + +test "grid_ref_hyperlink_uri with hyperlink" { + var terminal: terminal_c.Terminal = null; + try testing.expectEqual(Result.success, terminal_c.new( + &lib.alloc.test_allocator, + &terminal, + .{ .cols = 80, .rows = 24, .max_scrollback = 10_000 }, + )); + defer terminal_c.free(terminal); + + // Write OSC 8 hyperlink: \e]8;;uri\e\\text\e]8;;\e\\ + const seq = "\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\"; + terminal_c.vt_write(terminal, seq, seq.len); + + var ref: CGridRef = undefined; + try testing.expectEqual(Result.success, terminal_c.grid_ref( + terminal, + point.Point.cval(.{ .active = .{ .x = 0, .y = 0 } }), + &ref, + )); + + // First query length with null buf + var len: usize = undefined; + try testing.expectEqual(Result.out_of_space, grid_ref_hyperlink_uri(&ref, null, 0, &len)); + try testing.expectEqual(@as(usize, 19), len); // "https://example.com" + + // Now read with a properly sized buffer + var buf: [256]u8 = undefined; + try testing.expectEqual(Result.success, grid_ref_hyperlink_uri(&ref, &buf, buf.len, &len)); + try testing.expectEqualStrings("https://example.com", buf[0..len]); +} diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig index 170567796..699ae5ade 100644 --- a/src/terminal/c/main.zig +++ b/src/terminal/c/main.zig @@ -148,6 +148,7 @@ pub const type_json = types.get_json; pub const grid_ref_cell = grid_ref.grid_ref_cell; 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; test {