How to Implement Hyperlinks in Ghostty Using OSC 8 Sequences
Ghostty treats OSC 8 hyperlinks as first-class terminal commands through a dedicated parser in src/terminal/osc/parsers/hyperlink.zig, cursor-based state tracking in Screen.zig, and optional re-emission via the formatter, enabling applications to embed clickable URIs that persist across screen operations.
Ghostty is a GPU-accelerated terminal emulator written in Zig that implements OSC 8 hyperlinks as native protocol commands rather than decorative text attributes. This architecture allows applications to embed clickable URIs using standard escape sequences, while Ghostty maintains rigorous state management to track active links per cursor position and per page.
Architecture Overview
Ghostty's OSC 8 implementation follows a four-stage pipeline: parsing raw escape sequences into structured commands, dispatching those commands through the terminal stream handler, tracking hyperlink state in the cursor and page structures, and optionally re-emitting sequences when rendering screen content.
Parsing the Raw Sequence
The entry point for OSC 8 support is src/terminal/osc/parsers/hyperlink.zig, which parses the raw string \x1b]8;[params];[URI]\x1b\\ into a structured Command. The parser distinguishes between two command types:
hyperlink_start— Contains the target URI and an optional explicit IDhyperlink_end— Signals the termination of the current hyperlink region
The parser includes exhaustive unit tests covering normal links, explicit IDs, empty IDs, malformed key-value pairs, and empty URIs (lines 59–164).
Dispatching Commands to the Handler
Once parsed, commands travel through src/terminal/stream.zig via the oscDispatch mechanism (around lines 33–44). The dispatcher forwards parsed commands to the terminal handler using a switch statement:
.hyperlink_start => |v| self.handler.vt(.start_hyperlink, .{ .uri = v.uri, .id = v.id });
.hyperlink_end => self.handler.vt(.end_hyperlink, {});
This dispatch converts OSC events into internal virtual terminal calls, passing the URI and optional ID to the underlying handler in src/terminal/Terminal.zig.
Tracking Link State in the Cursor and Page
The handler stores active hyperlink data in two locations. In src/terminal/Screen.zig (line 152), the cursor structure tracks the current hyperlink via:
hyperlink_id: hyperlink.Id = 0,
When a hyperlink_start command executes, Screen.setHyperlink registers the hyperlink in the page's hyperlink_set (managed in src/terminal/page.zig) and updates cursor.hyperlink_id to reference the active entry. The page stores the URI and ID (explicit or implicit), while the cursor holds only the reference ID.
Upon receiving hyperlink_end, Ghostty releases the ID and resets cursor.hyperlink_id to 0, ending the hyperlink region.
Re-emitting OSC 8 Sequences
When formatting screen content for export or inspection, src/terminal/formatter.zig (lines 72–88) can reproduce the exact OSC 8 sequences that created the hyperlinks:
if (self.extra.hyperlink) {
const cursor = &self.screen.cursor;
if (cursor.hyperlink) |link| {
switch (link.id) {
.explicit => |id| try writer.print("\x1b]8;id={s};{s}\x1b\\", .{ id, link.uri }),
.implicit => try writer.print("\x1b]8;;{s}\x1b\\", .{ link.uri }),
}
}
}
This capability allows Ghostty to faithfully reconstruct hyperlinks when dumping screen buffers or streaming content to another terminal.
Practical Implementation Examples
Sending Hyperlinks from an Application
Applications running in Ghostty can emit OSC 8 sequences directly to create clickable regions. The following Zig code demonstrates opening a hyperlink with an explicit ID, writing text, then closing the link:
const std = @import("std");
// Open hyperlink with explicit ID
std.debug.print("\x1b]8;id=myLink;https://example.com\x1b\\", .{});
// Text within the hyperlink region
std.debug.print("Visit example.com", .{});
// Close the hyperlink
std.debug.print("\x1b]8;;\x1b\\", .{});
When Ghostty receives this output, the parser creates a hyperlink_start command, stores the URI and ID in the page's hyperlink set, and associates subsequent text with the active cursor hyperlink ID.
Rendering Screens with Active Hyperlinks
To reproduce a screen dump including hyperlink sequences:
var formatter = ScreenFormatter.init(screen, opts);
formatter.extra = .{ .hyperlink = true, .style = true, .cursor = true };
try formatter.format(&writer);
With extra.hyperlink enabled, the formatter emits OSC 8 start sequences when encountering hyperlink cells and automatically emits the end sequence (\x1b]8;;\x1b\\) when the cursor moves out of the link region.
Handling Hyperlink Events in Custom Code
Custom terminal handlers can intercept hyperlink commands through the start_hyperlink virtual terminal call:
fn onStartHyperlink(uri: [:0]const u8, id: ?[:0]const u8) void {
// uri contains the target address
// id is null for implicit links or contains the explicit ID string
std.log.info("Hyperlink started: {s} (id={?})", .{ uri, id });
}
This access enables implementing click-to-open functionality or clipboard integration for hyperlink targets.
Summary
- Parsing:
src/terminal/osc/parsers/hyperlink.zigconverts raw OSC 8 strings intohyperlink_startandhyperlink_endcommands with full test coverage (lines 59–164). - Dispatch:
src/terminal/stream.zigroutes parsed commands to the terminal handler viaoscDispatch. - State Management:
src/terminal/Screen.zigtracks the active hyperlink ID in the cursor (line 152), whilesrc/terminal/page.zigmanages the underlying hyperlink set containing URIs and IDs. - Re-emission:
src/terminal/formatter.zig(lines 72–88) can reproduce exact OSC 8 sequences when dumping screen content, preserving hyperlink integrity across terminal streams.
Frequently Asked Questions
What is the exact OSC 8 syntax supported by Ghostty?
Ghostty supports the standard OSC 8 format: \x1b]8;[params];[URI]\x1b\\ to open a link and \x1b]8;;\x1b\\ to close it. The [params] field optionally accepts id=string to define an explicit hyperlink identifier; otherwise, Ghostty treats the hyperlink as implicit. According to the source code in src/terminal/osc/parsers/hyperlink.zig, the parser handles malformed key-value pairs and empty URIs gracefully.
How does Ghostty store hyperlink state internally?
Ghostty stores hyperlink state using a two-tier system. The cursor structure in src/terminal/Screen.zig maintains an integer hyperlink_id field (default 0 indicating no active link) that references entries in a per-page hyperlink_set managed in src/terminal/page.zig. This set contains the actual URI strings and optional explicit IDs. When a hyperlink ends, the cursor ID resets to 0 and the page releases the associated hyperlink resources.
Can applications define explicit hyperlink IDs?
Yes. Applications can specify explicit IDs using the syntax \x1b]8;id=myCustomId;https://example.com\x1b\\. As implemented in src/terminal/osc/parsers/hyperlink.zig, these IDs propagate through the dispatch system to src/terminal/Terminal.zig, where they are stored in the page's hyperlink set. Explicit IDs allow multiple separate text regions to link to the same URI while maintaining distinct identity for terminal applications that support hyperlink hovering or auditing.
Does Ghostty support hyperlink re-emission in screen dumps?
Yes. Ghostty's formatter in src/terminal/formatter.zig (lines 72–88) supports re-emission when the extra.hyperlink configuration is enabled. The formatter checks the current cursor's hyperlink state and outputs the appropriate OSC 8 start sequence including the explicit ID if present. When moving to cells outside the hyperlink region, it automatically emits the end sequence \x1b]8;;\x1b\\, allowing downstream consumers to reconstruct the hyperlink structure faithfully.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →