Skip to content

Comments

feat: enable OSC 8 hyperlink clicking with Cmd/Ctrl modifier#117

Open
slang25 wants to merge 2 commits intocoder:mainfrom
slang25:slang25/osc8-hyperlink-click
Open

feat: enable OSC 8 hyperlink clicking with Cmd/Ctrl modifier#117
slang25 wants to merge 2 commits intocoder:mainfrom
slang25:slang25/osc8-hyperlink-click

Conversation

@slang25
Copy link
Contributor

@slang25 slang25 commented Jan 11, 2026

Summary

  • Add support for clicking OSC 8 hyperlinks (Cmd/Ctrl+click opens the link)
  • Add getHyperlinkUri API to WASM to retrieve the actual URI for hyperlinked cells
  • Fix link caching to handle multiple hyperlinks on the same line correctly

Problem

OSC 8 hyperlinks (created with escape sequences like \e]8;;http://example.com\e\\Link text\e]8;;\e\\) were visually detected and underlined on hover, but Cmd+clicking them did nothing. Additionally, when multiple hyperlinks existed on the same line, all links would incorrectly open the same URL.

Root Causes

  1. Missing WASM API: The WASM didn't expose a function to retrieve the hyperlink URI. The hyperlink_id field in cells was just a boolean (0 or 1) indicating presence of a hyperlink, not a unique identifier.

  2. Incorrect link caching: The LinkDetector cached links by h${hyperlinkId}, but since all hyperlinks have hyperlink_id=1, all links on a line would share the same cache key, causing only the last discovered link to be returned for any hyperlink cell.

Solution

  1. Added ghostty_terminal_get_hyperlink_uri(row, col) to the WASM API that looks up the actual URI from Ghostty's internal hyperlink storage.

  2. Updated OSC8LinkProvider to use the new WASM API to fetch URIs, with proper coordinate conversion from buffer rows to viewport rows.

  3. Fixed LinkDetector to cache links by position range (r${row}:${startX}-${endX}) instead of hyperlink_id, ensuring each link is cached uniquely.

Test Plan

  • Create OSC 8 hyperlink: printf '\e]8;;http://example.com\e\\Link text\e]8;;\e\\\n'
  • Verify link is underlined on hover
  • Verify Cmd+click opens the URL
  • Create multiple links on one line: printf '\e]8;;http://google.com\e\\Google\e]8;;\e\\ and \e]8;;http://github.com\e\\GitHub\e]8;;\e\\\n'
  • Verify each link opens its respective URL when Cmd+clicked
  • Verify plain text URLs (regex detection) still work

🤖 Generated with Claude Code

@sreya
Copy link
Contributor

sreya commented Jan 13, 2026

@codex review

@sreya
Copy link
Contributor

sreya commented Jan 13, 2026

@slang25 seems the updates to the patch do not cleanly apply would you mind fixing that up?

Run ./scripts/build-wasm.sh
🔨 Building ghostty-vt.wasm...
✓ Found Zig 0.15.2
📦 Initializing Ghostty submodule...
🔧 Applying WASM API patch...
error: corrupt patch at line 354
❌ Patch doesn't apply cleanly
Ghostty may have changed. Check patches/ghostty-wasm-api.patch

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05bc928b79

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

lib/ghostty.ts Outdated
Comment on lines 620 to 622
const bufSize = 2048; // URLs can be long
const bufPtr = this.exports.ghostty_wasm_alloc_u8_array(bufSize);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry OSC8 URI fetch when buffer is too small

The URI lookup uses a fixed 2048‑byte buffer and returns null on any non‑positive result. The new WASM API returns -1 when the buffer is too small, so OSC 8 links with long URIs (e.g., signed URLs or large query strings) will never be detected/clickable because the call always fails. Consider a retry path with a larger buffer (or a length query) so long hyperlinks still resolve.

Useful? React with 👍 / 👎.

Comment on lines 57 to 61
const scrollbackLength = this.terminal.wasmTerm.getScrollbackLength();
const viewportRow = y - scrollbackLength;

// Find the full extent of this link (may span multiple lines)
const range = this.findLinkRange(hyperlinkId, y, x);
// Skip if this row is in scrollback (not in active viewport)
if (viewportRow < 0) continue;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Don’t skip OSC8 links that are in scrollback

When the terminal is scrolled up, rows mapped into the scrollback region are explicitly skipped, so OSC 8 links in visible scrollback lines will never be returned to the LinkDetector and Cmd/Ctrl‑click won’t open them. This is user‑visible when you scroll back to click a link in output history. If scrollback should be interactive like regex URLs, the provider needs a scrollback‑capable URI lookup (or a fallback) instead of early‑returning.

Useful? React with 👍 / 👎.

@slang25 slang25 force-pushed the slang25/osc8-hyperlink-click branch 3 times, most recently from 30ecf0f to 9c94f68 Compare January 13, 2026 00:26
Add support for clicking OSC 8 hyperlinks in the terminal. This involves:

1. Add ghostty_terminal_get_hyperlink_uri() to the WASM API to retrieve the
   actual URI for cells marked with hyperlinks. The hyperlink_id field is
   just a boolean indicator; the real URI is stored in Ghostty's internal
   hyperlink set and must be looked up via this new function.

2. Update OSC8LinkProvider to use the new WASM API, with proper coordinate
   conversion from buffer rows to viewport rows (accounting for scrollback).

3. Fix LinkDetector to cache links by position range rather than hyperlink_id,
   since all hyperlinks incorrectly shared the same ID value (1), causing
   multiple links on one line to all open the same URL.

Now Cmd+clicking (Mac) or Ctrl+clicking (Windows/Linux) an OSC 8 hyperlink
correctly opens that specific link's URI.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@slang25 slang25 force-pushed the slang25/osc8-hyperlink-click branch from 9c94f68 to e593548 Compare January 13, 2026 00:33
@ibetitsmike
Copy link

@codex review again

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e593548680

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 111 to 112
const cacheKey = `r${s.y}:${s.x}-${e.x}`;
this.linkCache.set(cacheKey, link);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve OSC8 target when regex matches same range

Caching links only by r${row}:${startX}-${endX} lets later providers overwrite earlier ones for identical ranges. In scanRow, providers are queried in registration order (OSC8 first, regex second), so an OSC8 hyperlink whose visible text is also URL-shaped will be replaced by the regex link, and Ctrl/Cmd+click opens the displayed URL instead of the OSC8 URI. This breaks the intended precedence of explicit OSC8 links and can send users to the wrong destination.

Useful? React with 👍 / 👎.

Comment on lines 60 to 61
// Skip if this row is in scrollback (not in active viewport)
if (viewportRow < 0) continue;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve OSC8 URIs for scrollback rows

This early-continue drops every row that lives in scrollback (y < scrollbackLength), but LinkDetector passes absolute buffer rows while users hover/click scrolled history. As a result, OSC8 links become non-clickable once they scroll off the active viewport (unless regex detection happens to recover them), which is a user-visible regression for terminal logs containing labeled OSC8 links.

Useful? React with 👍 / 👎.

- Add scrollback support so OSC 8 links remain clickable after scrolling
- Retry with larger buffers (2KB/8KB/32KB) for long URIs
- Preserve OSC8 link precedence over regex when ranges overlap

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants