Skip to content

Commit 776140f

Browse files
authored
feat: add hyperlink parsing (#35)
1 parent 771cdb3 commit 776140f

File tree

5 files changed

+149
-24
lines changed

5 files changed

+149
-24
lines changed

lib/buffer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export class Buffer implements IBuffer {
113113
bg_b: 0,
114114
flags: 0,
115115
width: 1,
116+
hyperlink_id: 0,
116117
};
117118
this.nullCell = new BufferCell(nullCellData, 0);
118119
}
@@ -248,6 +249,7 @@ export class BufferLine implements IBufferLine {
248249
bg_b: 0,
249250
flags: 0,
250251
width: 1,
252+
hyperlink_id: 0,
251253
},
252254
x
253255
);

lib/ghostty.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,10 @@ export class GhosttyTerminal {
274274
private _rows: number;
275275

276276
/**
277-
* Size of ghostty_cell_t in bytes (12 bytes in WASM)
278-
* Structure: codepoint(u32) + fg_rgb(3xu8) + bg_rgb(3xu8) + flags(u8) + width(u8)
277+
* Size of ghostty_cell_t in bytes (16 bytes in WASM)
278+
* Structure: codepoint(u32) + fg_rgb(3xu8) + bg_rgb(3xu8) + flags(u8) + width(u8) + hyperlink_id(u16) + padding(u32)
279279
*/
280-
private static readonly CELL_SIZE = 12;
280+
private static readonly CELL_SIZE = 16;
281281

282282
/**
283283
* Create a new terminal.
@@ -491,6 +491,7 @@ export class GhosttyTerminal {
491491
bg_b: view.getUint8(offset + 9),
492492
flags: view.getUint8(offset + 10),
493493
width: view.getUint8(offset + 11),
494+
hyperlink_id: view.getUint16(offset + 12, true),
494495
});
495496
}
496497

@@ -544,6 +545,7 @@ export class GhosttyTerminal {
544545
bg_b: view.getUint8(offset + 9),
545546
flags: view.getUint8(offset + 10),
546547
width: view.getUint8(offset + 11),
548+
hyperlink_id: view.getUint16(offset + 12, true),
547549
});
548550
}
549551

@@ -608,4 +610,33 @@ export class GhosttyTerminal {
608610
}
609611
return dirtyLines;
610612
}
613+
614+
/**
615+
* Get hyperlink URI by ID
616+
*
617+
* @param hyperlinkId Hyperlink ID from a GhosttyCell (0 = no link)
618+
* @returns URI string or null if ID is invalid/not found
619+
*/
620+
getHyperlinkUri(hyperlinkId: number): string | null {
621+
if (hyperlinkId === 0) return null;
622+
623+
const maxUriLen = 2048; // Reasonable limit for URIs
624+
const bufferPtr = this.exports.ghostty_wasm_alloc_u8_array(maxUriLen);
625+
626+
try {
627+
const bytesWritten = this.exports.ghostty_terminal_get_hyperlink_uri(
628+
this.handle,
629+
hyperlinkId,
630+
bufferPtr,
631+
maxUriLen
632+
);
633+
634+
if (bytesWritten === 0) return null;
635+
636+
const buffer = new Uint8Array(this.memory.buffer, bufferPtr, bytesWritten);
637+
return new TextDecoder().decode(buffer);
638+
} finally {
639+
this.exports.ghostty_wasm_free_u8_array(bufferPtr, maxUriLen);
640+
}
641+
}
611642
}

lib/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,12 @@ export interface GhosttyWasmExports extends WebAssembly.Exports {
358358
ghostty_terminal_is_dirty(terminal: TerminalHandle): boolean;
359359
ghostty_terminal_is_row_dirty(terminal: TerminalHandle, row: number): boolean;
360360
ghostty_terminal_clear_dirty(terminal: TerminalHandle): void;
361+
ghostty_terminal_get_hyperlink_uri(
362+
terminal: TerminalHandle,
363+
hyperlinkId: number,
364+
bufPtr: number,
365+
bufLen: number
366+
): number; // returns bytes written
361367
ghostty_terminal_get_line(
362368
terminal: TerminalHandle,
363369
row: number,
@@ -383,7 +389,7 @@ export interface GhosttyWasmExports extends WebAssembly.Exports {
383389
export type TerminalHandle = number;
384390

385391
/**
386-
* Cell structure matching ghostty_cell_t in C (12 bytes)
392+
* Cell structure matching ghostty_cell_t in C (16 bytes)
387393
*/
388394
export interface GhosttyCell {
389395
codepoint: number; // u32 (Unicode codepoint)
@@ -395,6 +401,7 @@ export interface GhosttyCell {
395401
bg_b: number; // u8 (background blue)
396402
flags: number; // u8 (style flags bitfield)
397403
width: number; // u8 (character width: 1=normal, 2=wide, etc.)
404+
hyperlink_id: number; // u16 (0 = no link, >0 = hyperlink ID in set)
398405
}
399406

400407
/**

patches/ghostty-wasm-api.patch

Lines changed: 104 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ index 4f8fef88e..ca9fb1d4d 100644
2929
#include <ghostty/vt/key.h>
3030
diff --git a/include/ghostty/vt/terminal.h b/include/ghostty/vt/terminal.h
3131
new 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 */
419454
diff --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()) {
448484
diff --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");
494531
diff --git a/src/terminal/c/terminal.zig b/src/terminal/c/terminal.zig
495532
new 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
+

scripts/build-wasm.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/bin/bash
2-
set -e
2+
set -euo pipefail
33

44
echo "🔨 Building ghostty-vt.wasm..."
55

0 commit comments

Comments
 (0)