Skip to content

Commit

Permalink
Updated prototype for hyperlink ansi ecsapes (issue xtermjs#1134).
Browse files Browse the repository at this point in the history
  • Loading branch information
PerBothner committed Jan 15, 2019
1 parent 509ce5f commit 47845d9
Show file tree
Hide file tree
Showing 4 changed files with 89 additions and 37 deletions.
38 changes: 36 additions & 2 deletions src/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
* @license MIT
*/

import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types';
import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal, ILinkOptions } from './Types';
import { C0, C1 } from './common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_ATTR_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser';
Expand Down Expand Up @@ -115,6 +115,9 @@ class DECRQSS implements IDcsHandler {
*/
export class InputHandler extends Disposable implements IInputHandler {
private _surrogateFirst: string;
private _linkStartX: number;
private _linkStartY: number;
private _linkUri: string;

constructor(
protected _terminal: IInputHandlingTerminal,
Expand Down Expand Up @@ -219,6 +222,8 @@ export class InputHandler extends Disposable implements IInputHandler {
// 5 - Change Special Color Number
// 6 - Enable/disable Special Color Number c
// 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939)
// 8 - create hyperlink (https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)
this._parser.setOscHandler(8, (data) => this.createHyperLink(data));
// 10 - Change VT100 text foreground color to Pt.
// 11 - Change VT100 text background color to Pt.
// 12 - Change text cursor color to Pt.
Expand Down Expand Up @@ -1906,6 +1911,35 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.handleTitle(data);
}

public createHyperLink(data: string): void {
const m = data.match(/([^;]*);(.*)/);
if (!m) {
return;
}
// const params = m[1];
const uri = m[2];
if (uri) {
this._linkStartX = this._terminal.buffer.x;
this._linkStartY = this._terminal.buffer.y;
this._linkUri = uri;
} else {
const startX = this._linkStartX;
const startY = this._linkStartY;
const endX = this._terminal.buffer.x;
const endY = this._terminal.buffer.y;
const uri = this._linkUri;
const line = this._terminal.buffer.lines.get(startY);
const char = line.get(startX);
let fg: number | undefined;
if (char) {
const attr: number = char[CHAR_DATA_ATTR_INDEX];
fg = (attr >> 9) & 0x1ff;
}
const options: ILinkOptions = {};
this._terminal.linkifier.doAddLink(startX, startY, endX, endY, options, uri, fg);
}
}

/**
* ESC E
* C1.NEL
Expand Down
41 changes: 27 additions & 14 deletions src/Linkifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { IMouseZoneManager } from './ui/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkOptions, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types';
import { MouseZone } from './ui/MouseZoneManager';
import { EventEmitter } from './common/EventEmitter';
import { CHAR_DATA_ATTR_INDEX } from './Buffer';
Expand Down Expand Up @@ -34,6 +34,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
private _rowsTimeoutId: number;
private _nextLinkMatcherId = 0;
private _rowsToLinkify: { start: number, end: number };
private _pendingLinks = new Array();

constructor(
protected _terminal: ITerminal
Expand Down Expand Up @@ -118,6 +119,9 @@ export class Linkifier extends EventEmitter implements ILinkifier {
this._doLinkifyRow(lineData.range.first, lineData.content, this._linkMatchers[i]);
}
}
for (let i = this._pendingLinks.length; --i >= 0; ) {
this._mouseZoneManager.add(this._pendingLinks.pop());
}

this._rowsToLinkify.start = null;
this._rowsToLinkify.end = null;
Expand All @@ -140,11 +144,11 @@ export class Linkifier extends EventEmitter implements ILinkifier {
const matcher: ILinkMatcher = {
id: this._nextLinkMatcherId++,
regex,
handler,
handler: handler || options.handler,
matchIndex: options.matchIndex,
validationCallback: options.validationCallback,
hoverTooltipCallback: options.tooltipCallback,
hoverLeaveCallback: options.leaveCallback,
tooltipCallback: options.tooltipCallback,
leaveCallback: options.leaveCallback,
willLinkActivate: options.willLinkActivate,
priority: options.priority || 0
};
Expand Down Expand Up @@ -273,15 +277,19 @@ export class Linkifier extends EventEmitter implements ILinkifier {
x2 = this._terminal.cols;
y2--;
}
this._mouseZoneManager.add(this._createLink(x1, y1, x2, y2, matcher, uri, fg));
}

this._mouseZoneManager.add(new MouseZone(
private _createLink(x1: number, y1: number, x2: number, y2: number,
options: ILinkOptions, uri: string, fg: number): MouseZone {
return new MouseZone(
x1 + 1,
y1 + 1,
x2 + 1,
y2 + 1,
e => {
if (matcher.handler) {
return matcher.handler(e, uri);
if (options.handler) {
return options.handler(e, uri);
}
window.open(uri, '_blank');
},
Expand All @@ -291,24 +299,29 @@ export class Linkifier extends EventEmitter implements ILinkifier {
},
e => {
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
if (matcher.hoverTooltipCallback) {
matcher.hoverTooltipCallback(e, uri);
if (options.tooltipCallback) {
options.tooltipCallback(e, uri);
}
},
() => {
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
this._terminal.element.classList.remove('xterm-cursor-pointer');
if (matcher.hoverLeaveCallback) {
matcher.hoverLeaveCallback();
if (options.leaveCallback) {
options.leaveCallback();
}
},
e => {
if (matcher.willLinkActivate) {
return matcher.willLinkActivate(e, uri);
if (options.willLinkActivate) {
return options.willLinkActivate(e, uri);
}
return true;
}
));
);
}

doAddLink(x1: number, y1: number, x2: number, y2: number,
options: ILinkOptions, uri: string, fg: number): void {
this._pendingLinks.push(this._createLink(x1, y1, x2, y2, options, uri, fg));
}

private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkHoverEvent {
Expand Down
46 changes: 25 additions & 21 deletions src/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
export type CharData = [number, string, number, number];
export type LineData = CharData[];

// FIXME rename to LinkHandler
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;

Expand Down Expand Up @@ -59,6 +60,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
sgrMouse: boolean;
urxvtMouse: boolean;
cursorHidden: boolean;
linkifier: ILinkifier;

buffers: IBufferSet;
buffer: IBuffer;
Expand Down Expand Up @@ -182,16 +184,31 @@ export interface IInputHandler {
ESC ~ */ setgLevel(level: number): void;
}

export interface ILinkMatcher {
export interface ILinkOptions {
handler?: LinkMatcherHandler;
/**
* A callback that fires when the mouse hovers over a link.
*/
tooltipCallback?: LinkMatcherHandler;
/**
* A callback that fires when the mouse leaves a link that was hovered.
*/
leaveCallback?: () => void;
/**
* A callback that fires when the mousedown and click events occur that
* determines whether a link will be activated upon click. This enables
* only activating a link when a certain modifier is held down, if not the
* mouse event will continue propagation (eg. double click to select word).
*/
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}

export interface ILinkMatcher extends ILinkOptions {
id: number;
regex: RegExp;
handler: LinkMatcherHandler;
hoverTooltipCallback?: LinkMatcherHandler;
hoverLeaveCallback?: () => void;
matchIndex?: number;
validationCallback?: LinkMatcherValidationCallback;
priority?: number;
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}

export interface ILinkHoverEvent {
Expand Down Expand Up @@ -326,9 +343,11 @@ export interface ILinkifier extends IEventEmitter {
linkifyRows(start: number, end: number): void;
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): boolean;
doAddLink(x1: number, y1: number, x2: number, y2: number,
matcher: ILinkOptions, uri: string, fg: number): void;
}

export interface ILinkMatcherOptions {
export interface ILinkMatcherOptions extends ILinkOptions {
/**
* The index of the link from the regex.match(text) call. This defaults to 0
* (for regular expressions without capture groups).
Expand All @@ -339,27 +358,12 @@ export interface ILinkMatcherOptions {
* false if invalid.
*/
validationCallback?: LinkMatcherValidationCallback;
/**
* A callback that fires when the mouse hovers over a link.
*/
tooltipCallback?: LinkMatcherHandler;
/**
* A callback that fires when the mouse leaves a link that was hovered.
*/
leaveCallback?: () => void;
/**
* The priority of the link matcher, this defines the order in which the link
* matcher is evaluated relative to others, from highest to lowest. The
* default value is 0.
*/
priority?: number;
/**
* A callback that fires when the mousedown and click events occur that
* determines whether a link will be activated upon click. This enables
* only activating a link when a certain modifier is held down, if not the
* mouse event will continue propagation (eg. double click to select word).
*/
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}

export interface IBrowser {
Expand Down
1 change: 1 addition & 0 deletions src/ui/TestUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
sgrMouse: boolean;
urxvtMouse: boolean;
cursorHidden: boolean;
linkifier: ILinkifier;
buffers: IBufferSet;
buffer: IBuffer = new MockBuffer();
viewport: IViewport;
Expand Down

0 comments on commit 47845d9

Please sign in to comment.