From d9b8c6a838ed2e03972f418065858f2494ee5310 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sun, 7 Aug 2022 07:35:46 -0700 Subject: [PATCH] linkHandler option and default handler --- demo/client.ts | 4 +-- src/browser/OscLinkProvider.ts | 34 +++++++++++++++++++++---- src/common/services/OptionsService.ts | 1 + src/common/services/OscLinkService.ts | 4 +-- src/common/services/Services.ts | 3 ++- typings/xterm.d.ts | 36 +++++++++++++++++++++++++++ 6 files changed, 72 insertions(+), 10 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 96f6b3351..cfebf3849 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -835,12 +835,12 @@ function addAnsiHyperlink() { term.writeln('\x1b]8;;https://xtermjs.org\x07https://xtermjs.org\x1b]8;;\x07\x1b[C<- null cell'); term.writeln(`\nAdjacent links:`); term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;https://xtermjs.org\x07xterm.js\x1b]8;;\x07'); - term.writeln(`\nShared ID link:`); + term.writeln(`\nShared ID link (underline should be shared):`); term.writeln('╔════╗'); term.writeln('║\x1b]8;id=testid;https://github.com\x07GitH\x1b]8;;\x07║'); term.writeln('║\x1b]8;id=testid;https://github.com\x07ub\x1b]8;;\x07 ║'); term.writeln('╚════╝'); - term.writeln(`\nWrapped link with no ID (not meant to share underline):`); + term.writeln(`\nWrapped link with no ID (not necessarily meant to share underline):`); term.writeln('╔════╗'); term.writeln('║ ║'); term.writeln('║ ║'); diff --git a/src/browser/OscLinkProvider.ts b/src/browser/OscLinkProvider.ts index 7fee62aff..96a2c395d 100644 --- a/src/browser/OscLinkProvider.ts +++ b/src/browser/OscLinkProvider.ts @@ -5,16 +5,22 @@ import { ILink, ILinkProvider } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; -import { IBufferService, IOscLinkService } from 'common/services/Services'; +import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services'; export class OscLinkProvider implements ILinkProvider { constructor( @IBufferService private readonly _bufferService: IBufferService, + @IOptionsService private readonly _optionsService: IOptionsService, @IOscLinkService private readonly _oscLinkService: IOscLinkService ) { } public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void { + // OSC links only work when a link handler is set + // if (this._optionsService.rawOptions.linkHandler === null) { + // return; + // } + const line = this._bufferService.buffer.lines.get(y - 1); if (!line) { callback(undefined); @@ -52,12 +58,14 @@ export class OscLinkProvider implements ILinkProvider { if (finishLink || (currentStart !== -1 && x === lineLength - 1)) { const text = this._oscLinkService.getLinkData(currentLinkId)?.uri; if (text) { + const linkHandler = this._optionsService.rawOptions.linkHandler; // OSC links always use underline and pointer decorations result.push({ text, // These ranges are 1-based range: { start: { + // TODO: Adjacent links aren't working correctly x: currentStart + 1, y }, @@ -67,10 +75,9 @@ export class OscLinkProvider implements ILinkProvider { y } }, - activate(e, text) { - console.log('activate!', text); - } - // TODO: Embedder API to handle hover + activate: linkHandler?.activate || defaultActivate, + hover: linkHandler?.hover, + leave: linkHandler?.leave }); } currentStart = -1; @@ -82,3 +89,20 @@ export class OscLinkProvider implements ILinkProvider { callback(result); } } + +function defaultActivate(e: MouseEvent, uri: string): void { + const answer = confirm(`Do you want to navigate to ${uri}?`); + if (answer) { + const newWindow = window.open(); + if (newWindow) { + try { + newWindow.opener = null; + } catch { + // no-op, Electron can throw + } + newWindow.location.href = uri; + } else { + console.warn('Opening link blocked as opener could not be cleared'); + } + } +} diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index ab9edfbfb..744903f17 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -24,6 +24,7 @@ export const DEFAULT_OPTIONS: Readonly = { fontWeightBold: 'bold', lineHeight: 1.0, letterSpacing: 0, + linkHandler: null, logLevel: 'info', scrollback: 1000, scrollSensitivity: 1, diff --git a/src/common/services/OscLinkService.ts b/src/common/services/OscLinkService.ts index a3f8af1d7..d3744229c 100644 --- a/src/common/services/OscLinkService.ts +++ b/src/common/services/OscLinkService.ts @@ -24,7 +24,6 @@ export class OscLinkService implements IOscLinkService { } public registerLink(data: IOscLinkData): number { - // TODO: Extend range where appropriate const buffer = this._bufferService.buffer; // Links with no id will only ever be registered a single time @@ -39,6 +38,7 @@ export class OscLinkService implements IOscLinkService { return entry.id; } + // Add the line to the link if it already exists const castData = data as Required; const key = this._getEntryIdKey(castData); const match = this._entriesWithId.get(key); @@ -47,6 +47,7 @@ export class OscLinkService implements IOscLinkService { return match.id; } + // Create the link const entry: IOscLinkEntryWithId = { id: this._nextId++, key: this._getEntryIdKey(castData), @@ -64,7 +65,6 @@ export class OscLinkService implements IOscLinkService { return; } if (link.lines.every(e => e.line !== y)) { - console.log(' add new line', y); link.lines.push(this._bufferService.buffer.addMarker(y)); } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 88a212520..63543c568 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -7,7 +7,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource, IDisposable, IColorRGB, IColor, CursorStyle, IOscLinkData } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; -import { IDecorationOptions, IDecoration } from 'xterm'; +import { IDecorationOptions, IDecoration, ILinkHandler } from 'xterm'; export const IBufferService = createDecorator('BufferService'); export interface IBufferService { @@ -223,6 +223,7 @@ export interface ITerminalOptions { fontWeightBold: FontWeight; letterSpacing: number; lineHeight: number; + linkHandler: ILinkHandler | null; logLevel: LogLevel; macOptionIsMeta: boolean; macOptionClickForcesSelection: boolean; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 05058d04d..2fa174217 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -128,6 +128,14 @@ declare module 'xterm' { */ lineHeight?: number; + /** + * The handler for OSC 8 hyperlinks. Links will use the `confirm` browser + * API if no link handler is set. Consider the security of users when using + * this, there should be some tooltip or prompt when hovering or activating + * the link. + */ + linkHandler?: ILinkHandler | null; + /** * What log level to use, this will log for all levels below and including * what is set: @@ -1101,6 +1109,34 @@ declare module 'xterm' { y: number; } + /** + * A link handler for OSC 8 hyperlinks. + */ + interface ILinkHandler { + /** + * Calls when the link is activated. + * @param event The mouse event triggering the callback. + * @param text The text of the link. + */ + activate(event: MouseEvent, text: string): void; + + /** + * Called when the mouse hovers the link. To use this to create a DOM-based hover tooltip, + * create the hover element within `Terminal.element` and add the `xterm-hover` class to it, + * that will cause mouse events to not fall through and activate other links. + * @param event The mouse event triggering the callback. + * @param text The text of the link. + */ + hover?(event: MouseEvent, text: string): void; + + /** + * Called when the mouse leaves the link. + * @param event The mouse event triggering the callback. + * @param text The text of the link. + */ + leave?(event: MouseEvent, text: string): void; + } + /** * A custom link provider. */