Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion packages/void/src/cli/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,21 @@ export function createSpinner(
};
}

/** The URL as the box title shows it: whole, or its longest start cut with "…". */
function shownUrl(chunk: string, url: string): string | undefined {
if (chunk.includes(url)) {
return url;
}
const plain = stripVTControlCharacters(chunk);
for (let end = url.length - 1; end > 0; end--) {
const label = `${url.slice(0, end)}…`;
if (plain.includes(label)) {
return label;
}
}
return undefined;
}

/** Narrowest terminal the shared box renderer can draw a note in, guide bar included. */
const MIN_NOTE_COLUMNS = 6;

Expand Down Expand Up @@ -127,7 +142,10 @@ export function note(...args: Parameters<typeof promptsNote>): void {
// Insert the link after layout: the renderer counts OSC 8 destinations
// toward the title width and can otherwise truncate a URL that fits.
if (header && urlTitle) {
chunk = chunk.replace(` ${urlTitle} `, ` ${formatUrl(urlTitle)} `);
const label = shownUrl(chunk, urlTitle);
if (label) {
chunk = chunk.replace(label, () => terminalLink(label, urlTitle, { fallback: false }));
}
}
header = false;
return output.write(chunk);
Expand Down
13 changes: 7 additions & 6 deletions packages/void/test/integration/terminal-links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { describe, expect, it } from 'vite-plus/test';
const outputModule = new URL('../../src/cli/output.ts', import.meta.url).href;
const url = 'https://app.example.com/hello?name=void&mode=preview#content';

function render(hyperlinks: boolean): { url: string; named: string; box: string } {
function render(hyperlinks: boolean, columns = 80): { url: string; named: string; box: string } {
const script = `
import { formatLink, formatUrl, note } from ${JSON.stringify(outputModule)};
const url = ${JSON.stringify(url)};
let box = '';
note('Static Site — 1 asset', url, {
output: { columns: 80, write(chunk) { box += chunk; return true; } },
output: { columns: ${columns}, write(chunk) { box += chunk; return true; } },
});
console.log(JSON.stringify({ url: formatUrl(url), named: formatLink('Dashboard', url), box }));
`;
Expand All @@ -31,15 +31,16 @@ console.log(JSON.stringify({ url: formatUrl(url), named: formatLink('Dashboard',
}

describe('terminal links (real capability detection)', () => {
it('emits OSC 8 links with the exact destination and keeps box padding outside the link', () => {
const output = render(true);
it.each([40, 80])('keeps the complete link destination in a %s-column box title', (columns) => {
const output = render(true, columns);
// ansi-escapes uses BEL to delimit OSC 8 control sequences.
const opening = `\u001b]8;;${url}\u0007`;
const closing = '\u001b]8;;\u0007';
expect(output.url).toBe(`${opening}${url}${closing}`);
expect(output.named).toBe(`${opening}Dashboard${closing}`);
expect(output.box).toContain(` ${opening}${url}${closing} `);
expect(stripVTControlCharacters(output.box).split('\n')[0]).toContain(` ${url} `);
const label = columns === 40 ? `${url.slice(0, 34)}…` : url;
expect(output.box).toContain(` ${opening}${label}${closing}`);
expect(stripVTControlCharacters(output.box).split('\n')[0]).toContain(` ${label}`);
});

it('prints each URL once as plain text when hyperlinks are unsupported or output is redirected', () => {
Expand Down
Loading