feat(tui): use OSC 8 hyperlinks in Markdown when terminal supports them (#3248)
TerminalCapabilities already tracks hyperlinks: boolean and returns true for Ghostty, Kitty, WezTerm, and iTerm2, but nothing generated OSC 8 sequences. This completes that stub. Changes to packages/tui: - terminal-image.ts: add hyperlink(text, url) and setCapabilities() - index.ts: export hyperlink and setCapabilities - utils.ts: extend AnsiCodeTracker to track active OSC 8 URLs - process() now handles OSC 8 open/close sequences - getActiveCodes() re-emits the OSC 8 open at each line start - getLineEndReset() closes the OSC 8 hyperlink before each line break This ensures hyperlinks wrap correctly across multiple lines. - components/markdown.ts: link renderer uses hyperlink() when getCapabilities().hyperlinks is true; falls back to (url) text - Tests: new wrap-ansi tests for OSC 8 line-wrapping; terminal-image tests for hyperlink(); markdown tests covering both code paths; table-cell width test pinned to hyperlinks:false (checks raw columns) closes #3239 Co-authored-by: AI (Pi/Claude Sonnet 4.6) <noreply@pi.dev> Co-authored-by: Mario Zechner <badlogicgames@gmail.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
import assert from "node:assert";
|
||||
import { describe, it } from "node:test";
|
||||
import { isImageLine } from "../src/terminal-image.js";
|
||||
import { hyperlink, isImageLine } from "../src/terminal-image.js";
|
||||
|
||||
describe("isImageLine", () => {
|
||||
describe("iTerm2 image protocol", () => {
|
||||
@@ -151,3 +151,29 @@ describe("isImageLine", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("hyperlink", () => {
|
||||
it("wraps text in OSC 8 open and close sequences", () => {
|
||||
const result = hyperlink("click me", "https://example.com");
|
||||
assert.strictEqual(result, "\x1b]8;;https://example.com\x1b\\click me\x1b]8;;\x1b\\");
|
||||
});
|
||||
|
||||
it("preserves ANSI styling inside the hyperlink", () => {
|
||||
const styled = "\x1b[4m\x1b[34mclick me\x1b[0m";
|
||||
const result = hyperlink(styled, "https://example.com");
|
||||
assert.ok(result.startsWith("\x1b]8;;https://example.com\x1b\\"));
|
||||
assert.ok(result.includes(styled));
|
||||
assert.ok(result.endsWith("\x1b]8;;\x1b\\"));
|
||||
});
|
||||
|
||||
it("works with empty text", () => {
|
||||
const result = hyperlink("", "https://example.com");
|
||||
assert.strictEqual(result, "\x1b]8;;https://example.com\x1b\\\x1b]8;;\x1b\\");
|
||||
});
|
||||
|
||||
it("works with file:// URIs", () => {
|
||||
const result = hyperlink("README.md", "file:///home/user/README.md");
|
||||
assert.ok(result.includes("file:///home/user/README.md"));
|
||||
assert.ok(result.includes("README.md"));
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user