@@ -29,10 +29,10 @@ index 4f8fef88e..ca9fb1d4d 100644
2929 #include <ghostty/vt/key.h>
3030diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h
3131new file mode 100644
32- index 000000000..26ee23ed5
32+ index 000000000..dd7d1ee6b
3333--- /dev/null
3434+++ b/include/ghostty/vt/terminal.h
35- @@ -0,0 +1,383 @@
35+ @@ -0,0 +1,418 @@
3636+ /**
3737+ * @file terminal.h
3838+ *
@@ -164,6 +164,12 @@ index 000000000..26ee23ed5
164164+
165165+ /** Character width: 0=combining, 1=normal, 2=wide (CJK) */
166166+ uint8_t width;
167+ +
168+ + /** Hyperlink ID (0 = no link, >0 = lookup in hyperlink set) */
169+ + uint16_t hyperlink_id;
170+ +
171+ + /** Padding for alignment (keeps struct at 16 bytes) */
172+ + uint32_t _padding;
167173+ } GhosttyCell;
168174+
169175+ /** Cell flag: Bold text */
@@ -409,6 +415,35 @@ index 000000000..26ee23ed5
409415+ */
410416+ void ghostty_terminal_clear_dirty(GhosttyTerminal term);
411417+
418+ + /* ============================================================================
419+ + * Hyperlink Support
420+ + * ========================================================================= */
421+ +
422+ + /**
423+ + * Get hyperlink URI by ID.
424+ + *
425+ + * Retrieves the URI string for a hyperlink ID obtained from a GhosttyCell.
426+ + * The URI is written to the provided buffer.
427+ + *
428+ + * @param term Terminal instance
429+ + * @param hyperlink_id Hyperlink ID from GhosttyCell (must be > 0)
430+ + * @param out_buffer Buffer to write URI string (UTF-8)
431+ + * @param buffer_size Size of output buffer in bytes
432+ + * @return Number of bytes written (not including null terminator), or 0 if:
433+ + * - hyperlink_id is 0 or invalid
434+ + * - URI doesn't exist
435+ + * - buffer is too small (URI is truncated)
436+ + *
437+ + * @note The returned string is NOT null-terminated. Use the return value
438+ + * to determine the actual length.
439+ + */
440+ + int ghostty_terminal_get_hyperlink_uri(
441+ + GhosttyTerminal term,
442+ + uint16_t hyperlink_id,
443+ + uint8_t* out_buffer,
444+ + size_t buffer_size
445+ + );
446+ +
412447+ #ifdef __cplusplus
413448+ }
414449+ #endif
@@ -417,10 +452,10 @@ index 000000000..26ee23ed5
417452+
418453+ #endif /* GHOSTTY_VT_TERMINAL_H */
419454diff --git a/src/lib_vt.zig b/src/lib_vt.zig
420- index e95eee5f4..4b57361f6 100644
455+ index e95eee5f4..1ac0672e4 100644
421456--- a/src/lib_vt.zig
422457+++ b/src/lib_vt.zig
423- @@ -137,6 +137,24 @@ comptime {
458+ @@ -137,6 +137,25 @@ comptime {
424459 @export(&c.sgr_unknown_partial, .{ .name = "ghostty_sgr_unknown_partial" });
425460 @export(&c.sgr_attribute_tag, .{ .name = "ghostty_sgr_attribute_tag" });
426461 @export(&c.sgr_attribute_value, .{ .name = "ghostty_sgr_attribute_value" });
@@ -442,11 +477,12 @@ index e95eee5f4..4b57361f6 100644
442477+ @export(&c.terminal_is_dirty, .{ .name = "ghostty_terminal_is_dirty" });
443478+ @export(&c.terminal_is_row_dirty, .{ .name = "ghostty_terminal_is_row_dirty" });
444479+ @export(&c.terminal_clear_dirty, .{ .name = "ghostty_terminal_clear_dirty" });
480+ + @export(&c.terminal_get_hyperlink_uri, .{ .name = "ghostty_terminal_get_hyperlink_uri" });
445481
446482 // On Wasm we need to export our allocator convenience functions.
447483 if (builtin.target.cpu.arch.isWasm()) {
448484diff --git a/src/terminal/c/main.zig b/src/terminal/c/main.zig
449- index bc92597f5..3ce57e206 100644
485+ index bc92597f5..1b3cc92a5 100644
450486--- a/src/terminal/c/main.zig
451487+++ b/src/terminal/c/main.zig
452488@@ -4,6 +4,7 @@ pub const key_event = @import("key_event.zig");
@@ -457,7 +493,7 @@ index bc92597f5..3ce57e206 100644
457493
458494 // The full C API, unexported.
459495 pub const osc_new = osc.new;
460- @@ -52,6 +53,25 @@ pub const key_encoder_encode = key_encode.encode;
496+ @@ -52,6 +53,26 @@ pub const key_encoder_encode = key_encode.encode;
461497
462498 pub const paste_is_safe = paste.is_safe;
463499
@@ -479,11 +515,12 @@ index bc92597f5..3ce57e206 100644
479515+ pub const terminal_is_dirty = terminal.isDirty;
480516+ pub const terminal_is_row_dirty = terminal.isRowDirty;
481517+ pub const terminal_clear_dirty = terminal.clearDirty;
518+ + pub const terminal_get_hyperlink_uri = terminal.getHyperlinkUri;
482519+
483520 test {
484521 _ = color;
485522 _ = osc;
486- @@ -59,6 +79 ,7 @@ test {
523+ @@ -59,6 +80 ,7 @@ test {
487524 _ = key_encode;
488525 _ = paste;
489526 _ = sgr;
@@ -493,10 +530,10 @@ index bc92597f5..3ce57e206 100644
493530 _ = @import("../../lib/allocator.zig");
494531diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig
495532new file mode 100644
496- index 000000000..d195e1acf
533+ index 000000000..7693111c6
497534--- /dev/null
498535+++ b/src/terminal/c/terminal.zig
499- @@ -0,0 +1,530 @@
536+ @@ -0,0 +1,578 @@
500537+ //! C API wrapper for Terminal
501538+ //!
502539+ //! This provides a C-compatible interface to Ghostty's Terminal for WASM export.
@@ -540,17 +577,20 @@ index 000000000..d195e1acf
540577+ };
541578+ };
542579+
543- + /// C-compatible cell structure
580+ + /// C-compatible cell structure (14 bytes actual, padded to 16 by compiler)
544581+ pub const GhosttyCell = extern struct {
545- + codepoint: u32,
546- + fg_r: u8,
547- + fg_g: u8,
548- + fg_b: u8,
549- + bg_r: u8,
550- + bg_g: u8,
551- + bg_b: u8,
552- + flags: u8,
553- + width: u8,
582+ + codepoint: u32, // 0-3
583+ + fg_r: u8, // 4
584+ + fg_g: u8, // 5
585+ + fg_b: u8, // 6
586+ + bg_r: u8, // 7
587+ + bg_g: u8, // 8
588+ + bg_b: u8, // 9
589+ + flags: u8, // 10
590+ + width: u8, // 11
591+ + hyperlink_id: u16, // 12-13 (0 = no link, >0 = hyperlink ID in set)
592+ + // Compiler adds 2 bytes padding here to align to 4 bytes
593+ + // Total size: 16 bytes with padding
554594+ };
555595+
556596+ /// C-compatible terminal configuration
@@ -843,6 +883,19 @@ index 000000000..d195e1acf
843883+ .spacer_head => 0,
844884+ };
845885+
886+ + // Get hyperlink ID if cell has hyperlink
887+ + const hyperlink_id: u16 = if (cell.hyperlink) blk: {
888+ + if (list_cell_opt) |list_cell| {
889+ + const page = &list_cell.node.data;
890+ + const cell_offset = size.getOffset(Cell, page.memory, list_cell.cell);
891+ + const map = page.hyperlink_map.map(page.memory);
892+ + if (map.get(cell_offset)) |id| {
893+ + break :blk id;
894+ + }
895+ + }
896+ + break :blk 0;
897+ + } else 0;
898+ +
846899+ return .{
847900+ .codepoint = cp,
848901+ .fg_r = fg_rgb.r,
@@ -853,6 +906,7 @@ index 000000000..d195e1acf
853906+ .bg_b = bg_rgb.b,
854907+ .flags = flags,
855908+ .width = width,
909+ + .hyperlink_id = hyperlink_id,
856910+ };
857911+ }
858912+
@@ -960,6 +1014,37 @@ index 000000000..d195e1acf
9601014+ }
9611015+
9621016+ // ============================================================================
1017+ + // Hyperlink Support
1018+ + // ============================================================================
1019+ +
1020+ + pub fn getHyperlinkUri(
1021+ + ptr: ?*anyopaque,
1022+ + hyperlink_id: u16,
1023+ + out_buffer: [*]u8,
1024+ + buffer_size: usize,
1025+ + ) callconv(.c) c_int {
1026+ + const wrapper: *const TerminalWrapper = @ptrCast(@alignCast(ptr orelse return 0));
1027+ +
1028+ + // Hyperlink ID 0 means no link
1029+ + if (hyperlink_id == 0) return 0;
1030+ +
1031+ + // Get the current page
1032+ + const page = &wrapper.terminal.screen.cursor.page_pin.node.data;
1033+ +
1034+ + // Look up hyperlink in the set
1035+ + const hyperlink_entry = page.hyperlink_set.get(page.memory, hyperlink_id);
1036+ +
1037+ + // Get URI string from page memory
1038+ + const uri = hyperlink_entry.uri.offset.ptr(page.memory)[0..hyperlink_entry.uri.len];
1039+ +
1040+ + // Copy to output buffer (truncate if necessary)
1041+ + const copy_len = @min(uri.len, buffer_size);
1042+ + @memcpy(out_buffer[0..copy_len], uri[0..copy_len]);
1043+ +
1044+ + return @intCast(copy_len);
1045+ + }
1046+ +
1047+ + // ============================================================================
9631048+ // Tests
9641049+ // ============================================================================
9651050+
0 commit comments