Skip to content

Add OSC 8 hyperlink support (+ fix ST-terminated OSC and a broken main build) - #3

Open
codymullins wants to merge 2 commits into
mainfrom
osc8-hyperlinks
Open

Add OSC 8 hyperlink support (+ fix ST-terminated OSC and a broken main build)#3
codymullins wants to merge 2 commits into
mainfrom
osc8-hyperlinks

Conversation

@codymullins

Copy link
Copy Markdown
Collaborator

What

Adds OSC 8 hyperlink support to the terminal emulator, so a UI layer can detect, highlight, and open links that programs mark up (e.g. ls --hyperlink, gh).

Programs wrap links as ESC ] 8 ; params ; URI ST … text … ESC ] 8 ; ; ST. This tracks that state through the grid exactly like the existing pen tracks colors and attributes.

Changes

  • Cell.HyperlinkId — a small ushort id into a per-terminal URI table (0 = no link). Storing an id rather than the URI string keeps Cell a cheap value type to copy through scrollback and reflow.
  • Terminal — a hyperlink pen plus an id→URI table, deduped by the OSC 8 id= param (or by URI when none is given) so a link wrapped across rows — or repeated — reuses one id and the table stays bounded. Cleared on RIS.
  • VtHandler — parses OSC 8, extracting the id= param. The OSC payload cap grows from 512 to 4096 bytes for long URIs.
  • TerminalScreenSnapshot — carries the per-cell HyperlinkId, the Hyperlinks id→URI map, and a HyperlinkAt(col, row) helper.

Pre-existing bug fix (included)

Two commits, so they can be split if preferred:

  1. ZshPromptShim.Cleanup build fixmain has not compiled since f4f7964: the method lost its closing brace, and a catch (DirectoryNotFoundException) was added after catch (IOException) (a supertype), making it unreachable. Restored to its working shape. Without this the branch doesn't build.
  2. ST-terminated OSC dispatch — the "anywhere" ESC transition jumped to the Escape state without finalizing an open OSC string, so only the BEL terminator ever dispatched. OSC titles, cwd, and hyperlinks set via ST (ESC \) were silently dropped. The ESC transition now dispatches a pending OSC first. (Folded into the OSC 8 commit since the feature needs it.)

Tests

New HyperlinkTests cover parsing, id= grouping across rows, dedup, long URIs within the cap, malformed sequences, RIS reset, and the snapshot surface. Full suite: 223 passed.

API note

TerminalRenderCell and the TerminalScreenSnapshot record gain a field each (additive; property reads are unaffected, positional construction updated).

… catch

main has not compiled since f4f7964: the Cleanup method lost its closing
brace, and a catch (DirectoryNotFoundException) was added after
catch (IOException), which already catches it (subtype -> unreachable).
Restore the method to its working shape.
Terminals mark links with OSC 8 (ESC ] 8 ; params ; URI ST). Track them
through the emulator so a UI can hover/click them.

- Cell gains a HyperlinkId (a small id into a per-terminal URI table, kept
  cheap to copy through scrollback and reflow; 0 = no link).
- Terminal carries a hyperlink pen and an id->URI table, deduped by the
  OSC 8 id= param (or URI when absent) so a link wrapped across rows, or
  repeated, reuses one id and the table stays bounded.
- VtHandler parses OSC 8; the OSC payload cap grows to 4096 for long URIs.
- TerminalScreenSnapshot exposes the per-cell id, the URI map, and a
  HyperlinkAt(col,row) helper.

Also fixes a pre-existing bug: ST-terminated OSC (ESC \) never dispatched
because the 'anywhere' ESC transition jumped to the Escape state without
finalizing the OSC string. Only the BEL terminator worked, so OSC titles,
cwd, and now hyperlinks set via ST were silently dropped. The ESC
transition now dispatches a pending OSC first.

Adds HyperlinkTests covering parsing, id grouping, dedup, long URIs, RIS
reset, and the snapshot surface.
Copilot AI review requested due to automatic review settings July 8, 2026 00:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds OSC 8 hyperlink support end-to-end in the terminal emulator (parse → pen/state → per-cell storage → snapshot surface), while also fixing ST-terminated OSC dispatch and restoring a broken build in ZshPromptShim.Cleanup.

Changes:

  • Add per-cell hyperlink tracking via a small ushort HyperlinkId and a per-terminal id→URI table.
  • Extend the VT parser/handler to dispatch ST-terminated OSC correctly and to parse OSC 8 hyperlink sequences (with a larger OSC payload cap).
  • Expose hyperlink information through TerminalScreenSnapshot and add focused unit tests for the new behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
Dotty.Terminal/Terminal.cs Stores hyperlink pen state and manages the hyperlink id→URI table; writes hyperlink ids into cells.
Dotty.Terminal/Rendering/TerminalScreenSnapshot.cs Adds hyperlink table + HyperlinkAt helper to snapshots and includes per-cell HyperlinkId in render cells.
Dotty.Terminal/Parser/VtStateMachine.cs Increases OSC payload buffer and fixes ST-terminated OSC dispatch behavior.
Dotty.Terminal/Parser/VtHandler.cs Adds OSC 8 parsing and applies hyperlink open/close to the terminal pen state.
Dotty.Terminal/Hosting/ZshPromptShim.cs Fixes build break in Cleanup by restoring a missing closing brace / removing unreachable catch.
Dotty.Terminal/Cell.cs Adds HyperlinkId field to the cell and resets it appropriately.
Dotty.Terminal.Tests/HyperlinkTests.cs Adds test coverage for hyperlink parsing, dedup/grouping, reset behavior, and snapshot surface.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// Group by explicit id when present so a wrapped/multi-span link is one
// entry; otherwise group by URI so repeats of the same link don't grow
// the table without bound.
var key = string.IsNullOrEmpty(id) ? "u" + uri : "i" + id;
Comment on lines +760 to +767
linkId = _nextHyperlinkId;
// ushort ids top out at 65535 — vanishingly unlikely in a session, but
// stop allocating rather than wrap to 0 ("no link") or collide.
if (_nextHyperlinkId != ushort.MaxValue)
_nextHyperlinkId++;
_hyperlinkKeys[key] = linkId;
_hyperlinks[linkId] = uri;
}
Comment on lines +772 to +773
/// <summary>OSC 8 hyperlink table: cell <see cref="Cell.HyperlinkId"/> → URI.</summary>
public IReadOnlyDictionary<ushort, string> Hyperlinks => _hyperlinks;
Comment on lines +64 to +71
private static IReadOnlyDictionary<ushort, string> SnapshotHyperlinks(
IReadOnlyDictionary<ushort, string> live) =>
live.Count == 0
? EmptyHyperlinks
: new Dictionary<ushort, string>(live);

private static readonly IReadOnlyDictionary<ushort, string> EmptyHyperlinks =
new Dictionary<ushort, string>();
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.

2 participants