OSC link progress

This commit is contained in:
Daniel Imms
2022-08-07 05:08:13 -07:00
parent c021571194
commit 91d46dae50
9 changed files with 237 additions and 15 deletions
@@ -356,7 +356,7 @@ export class WebglCharAtlas implements IDisposable {
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph {
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
console.log('_drawToCache', chars, ext);
this.hasCanvasChanged = true;
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
+72
View File
@@ -0,0 +1,72 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ILink, ILinkProvider } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { IBufferService, IOscLinkService } from 'common/services/Services';
export class OscLinkProvider implements ILinkProvider {
constructor(
@IBufferService private readonly _bufferService: IBufferService,
@IOscLinkService private readonly _oscLinkService: IOscLinkService
) {
}
public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void {
const line = this._bufferService.buffer.lines.get(y - 1);
if (!line) {
callback(undefined);
return;
}
const result: ILink[] = [];
const cell = new CellData();
const lineLength = line.getTrimmedLength();
let currentLinkId = -1;
let currentStart = -1;
let finishLink = false;
for (let x = 0; x < lineLength; x++) {
if (!line.hasContent(x)) {
continue;
}
line.loadCell(x, cell);
if (cell.extended.urlId) {
if (currentStart === -1) {
currentStart = x;
currentLinkId = cell.extended.urlId;
continue;
} else {
finishLink = cell.extended.urlId !== currentLinkId;
}
} else {
if (currentStart !== -1) {
finishLink = true;
}
}
if (finishLink || (currentStart !== -1 && x === lineLength - 1)) {
const text = this._oscLinkService.getLinkData(currentLinkId)?.uri;
if (text) {
// OSC links always use underline and pointer decorations
result.push({
text,
// These ranges are 1-based
range: {
start: { x: currentStart + 1, y },
end: { x: x + 1, y }
},
activate(e, text) {
console.log('activate!', text);
}
// TODO: Embedder API to handle hover
});
}
}
}
// TODO: Handle fetching and returning other link ranges to underline other links with the same id
callback(result);
}
}
+2
View File
@@ -55,6 +55,7 @@ import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRe
import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
import { DecorationService } from 'common/services/DecorationService';
import { IDecorationService } from 'common/services/Services';
import { OscLinkProvider } from 'browser/OscLinkProvider';
// Let it work inside Node.js for automated testing purposes.
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
@@ -163,6 +164,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._setup();
this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2));
this.linkifier2.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider));
this._decorationService = this._instantiationService.createInstance(DecorationService);
this._instantiationService.setService(IDecorationService, this._decorationService);
+5 -1
View File
@@ -22,7 +22,7 @@
*/
import { Disposable } from 'common/Lifecycle';
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions } from 'common/services/Services';
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions, IOscLinkService } from 'common/services/Services';
import { InstantiationService } from 'common/services/InstantiationService';
import { LogService } from 'common/services/LogService';
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
@@ -39,6 +39,7 @@ import { IFunctionIdentifier, IParams } from 'common/parser/Types';
import { IBufferSet } from 'common/buffer/Types';
import { InputHandler } from 'common/InputHandler';
import { WriteBuffer } from 'common/input/WriteBuffer';
import { OscLinkService } from 'common/services/OscLinkService';
// Only trigger this warning a single time per session
let hasWriteSyncWarnHappened = false;
@@ -49,6 +50,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
protected readonly _logService: ILogService;
protected readonly _charsetService: ICharsetService;
protected readonly _dirtyRowService: IDirtyRowService;
protected readonly _oscLinkService: IOscLinkService;
public readonly coreMouseService: ICoreMouseService;
public readonly coreService: ICoreService;
@@ -118,6 +120,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._instantiationService.setService(IUnicodeService, this.unicodeService);
this._charsetService = this._instantiationService.createInstance(CharsetService);
this._instantiationService.setService(ICharsetService, this._charsetService);
this._oscLinkService = this._instantiationService.createInstance(OscLinkService);
this._instantiationService.setService(IOscLinkService, this._oscLinkService);
// Register input handler and handle/forward events
this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this.coreService, this._dirtyRowService, this._logService, this.optionsService, this.coreMouseService, this.unicodeService);
+68 -3
View File
@@ -4,7 +4,7 @@
* @license MIT
*/
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType } from 'common/Types';
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType, IOscLinkData } from 'common/Types';
import { C0, C1 } from 'common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets';
import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
@@ -214,8 +214,6 @@ class DECRQSS implements IDcsHandler {
* @vt: #N DCS XTSETTCAP "Set Terminfo Data" "DCS + p Pt ST" "Set Terminfo Data."
*/
/**
* The terminal's standard implementation of IInputHandler, this handles all
* input from the Parser.
@@ -230,6 +228,7 @@ export class InputHandler extends Disposable implements IInputHandler {
private _workCell: CellData = new CellData();
private _windowTitle = '';
private _iconName = '';
private _currentHyperlink?: IOscLinkData;
protected _windowTitleStack: string[] = [];
protected _iconNameStack: string[] = [];
@@ -265,6 +264,10 @@ export class InputHandler extends Disposable implements IInputHandler {
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
private _onColor = new EventEmitter<IColorEvent>();
public get onColor(): IEvent<IColorEvent> { return this._onColor.event; }
private _onStartHyperlink = new EventEmitter<IOscLinkData>();
public get onStartHyperlink(): IEvent<IOscLinkData> { return this._onStartHyperlink.event; }
private _onFinishHyperlink = new EventEmitter<void>();
public get onFinishHyperlink(): IEvent<void> { return this._onFinishHyperlink.event; }
private _parseStack: IParseStack = {
paused: false,
@@ -403,6 +406,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 (not in xterm spec, see https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda)
this._parser.registerOscHandler(8, new OscHandler(data => this.setHyperlink(data)));
// 10 - Change VT100 text foreground color to Pt.
this._parser.registerOscHandler(10, new OscHandler(data => this.setOrReportFgColor(data)));
// 11 - Change VT100 text background color to Pt.
@@ -2889,6 +2894,66 @@ export class InputHandler extends Disposable implements IInputHandler {
return true;
}
/**
* OSC 8 ; <params> ; <uri> ST - create hyperlink
* OSC 8 ; ; ST - finish hyperlink
*
* Test case:
*
* ```sh
* printf '\e]8;;http://example.com\e\\This is a link\e]8;;\e\\\n'
* ```
*
* @vt: #Y OSC 8 "Create hyperlink" "OSC 8 ; params ; uri BEL" "Create a hyperlink to `uri` using `params`."
* `uri` is a hyperlink starting with `http://`, `https://`, `ftp://`, `file://` or `mailto://`. `params` is an
* optional list of key=value assignments, separated by the : character. Example: `id=xyz123:foo=bar:baz=quux`.
* Currently only the id key is defined. Cells that share the same ID and URI share hover feedback.
* Use `OSC 8 ; ; BEL` to finish the current hyperlink.
*/
public setHyperlink(data: string): boolean {
const args = data.split(';');
console.log('hyperlink', args);
if (args.length < 2) {
return false;
}
if (args[1]) {
return this._createHyperlink(args[0], args[1]);
}
if (args[0]) {
return false;
}
return this._finishHyperlink();
}
private _createHyperlink(params: string, uri: string): boolean {
// It's legal to open a new hyperlink without explicitly finishing the previous one
if (this._currentHyperlink) {
this._finishHyperlink();
}
const parsedParams = params.split(':');
let id: string | undefined;
const idParamIndex = parsedParams.findIndex(e => e.startsWith('id='));
if (idParamIndex !== -1) {
id = parsedParams[idParamIndex].slice(3) || undefined;
}
this._currentHyperlink = { id, uri };
this._curAttrData.extended = this._curAttrData.extended.clone();
this._curAttrData.extended.urlId = 1;
this._curAttrData.updateExtended();
console.log('hasExtendedAttrs?', this._curAttrData.hasExtendedAttrs());
this._onStartHyperlink.fire(this._currentHyperlink);
return true;
}
private _finishHyperlink(): boolean {
this._curAttrData.extended = this._curAttrData.extended.clone();
this._curAttrData.extended.urlId = 0;
this._curAttrData.updateExtended();
this._onFinishHyperlink.fire();
this._currentHyperlink = undefined;
return true;
}
// special colors - OSC 10 | 11 | 12
private _specialColors = [ColorIndex.FOREGROUND, ColorIndex.BACKGROUND, ColorIndex.CURSOR];
+14 -1
View File
@@ -9,6 +9,7 @@ import { IDeleteEvent, IInsertEvent } from 'common/CircularList';
import { IParams } from 'common/parser/Types';
import { ICoreMouseService, ICoreService, IOptionsService, IUnicodeService } from 'common/services/Services';
import { IBufferSet } from 'common/buffer/Types';
import { UnderlineStyle } from 'common/buffer/Constants';
export interface ICoreTerminal {
coreMouseService: ICoreMouseService;
@@ -114,12 +115,24 @@ export type IColorRGB = [number, number, number];
export interface IExtendedAttrs {
ext: number;
underlineStyle: number;
underlineStyle: UnderlineStyle;
underlineColor: number;
urlId: number;
clone(): IExtendedAttrs;
isEmpty(): boolean;
}
/**
* Tracks the current hyperlink. Since these are treated as extended attirbutes, these get passed on
* to the linkifier when anything is printed. Doing it this way ensures that even when the cursor
* moves around unexpectedly the link is tracked, as opposed to using a start position and
* finalizing it at the end.
*/
export interface IOscLinkData {
id?: string;
uri: string;
}
/** Attribute data */
export interface IAttributeData {
fg: number;
+40 -8
View File
@@ -35,7 +35,12 @@ export class AttributeData implements IAttributeData {
// flags
public isInverse(): number { return this.fg & FgFlags.INVERSE; }
public isBold(): number { return this.fg & FgFlags.BOLD; }
public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; }
public isUnderline(): number {
if (this.hasExtendedAttrs() && this.extended.underlineStyle !== UnderlineStyle.NONE) {
return 1;
}
return this.fg & FgFlags.UNDERLINE;
}
public isBlink(): number { return this.fg & FgFlags.BLINK; }
public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; }
public isItalic(): number { return this.bg & BgFlags.ITALIC; }
@@ -128,10 +133,24 @@ export class AttributeData implements IAttributeData {
*/
export class ExtendedAttrs implements IExtendedAttrs {
private _ext: number = 0;
public get ext(): number { return this._ext; }
public get ext(): number {
// TODO: How to handle previous underline style if link overrides it?
if (this._urlId) {
console.log('ext, has url');
return (
(this._ext & ~ExtFlags.UNDERLINE_STYLE) |
(this.underlineStyle << 26)
);
}
return this._ext;
}
public set ext(value: number) { this._ext = value; }
public get underlineStyle(): UnderlineStyle {
// Always return the URL style if it has one
if (this._urlId) {
return UnderlineStyle.DASHED;
}
return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;
}
public set underlineStyle(value: UnderlineStyle) {
@@ -140,6 +159,11 @@ export class ExtendedAttrs implements IExtendedAttrs {
}
public get underlineColor(): number {
// Always return the URL color if it has one
if (this._urlId) {
// TODO: fix
return 0;
}
return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK);
}
public set underlineColor(value: number) {
@@ -147,16 +171,24 @@ export class ExtendedAttrs implements IExtendedAttrs {
this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);
}
private _urlId: number = 0;
public get urlId(): number {
return this._urlId;
}
public set urlId(value: number) {
this._urlId = value;
}
constructor(
underlineStyle: UnderlineStyle = UnderlineStyle.NONE,
underlineColor: number = Attributes.CM_DEFAULT
ext: number = 0,
urlId: number = 0
) {
this.underlineStyle = underlineStyle;
this.underlineColor = underlineColor;
this._ext = ext;
this._urlId = urlId;
}
public clone(): IExtendedAttrs {
return new ExtendedAttrs(this.underlineStyle, this.underlineColor);
return new ExtendedAttrs(this._ext, this._urlId);
}
/**
@@ -164,6 +196,6 @@ export class ExtendedAttrs implements IExtendedAttrs {
* that needs to be persistant in the buffer.
*/
public isEmpty(): boolean {
return this.underlineStyle === UnderlineStyle.NONE;
return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0;
}
}
+22
View File
@@ -0,0 +1,22 @@
import { IBufferService, IOscLinkService } from 'common/services/Services';
import { IOscLinkData } from 'common/Types';
export class OscLinkService implements IOscLinkService {
public serviceBrand: any;
constructor(
@IBufferService private readonly _bufferService: IBufferService
) {
}
public registerLink(linkData: IOscLinkData): number {
// TODO: Add and return properly
return 1;
}
public getLinkData(linkId: number): IOscLinkData | undefined {
return {
uri: 'https://github.com'
};
}
}
+13 -1
View File
@@ -5,7 +5,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 } from 'common/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';
@@ -272,6 +272,18 @@ export interface ITheme {
extendedAnsi?: string[];
}
export const IOscLinkService = createDecorator<IOscLinkService>('OscLinkService');
export interface IOscLinkService {
serviceBrand: undefined;
/**
* Registers a link to the service, returning the link ID. The link data is managed by this
* service and will be freed when this current cursor position is trimmed off the buffer.
*/
registerLink(linkData: IOscLinkData): number;
/** Get the link data associated with a link ID. */
getLinkData(linkId: number): IOscLinkData | undefined;
}
export const IUnicodeService = createDecorator<IUnicodeService>('UnicodeService');
export interface IUnicodeService {
serviceBrand: undefined;