# How to Implement Hyperlinks in Ghostty Using OSC 8 Sequences

> Learn how to implement OSC 8 hyperlinks in Ghostty. Discover how Ghostty's dedicated parser and cursor-based state tracking enable clickable URIs.

- Repository: [Ghostty/ghostty](https://github.com/ghostty-org/ghostty)
- Tags: how-to-guide
- Published: 2026-05-01

---

**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 ID
- `hyperlink_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:

```zig
.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:

```zig
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:

```zig
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:

```zig
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:

```zig
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:

```zig
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.zig` converts raw OSC 8 strings into `hyperlink_start` and `hyperlink_end` commands with full test coverage (lines 59–164).
- **Dispatch**: `src/terminal/stream.zig` routes parsed commands to the terminal handler via `oscDispatch`.
- **State Management**: `src/terminal/Screen.zig` tracks the active hyperlink ID in the cursor (line 152), while `src/terminal/page.zig` manages 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.