[GH-ISSUE #735] Hyperlinks spanning wrapped lines are not grouped (missing 'id' parameter in OSC 8) #199

Closed
opened 2026-03-02 23:45:13 +03:00 by kerem · 0 comments
Owner

Originally created by @mugnimaestra on GitHub (Feb 24, 2026).
Original GitHub issue: https://github.com/anomalyco/opentui/issues/735

Bug Description

When a hyperlink wraps across multiple terminal lines, Cmd/Ctrl+Click only selects the portion on the clicked line instead of the full URL. This happens because the OSC 8 hyperlink escape sequences emitted by the renderer are missing the id parameter, so the terminal treats the link segments on each row as separate hyperlinks rather than one continuous link.

Video Demonstration

https://github.com/user-attachments/assets/697edfef-6e11-4147-93fa-f7ae1d6f3441

Root Cause

In packages/core/src/zig/renderer.zig at line 670, the OSC 8 open sequence does not include an id parameter:

// Current (line 670):
writer.print("\x1b]8;;{s}\x1b\\", .{url_bytes}) catch {};

Per the OSC 8 hyperlink spec, when a hyperlink is split across multiple rows (e.g., link text ends at end of row N, padding spaces close it, then it reopens on row N+1), the terminal needs the id parameter to recognize that both segments belong to the same logical hyperlink.

Proposed Fix

Add the link ID to the OSC 8 open sequence:

// Fixed:
writer.print("\x1b]8;id={d};{s}\x1b\\", .{currentLinkId, url_bytes}) catch {};

currentLinkId is already tracked in the renderer's state and is the correct identifier to use here.

Rendering Flow (for context)

  1. JS layer: textBufferSetStyledText() receives chunks with optional link: { url } property
  2. Link encoding: linkAlloc() allocates URL in Zig LinkPool, returns numeric ID; ID is packed into the upper 24 bits of attributes u32
  3. Style registration: setStyledText() in text-buffer.zig calls registerStyle() with full attributes (including link_id bits)
  4. Word wrapping: text-buffer-view.zig calculateVirtualLinesGeneric() — wrapping is link-unaware, splits purely on geometry
  5. Cell rendering: buffer.zig drawTextBufferInternal() resolves style spans per-cell; link_id flows through resolved_style.attributes into cell attributes
  6. Terminal output: renderer.zig prepareRenderFrame() emits OSC 8 sequences — currentLinkId persists across rows (never reset between rows), but the missing id param prevents the terminal from grouping the segments

Test Coverage

Existing hyperlink tests in packages/core/src/zig/tests/renderer_test.zig (lines 621-760) cover basic hyperlink rendering but do not test multi-line wrapped hyperlinks. A new test should verify that wrapped hyperlinks emit matching id parameters.

Originally created by @mugnimaestra on GitHub (Feb 24, 2026). Original GitHub issue: https://github.com/anomalyco/opentui/issues/735 ## Bug Description When a hyperlink wraps across multiple terminal lines, Cmd/Ctrl+Click only selects the portion on the clicked line instead of the full URL. This happens because the OSC 8 hyperlink escape sequences emitted by the renderer are missing the `id` parameter, so the terminal treats the link segments on each row as separate hyperlinks rather than one continuous link. ## Video Demonstration https://github.com/user-attachments/assets/697edfef-6e11-4147-93fa-f7ae1d6f3441 ## Root Cause In `packages/core/src/zig/renderer.zig` at line 670, the OSC 8 open sequence does not include an `id` parameter: ```zig // Current (line 670): writer.print("\x1b]8;;{s}\x1b\\", .{url_bytes}) catch {}; ``` Per the [OSC 8 hyperlink spec](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda), when a hyperlink is split across multiple rows (e.g., link text ends at end of row N, padding spaces close it, then it reopens on row N+1), the terminal needs the `id` parameter to recognize that both segments belong to the same logical hyperlink. ## Proposed Fix Add the link ID to the OSC 8 open sequence: ```zig // Fixed: writer.print("\x1b]8;id={d};{s}\x1b\\", .{currentLinkId, url_bytes}) catch {}; ``` `currentLinkId` is already tracked in the renderer's state and is the correct identifier to use here. ## Rendering Flow (for context) 1. **JS layer**: `textBufferSetStyledText()` receives chunks with optional `link: { url }` property 2. **Link encoding**: `linkAlloc()` allocates URL in Zig `LinkPool`, returns numeric ID; ID is packed into the upper 24 bits of `attributes` u32 3. **Style registration**: `setStyledText()` in `text-buffer.zig` calls `registerStyle()` with full attributes (including link_id bits) 4. **Word wrapping**: `text-buffer-view.zig` `calculateVirtualLinesGeneric()` — wrapping is link-unaware, splits purely on geometry 5. **Cell rendering**: `buffer.zig` `drawTextBufferInternal()` resolves style spans per-cell; link_id flows through `resolved_style.attributes` into cell attributes 6. **Terminal output**: `renderer.zig` `prepareRenderFrame()` emits OSC 8 sequences — **`currentLinkId` persists across rows** (never reset between rows), but the missing `id` param prevents the terminal from grouping the segments ## Test Coverage Existing hyperlink tests in `packages/core/src/zig/tests/renderer_test.zig` (lines 621-760) cover basic hyperlink rendering but do **not** test multi-line wrapped hyperlinks. A new test should verify that wrapped hyperlinks emit matching `id` parameters.
kerem 2026-03-02 23:45:13 +03:00
  • closed this issue
  • added the
    core
    bug
    labels
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/opentui#199
No description provided.