Merge remote-tracking branch 'upstream/master' into Support-for-bubbling-scroll

This commit is contained in:
UmairShahzad
2019-08-21 00:46:07 +05:00
19 changed files with 2110 additions and 704 deletions
+6 -3
View File
@@ -343,9 +343,12 @@ export class SearchAddon implements ITerminalAddon {
return false;
}
terminal.select(result.col, result.row, result.term.length);
let scroll = result.row - terminal.buffer.viewportY;
scroll = scroll - Math.floor(terminal.rows / 2);
terminal.scrollLines(scroll);
// If it is not in the viewport then we scroll else it just gets selected
if (result.row > (terminal.buffer.viewportY + terminal.rows) || result.row < terminal.buffer.viewportY) {
let scroll = result.row - terminal.buffer.viewportY;
scroll = scroll - Math.floor(terminal.rows / 2);
terminal.scrollLines(scroll);
}
return true;
}
}
+9 -11
View File
@@ -53,39 +53,38 @@ describe('InputHandler', () => {
it('should call Terminal.setOption with correct params', () => {
const optionsService = new MockOptionsService();
const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService);
const collect = ' ';
inputHandler.setCursorStyle(Params.fromArray([0]), collect);
inputHandler.setCursorStyle(Params.fromArray([0]));
assert.equal(optionsService.options['cursorStyle'], 'block');
assert.equal(optionsService.options['cursorBlink'], true);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([1]), collect);
inputHandler.setCursorStyle(Params.fromArray([1]));
assert.equal(optionsService.options['cursorStyle'], 'block');
assert.equal(optionsService.options['cursorBlink'], true);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([2]), collect);
inputHandler.setCursorStyle(Params.fromArray([2]));
assert.equal(optionsService.options['cursorStyle'], 'block');
assert.equal(optionsService.options['cursorBlink'], false);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([3]), collect);
inputHandler.setCursorStyle(Params.fromArray([3]));
assert.equal(optionsService.options['cursorStyle'], 'underline');
assert.equal(optionsService.options['cursorBlink'], true);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([4]), collect);
inputHandler.setCursorStyle(Params.fromArray([4]));
assert.equal(optionsService.options['cursorStyle'], 'underline');
assert.equal(optionsService.options['cursorBlink'], false);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([5]), collect);
inputHandler.setCursorStyle(Params.fromArray([5]));
assert.equal(optionsService.options['cursorStyle'], 'bar');
assert.equal(optionsService.options['cursorBlink'], true);
optionsService.options = clone(DEFAULT_OPTIONS);
inputHandler.setCursorStyle(Params.fromArray([6]), collect);
inputHandler.setCursorStyle(Params.fromArray([6]));
assert.equal(optionsService.options['cursorStyle'], 'bar');
assert.equal(optionsService.options['cursorBlink'], false);
});
@@ -93,14 +92,13 @@ describe('InputHandler', () => {
describe('setMode', () => {
it('should toggle Terminal.bracketedPasteMode', () => {
const terminal = new MockInputHandlingTerminal();
const collect = '?';
terminal.bracketedPasteMode = false;
const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
// Set bracketed paste mode
inputHandler.setMode(Params.fromArray([2004]), collect);
inputHandler.setModePrivate(Params.fromArray([2004]));
assert.equal(terminal.bracketedPasteMode, true);
// Reset bracketed paste mode
inputHandler.resetMode(Params.fromArray([2004]), collect);
inputHandler.resetModePrivate(Params.fromArray([2004]));
assert.equal(terminal.bracketedPasteMode, false);
});
});
+229 -212
View File
File diff suppressed because it is too large Load Diff
+13 -3
View File
@@ -55,7 +55,7 @@ import { Disposable } from 'common/Lifecycle';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
import { Attributes } from 'common/buffer/Constants';
import { MouseService } from 'browser/services/MouseService';
import { IParams } from 'common/parser/Types';
import { IParams, IFunctionIdentifier } from 'common/parser/Types';
import { CoreService } from 'common/services/CoreService';
import { LogService } from 'common/services/LogService';
import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport } from 'browser/Types';
@@ -1402,9 +1402,19 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._customKeyEventHandler = customKeyEventHandler;
}
/** Add handler for ESC escape sequence. See xterm.d.ts for details. */
public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable {
return this._inputHandler.addEscHandler(id, callback);
}
/** Add handler for DCS escape sequence. See xterm.d.ts for details. */
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
return this._inputHandler.addDcsHandler(id, callback);
}
/** Add handler for CSI escape sequence. See xterm.d.ts for details. */
public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable {
return this._inputHandler.addCsiHandler(flag, callback);
public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
return this._inputHandler.addCsiHandler(id, callback);
}
/** Add handler for OSC escape sequence. See xterm.d.ts for details. */
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
+10 -4
View File
@@ -15,7 +15,7 @@ import { AttributeData } from 'common/buffer/AttributeData';
import { IColorManager, IColorSet, ILinkMatcherOptions, ILinkifier, IViewport } from 'browser/Types';
import { IOptionsService } from 'common/services/Services';
import { EventEmitter } from 'common/EventEmitter';
import { IParams } from 'common/parser/Types';
import { IParams, IFunctionIdentifier } from 'common/parser/Types';
import { ISelectionService } from 'browser/services/Services';
export class TestTerminal extends Terminal {
@@ -74,11 +74,17 @@ export class MockTerminal implements ITerminal {
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
throw new Error('Method not implemented.');
}
addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable {
throw new Error('Method not implemented.');
addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
throw new Error('Method not implemented.');
}
addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
throw new Error('Method not implemented.');
}
addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
throw new Error('Method not implemented.');
}
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
throw new Error('Method not implemented.');
throw new Error('Method not implemented.');
}
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
throw new Error('Method not implemented.');
+6 -3
View File
@@ -9,7 +9,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter';
import { IColorSet, ILinkifier, ILinkMatcherOptions, IViewport } from 'browser/Types';
import { IOptionsService } from 'common/services/Services';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IParams } from 'common/parser/Types';
import { IParams, IFunctionIdentifier } from 'common/parser/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
@@ -113,7 +113,8 @@ export interface IInputHandler {
/** CSI ` */ charPosAbsolute(params: IParams): void;
/** CSI a */ hPositionRelative(params: IParams): void;
/** CSI b */ repeatPrecedingCharacter(params: IParams): void;
/** CSI c */ sendDeviceAttributes(params: IParams, collect?: string): void;
/** CSI c */ sendDeviceAttributesPrimary(params: IParams): void;
/** CSI > c */ sendDeviceAttributesSecondary(params: IParams): void;
/** CSI d */ linePosAbsolute(params: IParams): void;
/** CSI e */ vPositionRelative(params: IParams): void;
/** CSI f */ hVPosition(params: IParams): void;
@@ -200,7 +201,9 @@ export interface IPublicTerminal extends IDisposable {
writeln(data: string): void;
open(parent: HTMLElement): void;
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable;
addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable;
addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable;
addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): void;
+13
View File
@@ -43,3 +43,16 @@ export const enum ParserAction {
DCS_PUT = 13,
DCS_UNHOOK = 14
}
/**
* Internal states of OscParser.
*/
export const enum OscState {
START = 0,
ID = 1,
PAYLOAD = 2,
ABORT = 3
}
// payload limit for OSC and DCS
export const PAYLOAD_LIMIT = 10000000;
+253
View File
@@ -0,0 +1,253 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { DcsParser, DcsHandler } from 'common/parser/DcsParser';
import { IDcsHandler, IParams, IFunctionIdentifier } from 'common/parser/Types';
import { utf32ToString, StringToUtf32 } from 'common/input/TextDecoder';
import { Params } from 'common/parser/Params';
import { PAYLOAD_LIMIT } from 'common/parser/Constants';
function toUtf32(s: string): Uint32Array {
const utf32 = new Uint32Array(s.length);
const decoder = new StringToUtf32();
const length = decoder.decode(s, utf32);
return utf32.subarray(0, length);
}
function identifier(id: IFunctionIdentifier): number {
let res = 0;
if (id.prefix) {
if (id.prefix.length > 1) {
throw new Error('only one byte as prefix supported');
}
res = id.prefix.charCodeAt(0);
if (res && 0x3c > res || res > 0x3f) {
throw new Error('prefix must be in range 0x3c .. 0x3f');
}
}
if (id.intermediates) {
if (id.intermediates.length > 2) {
throw new Error('only two bytes as intermediates are supported');
}
for (let i = 0; i < id.intermediates.length; ++i) {
const intermediate = id.intermediates.charCodeAt(i);
if (0x20 > intermediate || intermediate > 0x2f) {
throw new Error('intermediate must be in range 0x20 .. 0x2f');
}
res <<= 8;
res |= intermediate;
}
}
if (id.final.length !== 1) {
throw new Error('final must be a single byte');
}
const finalCode = id.final.charCodeAt(0);
if (0x40 > finalCode || finalCode > 0x7e) {
throw new Error('final must be in range 0x40 .. 0x7e');
}
res <<= 8;
res |= finalCode;
return res;
}
class TestHandler implements IDcsHandler {
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
hook(params: IParams): void {
this.output.push([this.msg, 'HOOK', params.toArray()]);
}
put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
}
unhook(success: boolean): void | boolean {
this.output.push([this.msg, 'UNHOOK', success]);
if (this.returnFalse) {
return false;
}
}
}
describe('DcsParser', () => {
let parser: DcsParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new DcsParser();
parser.setHandlerFallback((id, action, data) => {
if (action === 'HOOK') {
data = data.toArray();
}
reports.push([id, action, data]);
});
});
describe('handler registration', () => {
it('setDcsHandler', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
// messages from TestHandler
['th', 'HOOK', [1, 2, 3]],
['th', 'PUT', 'Here comes'],
['th', 'PUT', 'the mouse!'],
['th', 'UNHOOK', true]
]);
});
it('clearDcsHandler', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
parser.clearHandler(identifier({intermediates: '+', final: 'p'}));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
// messages from fallback handler
[identifier({intermediates: '+', final: 'p'}), 'HOOK', [1, 2, 3]],
[identifier({intermediates: '+', final: 'p'}), 'PUT', 'Here comes'],
[identifier({intermediates: '+', final: 'p'}), 'PUT', 'the mouse!'],
[identifier({intermediates: '+', final: 'p'}), 'UNHOOK', true]
]);
});
it('addDcsHandler', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2'));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
['th2', 'HOOK', [1, 2, 3]],
['th1', 'HOOK', [1, 2, 3]],
['th2', 'PUT', 'Here comes'],
['th1', 'PUT', 'Here comes'],
['th2', 'PUT', 'the mouse!'],
['th1', 'PUT', 'the mouse!'],
['th2', 'UNHOOK', true],
['th1', 'UNHOOK', false] // false due being already handled by th2!
]);
});
it('addDcsHandler with return false', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
['th2', 'HOOK', [1, 2, 3]],
['th1', 'HOOK', [1, 2, 3]],
['th2', 'PUT', 'Here comes'],
['th1', 'PUT', 'Here comes'],
['th2', 'PUT', 'the mouse!'],
['th1', 'PUT', 'the mouse!'],
['th2', 'UNHOOK', true],
['th1', 'UNHOOK', true] // true since th2 indicated to keep bubbling
]);
});
it('dispose handlers', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
const dispo = parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
dispo.dispose();
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
['th1', 'HOOK', [1, 2, 3]],
['th1', 'PUT', 'Here comes'],
['th1', 'PUT', 'the mouse!'],
['th1', 'UNHOOK', true]
]);
});
});
describe('DcsHandlerFactory', () => {
it('should be called once on end(true)', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]);
});
it('should not be called on end(false)', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.unhook(false);
assert.deepEqual(reports, []);
});
it('should be disposable', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data])));
const dispo = parser.addHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['two', params.toArray(), data])));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]);
dispo.dispose();
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
data = toUtf32('some other');
parser.put(data, 0, data.length);
data = toUtf32(' data');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]);
});
it('should respect return false', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data])));
parser.addHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return false; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]);
});
it('should work up to payload limit', function(): void {
this.timeout(10000);
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
const data = toUtf32('A'.repeat(1000));
for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
parser.put(data, 0, data.length);
}
parser.unhook(true);
assert.deepEqual(reports, [[[1, 2, 3], 'A'.repeat(PAYLOAD_LIMIT)]]);
});
it('should abort for payload limit +1', function(): void {
this.timeout(10000);
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('A'.repeat(1000));
for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
parser.put(data, 0, data.length);
}
data = toUtf32('A');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, []);
});
});
});
+146
View File
@@ -0,0 +1,146 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'common/Types';
import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType } from 'common/parser/Types';
import { utf32ToString } from 'common/input/TextDecoder';
import { Params } from 'common/parser/Params';
import { PAYLOAD_LIMIT } from 'common/parser/Constants';
const EMPTY_HANDLERS: IDcsHandler[] = [];
export class DcsParser implements IDcsParser {
private _handlers: IHandlerCollection<IDcsHandler> = Object.create(null);
private _active: IDcsHandler[] = EMPTY_HANDLERS;
private _ident: number = 0;
private _handlerFb: DcsFallbackHandlerType = () => {};
public dispose(): void {
this._handlers = Object.create(null);
this._handlerFb = () => {};
}
public addHandler(ident: number, handler: IDcsHandler): IDisposable {
if (this._handlers[ident] === undefined) {
this._handlers[ident] = [];
}
const handlerList = this._handlers[ident];
handlerList.push(handler);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(handler);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
public setHandler(ident: number, handler: IDcsHandler): void {
this._handlers[ident] = [handler];
}
public clearHandler(ident: number): void {
if (this._handlers[ident]) delete this._handlers[ident];
}
public setHandlerFallback(handler: DcsFallbackHandlerType): void {
this._handlerFb = handler;
}
public reset(): void {
if (this._active.length) {
this.unhook(false);
}
this._active = EMPTY_HANDLERS;
this._ident = 0;
}
public hook(ident: number, params: IParams): void {
// always reset leftover handlers
this.reset();
this._ident = ident;
this._active = this._handlers[ident] || EMPTY_HANDLERS;
if (!this._active.length) {
this._handlerFb(this._ident, 'HOOK', params);
} else {
for (let j = this._active.length - 1; j >= 0; j--) {
this._active[j].hook(params);
}
}
}
public put(data: Uint32Array, start: number, end: number): void {
if (!this._active.length) {
this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));
} else {
for (let j = this._active.length - 1; j >= 0; j--) {
this._active[j].put(data, start, end);
}
}
}
public unhook(success: boolean): void {
if (!this._active.length) {
this._handlerFb(this._ident, 'UNHOOK', success);
} else {
let j = this._active.length - 1;
for (; j >= 0; j--) {
if (this._active[j].unhook(success) !== false) {
break;
}
}
j--;
// cleanup left over handlers
for (; j >= 0; j--) {
this._active[j].unhook(false);
}
}
this._active = EMPTY_HANDLERS;
this._ident = 0;
}
}
/**
* Convenient class to create a DCS handler from a single callback function.
* Note: The payload is currently limited to 50 MB (hardcoded).
*/
export class DcsHandler implements IDcsHandler {
private _data = '';
private _params: IParams | undefined;
private _hitLimit: boolean = false;
constructor(private _handler: (data: string, params: IParams) => any) {}
public hook(params: IParams): void {
this._params = params.clone();
this._data = '';
this._hitLimit = false;
}
public put(data: Uint32Array, start: number, end: number): void {
if (this._hitLimit) {
return;
}
this._data += utf32ToString(data, start, end);
if (this._data.length > PAYLOAD_LIMIT) {
this._data = '';
this._hitLimit = true;
}
}
public unhook(success: boolean): any {
let ret;
if (this._hitLimit) {
ret = false;
} else if (success) {
ret = this._handler(this._data, this._params ? this._params : new Params());
}
this._params = undefined;
this._data = '';
this._hitLimit = false;
return ret;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+251
View File
@@ -0,0 +1,251 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { OscParser, OscHandler } from 'common/parser/OscParser';
import { StringToUtf32, utf32ToString } from 'common/input/TextDecoder';
import { IOscHandler } from 'common/parser/Types';
import { PAYLOAD_LIMIT } from 'common/parser/Constants';
function toUtf32(s: string): Uint32Array {
const utf32 = new Uint32Array(s.length);
const decoder = new StringToUtf32();
const length = decoder.decode(s, utf32);
return utf32.subarray(0, length);
}
class TestHandler implements IOscHandler {
constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {}
start(): void {
this.output.push([this.msg, this.id, 'START']);
}
put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
}
end(success: boolean): void | boolean {
this.output.push([this.msg, this.id, 'END', success]);
if (this.returnFalse) {
return false;
}
}
}
describe('OscParser', () => {
let parser: OscParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new OscParser();
parser.setHandlerFallback((id, action, data) => {
reports.push([id, action, data]);
});
});
describe('identifier parsing', () => {
it('no report for illegal ids', () => {
const data = toUtf32('hello world!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, []);
});
it('no payload', () => {
parser.start();
let data = toUtf32('12');
parser.put(data, 0, data.length);
data = toUtf32('34');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [[1234, 'START', undefined], [1234, 'END', true]]);
});
it('with payload', () => {
parser.start();
let data = toUtf32('12');
parser.put(data, 0, data.length);
data = toUtf32('34');
parser.put(data, 0, data.length);
data = toUtf32(';h');
parser.put(data, 0, data.length);
data = toUtf32('ello');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
[1234, 'START', undefined],
[1234, 'PUT', 'h'],
[1234, 'PUT', 'ello'],
[1234, 'END', true]
]);
});
});
describe('handler registration', () => {
it('setOscHandler', () => {
parser.setHandler(1234, new TestHandler(1234, reports, 'th'));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
// messages from TestHandler
['th', 1234, 'START'],
['th', 1234, 'PUT', 'Here comes'],
['th', 1234, 'PUT', 'the mouse!'],
['th', 1234, 'END', true]
]);
});
it('clearOscHandler', () => {
parser.setHandler(1234, new TestHandler(1234, reports, 'th'));
parser.clearHandler(1234);
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
// messages from fallback handler
[1234, 'START', undefined],
[1234, 'PUT', 'Here comes'],
[1234, 'PUT', 'the mouse!'],
[1234, 'END', true]
]);
});
it('addOscHandler', () => {
parser.setHandler(1234, new TestHandler(1234, reports, 'th1'));
parser.addHandler(1234, new TestHandler(1234, reports, 'th2'));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
['th2', 1234, 'START'],
['th1', 1234, 'START'],
['th2', 1234, 'PUT', 'Here comes'],
['th1', 1234, 'PUT', 'Here comes'],
['th2', 1234, 'PUT', 'the mouse!'],
['th1', 1234, 'PUT', 'the mouse!'],
['th2', 1234, 'END', true],
['th1', 1234, 'END', false] // false due being already handled by th2!
]);
});
it('addOscHandler with return false', () => {
parser.setHandler(1234, new TestHandler(1234, reports, 'th1'));
parser.addHandler(1234, new TestHandler(1234, reports, 'th2', true));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
['th2', 1234, 'START'],
['th1', 1234, 'START'],
['th2', 1234, 'PUT', 'Here comes'],
['th1', 1234, 'PUT', 'Here comes'],
['th2', 1234, 'PUT', 'the mouse!'],
['th1', 1234, 'PUT', 'the mouse!'],
['th2', 1234, 'END', true],
['th1', 1234, 'END', true] // true since th2 indicated to keep bubbling
]);
});
it('dispose handlers', () => {
parser.setHandler(1234, new TestHandler(1234, reports, 'th1'));
const dispo = parser.addHandler(1234, new TestHandler(1234, reports, 'th2', true));
dispo.dispose();
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
['th1', 1234, 'START'],
['th1', 1234, 'PUT', 'Here comes'],
['th1', 1234, 'PUT', 'the mouse!'],
['th1', 1234, 'END', true]
]);
});
});
describe('OscHandlerFactory', () => {
it('should be called once on end(true)', () => {
parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]);
});
it('should not be called on end(false)', () => {
parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(false);
assert.deepEqual(reports, []);
});
it('should be disposable', () => {
parser.setHandler(1234, new OscHandler(data => reports.push(['one', data])));
const dispo = parser.addHandler(1234, new OscHandler(data => reports.push(['two', data])));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
dispo.dispose();
parser.start();
data = toUtf32('1234;some other');
parser.put(data, 0, data.length);
data = toUtf32(' data');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
});
it('should respect return false', () => {
parser.setHandler(1234, new OscHandler(data => reports.push(['one', data])));
parser.addHandler(1234, new OscHandler(data => { reports.push(['two', data]); return false; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
});
it('should work up to payload limit', function(): void {
this.timeout(10000);
parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
parser.start();
let data = toUtf32('1234;');
parser.put(data, 0, data.length);
data = toUtf32('A'.repeat(1000));
for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
parser.put(data, 0, data.length);
}
parser.end(true);
assert.deepEqual(reports, [[1234, 'A'.repeat(PAYLOAD_LIMIT)]]);
});
it('should abort for payload limit +1', function(): void {
this.timeout(10000);
parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
parser.start();
let data = toUtf32('1234;');
parser.put(data, 0, data.length);
data = toUtf32('A'.repeat(1000));
for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
parser.put(data, 0, data.length);
}
data = toUtf32('A');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, []);
});
});
});
+203
View File
@@ -0,0 +1,203 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser } from 'common/parser/Types';
import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants';
import { utf32ToString } from 'common/input/TextDecoder';
import { IDisposable } from 'common/Types';
export class OscParser implements IOscParser {
private _state = OscState.START;
private _id = -1;
private _handlers: IHandlerCollection<IOscHandler> = Object.create(null);
private _handlerFb: OscFallbackHandlerType = () => { };
public addHandler(ident: number, handler: IOscHandler): IDisposable {
if (this._handlers[ident] === undefined) {
this._handlers[ident] = [];
}
const handlerList = this._handlers[ident];
handlerList.push(handler);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(handler);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
public setHandler(ident: number, handler: IOscHandler): void {
this._handlers[ident] = [handler];
}
public clearHandler(ident: number): void {
if (this._handlers[ident]) delete this._handlers[ident];
}
public setHandlerFallback(handler: OscFallbackHandlerType): void {
this._handlerFb = handler;
}
public dispose(): void {
this._handlers = Object.create(null);
this._handlerFb = () => {};
}
public reset(): void {
// cleanup handlers if payload was already sent
if (this._state === OscState.PAYLOAD) {
this.end(false);
}
this._id = -1;
this._state = OscState.START;
}
private _start(): void {
const handlers = this._handlers[this._id];
if (!handlers) {
this._handlerFb(this._id, 'START');
} else {
for (let j = handlers.length - 1; j >= 0; j--) {
handlers[j].start();
}
}
}
private _put(data: Uint32Array, start: number, end: number): void {
const handlers = this._handlers[this._id];
if (!handlers) {
this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));
} else {
for (let j = handlers.length - 1; j >= 0; j--) {
handlers[j].put(data, start, end);
}
}
}
private _end(success: boolean): void {
// other than the old code we always have to call .end
// to keep the bubbling we use `success` to indicate
// whether a handler should execute
const handlers = this._handlers[this._id];
if (!handlers) {
this._handlerFb(this._id, 'END', success);
} else {
let j = handlers.length - 1;
for (; j >= 0; j--) {
if (handlers[j].end(success) !== false) {
break;
}
}
j--;
// cleanup left over handlers
for (; j >= 0; j--) {
handlers[j].end(false);
}
}
}
public start(): void {
// always reset leftover handlers
this.reset();
this._id = -1;
this._state = OscState.ID;
}
/**
* Put data to current OSC command.
* Expects the identifier of the OSC command in the form
* OSC id ; payload ST/BEL
* Payload chunks are not further processed and get
* directly passed to the handlers.
*/
public put(data: Uint32Array, start: number, end: number): void {
if (this._state === OscState.ABORT) {
return;
}
if (this._state === OscState.ID) {
while (start < end) {
const code = data[start++];
if (code === 0x3b) {
this._state = OscState.PAYLOAD;
this._start();
break;
}
if (code < 0x30 || 0x39 < code) {
this._state = OscState.ABORT;
return;
}
if (this._id === -1) {
this._id = 0;
}
this._id = this._id * 10 + code - 48;
}
}
if (this._state === OscState.PAYLOAD && end - start > 0) {
this._put(data, start, end);
}
}
/**
* Indicates end of an OSC command.
* Whether the OSC got aborted or finished normally
* is indicated by `success`.
*/
public end(success: boolean): void {
if (this._state === OscState.START) {
return;
}
// do nothing if command was faulty
if (this._state !== OscState.ABORT) {
// if we are still in ID state and get an early end
// means that the command has no payload thus we still have
// to announce START and send END right after
if (this._state === OscState.ID) {
this._start();
}
this._end(success);
}
this._id = -1;
this._state = OscState.START;
}
}
/**
* Convenient class to allow attaching string based handler functions
* as OSC handlers.
*/
export class OscHandler implements IOscHandler {
private _data = '';
private _hitLimit: boolean = false;
constructor(private _handler: (data: string) => any) {}
public start(): void {
this._data = '';
this._hitLimit = false;
}
public put(data: Uint32Array, start: number, end: number): void {
if (this._hitLimit) {
return;
}
this._data += utf32ToString(data, start, end);
if (this._data.length > PAYLOAD_LIMIT) {
this._data = '';
this._hitLimit = true;
}
}
public end(success: boolean): any {
let ret;
if (this._hitLimit) {
ret = false;
} else if (success) {
ret = this._handler(this._data);
}
this._data = '';
this._hitLimit = false;
return ret;
}
}
+145 -43
View File
@@ -53,10 +53,8 @@ export interface IParsingState {
code: number;
// current parser state
currentState: ParserState;
// osc string buffer
osc: string;
// collect buffer with intermediate characters
collect: string;
collect: number;
// params buffer
params: IParams;
// should abort (default: false)
@@ -64,31 +62,83 @@ export interface IParsingState {
}
/**
* DCS handler signature for EscapeSequenceParser.
* EscapeSequenceParser handles DCS commands via separate
* subparsers that get hook/unhooked and can handle
* arbitrary amount of data.
*
* On entering a DSC sequence `hook` is called by
* `EscapeSequenceParser`. Use it to initialize or reset
* states needed to handle the current DCS sequence.
* Note: A DCS parser is only instantiated once, therefore
* you cannot rely on the ctor to reinitialize state.
*
* EscapeSequenceParser will call `put` several times if the
* parsed data got split, therefore you might have to collect
* `data` until `unhook` is called.
* Note: `data` is borrowed, if you cannot process the data
* in chunks you have to copy it, doing otherwise will lead to
* data losses or corruption.
*
* `unhook` marks the end of the current DCS sequence.
*/
* Command handler interfaces.
*/
/**
* CSI handler types.
* Note: `params` is borrowed.
*/
export type CsiHandlerType = (params: IParams) => boolean | void;
export type CsiFallbackHandlerType = (ident: number, params: IParams) => void;
/**
* DCS handler types.
*/
export interface IDcsHandler {
hook(collect: string, params: IParams, flag: number): void;
/**
* Called when a DCS command starts.
* Prepare needed data structures here.
* Note: `params` is borrowed.
*/
hook(params: IParams): void;
/**
* Incoming payload chunk.
* Note: `params` is borrowed.
*/
put(data: Uint32Array, start: number, end: number): void;
unhook(): void;
/**
* End of DCS command. `success` indicates whether the
* command finished normally or got aborted, thus final
* execution of the command should depend on `success`.
* To save memory also cleanup data structures here.
*/
unhook(success: boolean): void | boolean;
}
export type DcsFallbackHandlerType = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void;
/**
* ESC handler types.
*/
export type EscHandlerType = () => boolean | void;
export type EscFallbackHandlerType = (identifier: number) => void;
/**
* EXECUTE handler types.
*/
export type ExecuteHandlerType = () => boolean | void;
export type ExecuteFallbackHandlerType = (ident: number) => void;
/**
* OSC handler types.
*/
export interface IOscHandler {
/**
* Announces start of this OSC command.
* Prepare needed data structures here.
*/
start(): void;
/**
* Incoming data chunk.
* Note: Data is borrowed.
*/
put(data: Uint32Array, start: number, end: number): void;
/**
* End of OSC command. `success` indicates whether the
* command finished normally or got aborted, thus final
* execution of the command should depend on `success`.
* To save memory also cleanup data structures here.
*/
end(success: boolean): void | boolean;
}
export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
/**
* PRINT handler types.
*/
export type PrintHandlerType = (data: Uint32Array, start: number, end: number) => void;
export type PrintFallbackHandlerType = PrintHandlerType;
/**
* EscapeSequenceParser interface.
@@ -112,31 +162,83 @@ export interface IEscapeSequenceParser extends IDisposable {
*/
parse(data: Uint32Array, length: number): void;
setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void;
/**
* Get string from numercial function identifier `ident`.
* Useful in fallback handlers which expose the low level
* numcerical function identifier for debugging purposes.
* Note: A full back translation to `IFunctionIdentifier`
* is not implemented.
*/
identToString(ident: number): string;
setPrintHandler(handler: PrintHandlerType): void;
clearPrintHandler(): void;
setExecuteHandler(flag: string, callback: () => void): void;
setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void;
clearEscHandler(id: IFunctionIdentifier): void;
setEscHandlerFallback(handler: EscFallbackHandlerType): void;
addEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable;
setExecuteHandler(flag: string, handler: ExecuteHandlerType): void;
clearExecuteHandler(flag: string): void;
setExecuteHandlerFallback(callback: (code: number) => void): void;
setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void;
setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void;
clearCsiHandler(flag: string): void;
setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void;
addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
setCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): void;
clearCsiHandler(id: IFunctionIdentifier): void;
setCsiHandlerFallback(callback: CsiFallbackHandlerType): void;
addCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable;
setEscHandler(collectAndFlag: string, callback: () => void): void;
clearEscHandler(collectAndFlag: string): void;
setEscHandlerFallback(callback: (collect: string, flag: number) => void): void;
setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void;
clearDcsHandler(id: IFunctionIdentifier): void;
setDcsHandlerFallback(handler: DcsFallbackHandlerType): void;
addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable;
setOscHandler(ident: number, callback: (data: string) => void): void;
setOscHandler(ident: number, handler: IOscHandler): void;
clearOscHandler(ident: number): void;
setOscHandlerFallback(callback: (identifier: number, data: string) => void): void;
setOscHandlerFallback(handler: OscFallbackHandlerType): void;
addOscHandler(ident: number, handler: IOscHandler): IDisposable;
setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void;
clearDcsHandler(collectAndFlag: string): void;
setDcsHandlerFallback(handler: IDcsHandler): void;
setErrorHandler(callback: (state: IParsingState) => IParsingState): void;
setErrorHandler(handler: (state: IParsingState) => IParsingState): void;
clearErrorHandler(): void;
}
/**
* Subparser interfaces.
* The subparsers are instantiated in `EscapeSequenceParser` and
* called during `EscapeSequenceParser.parse`.
*/
export interface ISubParser<T, U> extends IDisposable {
reset(): void;
addHandler(ident: number, handler: T): IDisposable;
setHandler(ident: number, handler: T): void;
clearHandler(ident: number): void;
setHandlerFallback(handler: U): void;
put(data: Uint32Array, start: number, end: number): void;
}
export interface IOscParser extends ISubParser<IOscHandler, OscFallbackHandlerType> {
start(): void;
end(success: boolean): void;
}
export interface IDcsParser extends ISubParser<IDcsHandler, DcsFallbackHandlerType> {
hook(ident: number, params: IParams): void;
unhook(success: boolean): void;
}
/**
* Interface to denote a specific ESC, CSI or DCS handler slot.
* The values are used to create an integer respresentation during handler
* regristation before passed to the subparsers as `ident`.
* The integer translation is made to allow a faster handler access
* in `EscapeSequenceParser.parse`.
*/
export interface IFunctionIdentifier {
prefix?: string;
intermediates?: string;
final: string;
}
export interface IHandlerCollection<T> {
[key: string]: T[];
}
+25 -7
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm';
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier } from 'xterm';
import { ITerminal } from '../Types';
import { IBufferLine } from 'common/Types';
import { IBuffer } from 'common/buffer/Types';
@@ -16,6 +16,7 @@ import { IParams } from 'common/parser/Types';
export class Terminal implements ITerminalApi {
private _core: ITerminal;
private _addonManager: AddonManager;
private _parser: IParser;
constructor(options?: ITerminalOptions) {
this._core = new TerminalCore(options);
@@ -33,6 +34,12 @@ export class Terminal implements ITerminalApi {
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
public get element(): HTMLElement { return this._core.element; }
public get parser(): IParser {
if (!this._parser) {
this._parser = new ParserApi(this._core);
}
return this._parser;
}
public get textarea(): HTMLTextAreaElement { return this._core.textarea; }
public get rows(): number { return this._core.rows; }
public get cols(): number { return this._core.cols; }
@@ -57,12 +64,6 @@ export class Terminal implements ITerminalApi {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
}
public addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable {
return this._core.addCsiHandler(flag, (params: IParams, collect: string) => callback(params.toArray(), collect));
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._core.addOscHandler(ident, callback);
}
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number {
return this._core.registerLinkMatcher(regex, handler, options);
}
@@ -217,3 +218,20 @@ class BufferCellApiView implements IBufferCellApi {
public get char(): string { return this._line.getString(this._x); }
public get width(): number { return this._line.getWidth(this._x); }
}
class ParserApi implements IParser {
constructor(private _core: ITerminal) {}
public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray()));
}
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));
}
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
return this._core.addEscHandler(id, handler);
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._core.addOscHandler(ident, callback);
}
}
-17
View File
@@ -334,23 +334,6 @@ describe('InputHandler Integration Tests', function(): void {
});
});
});
describe('addCsiHandler', () => {
it('should call custom CSI handler with js array params', async () => {
await page.evaluate(`
window.term.reset();
const _customCsiHandlerParams = [];
const _customCsiHandler = window.term.addCsiHandler('m', (params, collect) => {
_customCsiHandlerParams.push(params);
return false;
}, '');
`);
await page.evaluate(`
window.term.write('\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams');
`);
assert.deepEqual(await page.evaluate(`(() => _customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]);
});
});
});
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
+134
View File
@@ -0,0 +1,134 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as puppeteer from 'puppeteer';
import { assert } from 'chai';
import { ITerminalOptions } from 'xterm';
const APP = 'http://127.0.0.1:3000/test';
let browser: puppeteer.Browser;
let page: puppeteer.Page;
const width = 800;
const height = 600;
describe('Parser Integration Tests', function(): void {
this.timeout(20000);
before(async function(): Promise<any> {
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
slowMo: 80,
args: [`--window-size=${width},${height}`]
});
page = (await browser.pages())[0];
await page.setViewport({ width, height });
await page.goto(APP);
await openTerminal();
});
after(() => {
browser.close();
});
describe('addCsiHandler', () => {
it('should call custom CSI handler with js array params', async () => {
await page.evaluate(`
window.term.reset();
const _customCsiHandlerParams = [];
const _customCsiHandler = window.term.parser.addCsiHandler({final: 'm'}, (params, collect) => {
_customCsiHandlerParams.push(params);
return false;
}, '');
`);
await page.evaluate(`
window.term.write('\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams');
`);
assert.deepEqual(await page.evaluate(`(() => _customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]);
});
});
describe('addDcsHandler', () => {
it('should respects return value', async () => {
await page.evaluate(`
window.term.reset();
const _customDcsHandlerCallStack = [];
const _customDcsHandlerA = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
_customDcsHandlerCallStack.push(['A', params, data]);
return false;
});
const _customDcsHandlerB = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
_customDcsHandlerCallStack.push(['B', params, data]);
return true;
});
const _customDcsHandlerC = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
_customDcsHandlerCallStack.push(['C', params, data]);
return false;
});
`);
await page.evaluate(`
window.term.write('\x1bP1;2+psome data\x1b\\\\');
`);
assert.deepEqual(await page.evaluate(`(() => _customDcsHandlerCallStack)();`), [['C', [1, 2], 'some data'], ['B', [1, 2], 'some data']]);
});
});
describe('addEscHandler', () => {
it('should respects return value', async () => {
await page.evaluate(`
window.term.reset();
const _customEscHandlerCallStack = [];
const _customEscHandlerA = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => {
_customEscHandlerCallStack.push('A');
return false;
});
const _customEscHandlerB = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => {
_customEscHandlerCallStack.push('B');
return true;
});
const _customEscHandlerC = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => {
_customEscHandlerCallStack.push('C');
return false;
});
`);
await page.evaluate(`
window.term.write('\x1b(B');
`);
assert.deepEqual(await page.evaluate(`(() => _customEscHandlerCallStack)();`), ['C', 'B']);
});
});
describe('addOscHandler', () => {
it('should respects return value', async () => {
await page.evaluate(`
window.term.reset();
const _customOscHandlerCallStack = [];
const _customOscHandlerA = window.term.parser.addOscHandler(1234, data => {
_customOscHandlerCallStack.push(['A', data]);
return false;
});
const _customOscHandlerB = window.term.parser.addOscHandler(1234, data => {
_customOscHandlerCallStack.push(['B', data]);
return true;
});
const _customOscHandlerC = window.term.parser.addOscHandler(1234, data => {
_customOscHandlerCallStack.push(['C', data]);
return false;
});
`);
await page.evaluate(`
window.term.write('\x1b]1234;some data\x07');
`);
assert.deepEqual(await page.evaluate(`(() => _customOscHandlerCallStack)();`), [['C', 'some data'], ['B', 'some data']]);
});
});
});
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
if (options.rendererType === 'dom') {
await page.waitForSelector('.xterm-rows');
} else {
await page.waitForSelector('.xterm-text-layer');
}
}
@@ -7,6 +7,7 @@ import { perfContext, before, beforeEach, ThroughputRuntimeCase } from 'xterm-be
import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
import { C0, C1 } from 'common/data/EscapeSequences';
import { IDcsHandler, IParams } from 'common/parser/Types';
import { OscHandler } from 'common/parser/OscParser';
function toUtf32(s: string): Uint32Array {
@@ -18,7 +19,7 @@ function toUtf32(s: string): Uint32Array {
}
class DcsHandler implements IDcsHandler {
hook(collect: string, params: IParams, flag: number) : void {}
hook(params: IParams) : void {}
put(data: Uint32Array, start: number, end: number) : void {}
unhook() :void {}
}
@@ -31,42 +32,42 @@ perfContext('Parser throughput - 50MB data', () => {
beforeEach(() => {
parser = new EscapeSequenceParser();
parser.setPrintHandler((data, start, end) => {});
parser.setCsiHandler('@', (params, collect) => {});
parser.setCsiHandler('A', (params, collect) => {});
parser.setCsiHandler('B', (params, collect) => {});
parser.setCsiHandler('C', (params, collect) => {});
parser.setCsiHandler('D', (params, collect) => {});
parser.setCsiHandler('E', (params, collect) => {});
parser.setCsiHandler('F', (params, collect) => {});
parser.setCsiHandler('G', (params, collect) => {});
parser.setCsiHandler('H', (params, collect) => {});
parser.setCsiHandler('I', (params, collect) => {});
parser.setCsiHandler('J', (params, collect) => {});
parser.setCsiHandler('K', (params, collect) => {});
parser.setCsiHandler('L', (params, collect) => {});
parser.setCsiHandler('M', (params, collect) => {});
parser.setCsiHandler('P', (params, collect) => {});
parser.setCsiHandler('S', (params, collect) => {});
parser.setCsiHandler('T', (params, collect) => {});
parser.setCsiHandler('X', (params, collect) => {});
parser.setCsiHandler('Z', (params, collect) => {});
parser.setCsiHandler('`', (params, collect) => {});
parser.setCsiHandler('a', (params, collect) => {});
parser.setCsiHandler('b', (params, collect) => {});
parser.setCsiHandler('c', (params, collect) => {});
parser.setCsiHandler('d', (params, collect) => {});
parser.setCsiHandler('e', (params, collect) => {});
parser.setCsiHandler('f', (params, collect) => {});
parser.setCsiHandler('g', (params, collect) => {});
parser.setCsiHandler('h', (params, collect) => {});
parser.setCsiHandler('l', (params, collect) => {});
parser.setCsiHandler('m', (params, collect) => {});
parser.setCsiHandler('n', (params, collect) => {});
parser.setCsiHandler('p', (params, collect) => {});
parser.setCsiHandler('q', (params, collect) => {});
parser.setCsiHandler('r', (params, collect) => {});
parser.setCsiHandler('s', (params, collect) => {});
parser.setCsiHandler('u', (params, collect) => {});
parser.setCsiHandler({final: '@'}, params => {});
parser.setCsiHandler({final: 'A'}, params => {});
parser.setCsiHandler({final: 'B'}, params => {});
parser.setCsiHandler({final: 'C'}, params => {});
parser.setCsiHandler({final: 'D'}, params => {});
parser.setCsiHandler({final: 'E'}, params => {});
parser.setCsiHandler({final: 'F'}, params => {});
parser.setCsiHandler({final: 'G'}, params => {});
parser.setCsiHandler({final: 'H'}, params => {});
parser.setCsiHandler({final: 'I'}, params => {});
parser.setCsiHandler({final: 'J'}, params => {});
parser.setCsiHandler({final: 'K'}, params => {});
parser.setCsiHandler({final: 'L'}, params => {});
parser.setCsiHandler({final: 'M'}, params => {});
parser.setCsiHandler({final: 'P'}, params => {});
parser.setCsiHandler({final: 'S'}, params => {});
parser.setCsiHandler({final: 'T'}, params => {});
parser.setCsiHandler({final: 'X'}, params => {});
parser.setCsiHandler({final: 'Z'}, params => {});
parser.setCsiHandler({final: '`'}, params => {});
parser.setCsiHandler({final: 'a'}, params => {});
parser.setCsiHandler({final: 'b'}, params => {});
parser.setCsiHandler({final: 'c'}, params => {});
parser.setCsiHandler({final: 'd'}, params => {});
parser.setCsiHandler({final: 'e'}, params => {});
parser.setCsiHandler({final: 'f'}, params => {});
parser.setCsiHandler({final: 'g'}, params => {});
parser.setCsiHandler({final: 'h'}, params => {});
parser.setCsiHandler({final: 'l'}, params => {});
parser.setCsiHandler({final: 'm'}, params => {});
parser.setCsiHandler({final: 'n'}, params => {});
parser.setCsiHandler({final: 'p'}, params => {});
parser.setCsiHandler({final: 'q'}, params => {});
parser.setCsiHandler({final: 'r'}, params => {});
parser.setCsiHandler({final: 's'}, params => {});
parser.setCsiHandler({final: 'u'}, params => {});
parser.setExecuteHandler(C0.BEL, () => {});
parser.setExecuteHandler(C0.LF, () => {});
parser.setExecuteHandler(C0.VT, () => {});
@@ -79,25 +80,25 @@ perfContext('Parser throughput - 50MB data', () => {
parser.setExecuteHandler(C1.IND, () => {});
parser.setExecuteHandler(C1.NEL, () => {});
parser.setExecuteHandler(C1.HTS, () => {});
parser.setOscHandler(0, (data) => {});
parser.setOscHandler(2, (data) => {});
parser.setEscHandler('7', () => {});
parser.setEscHandler('8', () => {});
parser.setEscHandler('D', () => {});
parser.setEscHandler('E', () => {});
parser.setEscHandler('H', () => {});
parser.setEscHandler('M', () => {});
parser.setEscHandler('=', () => {});
parser.setEscHandler('>', () => {});
parser.setEscHandler('c', () => {});
parser.setEscHandler('n', () => {});
parser.setEscHandler('o', () => {});
parser.setEscHandler('|', () => {});
parser.setEscHandler('}', () => {});
parser.setEscHandler('~', () => {});
parser.setEscHandler('%@', () => {});
parser.setEscHandler('%G', () => {});
parser.setDcsHandler('q', new DcsHandler());
parser.setOscHandler(0, new OscHandler((data) => {}));
parser.setOscHandler(2, new OscHandler((data) => {}));
parser.setEscHandler({final: '7'}, () => {});
parser.setEscHandler({final: '8'}, () => {});
parser.setEscHandler({final: 'D'}, () => {});
parser.setEscHandler({final: 'E'}, () => {});
parser.setEscHandler({final: 'H'}, () => {});
parser.setEscHandler({final: 'M'}, () => {});
parser.setEscHandler({final: '='}, () => {});
parser.setEscHandler({final: '>'}, () => {});
parser.setEscHandler({final: 'c'}, () => {});
parser.setEscHandler({final: 'n'}, () => {});
parser.setEscHandler({final: 'o'}, () => {});
parser.setEscHandler({final: '|'}, () => {});
parser.setEscHandler({final: '}'}, () => {});
parser.setEscHandler({final: '~'}, () => {});
parser.setEscHandler({intermediates: '%', final: '@'}, () => {});
parser.setEscHandler({intermediates: '%', final: 'G'}, () => {});
parser.setDcsHandler({final: 'q'}, new DcsHandler());
});
perfContext('PRINT - a', () => {
+141 -48
View File
@@ -207,47 +207,47 @@ declare module 'xterm' {
*/
export interface ITheme {
/** The default foreground color */
foreground?: string,
foreground?: string;
/** The default background color */
background?: string,
background?: string;
/** The cursor color */
cursor?: string,
cursor?: string;
/** The accent color of the cursor (fg color for a block cursor) */
cursorAccent?: string,
cursorAccent?: string;
/** The selection background color (can be transparent) */
selection?: string,
selection?: string;
/** ANSI black (eg. `\x1b[30m`) */
black?: string,
black?: string;
/** ANSI red (eg. `\x1b[31m`) */
red?: string,
red?: string;
/** ANSI green (eg. `\x1b[32m`) */
green?: string,
green?: string;
/** ANSI yellow (eg. `\x1b[33m`) */
yellow?: string,
yellow?: string;
/** ANSI blue (eg. `\x1b[34m`) */
blue?: string,
blue?: string;
/** ANSI magenta (eg. `\x1b[35m`) */
magenta?: string,
magenta?: string;
/** ANSI cyan (eg. `\x1b[36m`) */
cyan?: string,
cyan?: string;
/** ANSI white (eg. `\x1b[37m`) */
white?: string,
white?: string;
/** ANSI bright black (eg. `\x1b[1;30m`) */
brightBlack?: string,
brightBlack?: string;
/** ANSI bright red (eg. `\x1b[1;31m`) */
brightRed?: string,
brightRed?: string;
/** ANSI bright green (eg. `\x1b[1;32m`) */
brightGreen?: string,
brightGreen?: string;
/** ANSI bright yellow (eg. `\x1b[1;33m`) */
brightYellow?: string,
brightYellow?: string;
/** ANSI bright blue (eg. `\x1b[1;34m`) */
brightBlue?: string,
brightBlue?: string;
/** ANSI bright magenta (eg. `\x1b[1;35m`) */
brightMagenta?: string,
brightMagenta?: string;
/** ANSI bright cyan (eg. `\x1b[1;36m`) */
brightCyan?: string,
brightCyan?: string;
/** ANSI bright white (eg. `\x1b[1;37m`) */
brightWhite?: string
brightWhite?: string;
}
/**
@@ -386,6 +386,12 @@ declare module 'xterm' {
*/
readonly markers: ReadonlyArray<IMarker>;
/**
* (EXPERIMENTAL) Get the parser interface to register
* custom escape sequence handlers.
*/
readonly parser: IParser;
/**
* Natural language strings that can be localized.
*/
@@ -500,32 +506,6 @@ declare module 'xterm' {
*/
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
/**
* (EXPERIMENTAL) Adds a handler for CSI escape sequences.
* @param flag The flag should be one-character string, which specifies the
* final character (e.g "m" for SGR) of the CSI sequence.
* @param callback The function to handle the escape sequence. The callback
* is called with the numerical params, as well as the special characters
* (e.g. "$" for DECSCPP). If the sequence has subparams the array will
* contain subarrays with their numercial values.
* Return true if the sequence was handled; false if
* we should try a previous handler (set by addCsiHandler or setCsiHandler).
* The most recently-added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable;
/**
* (EXPERIMENTAL) Adds a handler for OSC escape sequences.
* @param ident The number (first parameter) of the sequence.
* @param callback The function to handle the escape sequence. The callback
* is called with OSC data string. Return true if the sequence was handled;
* false if we should try a previous handler (set by addOscHandler or
* setOscHandler). The most recently-added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
/**
* (EXPERIMENTAL) Registers a link matcher, allowing custom link patterns to
* be matched and handled.
@@ -804,7 +784,7 @@ declare module 'xterm' {
/**
* Perform a full reset (RIS, aka '\x1bc').
*/
reset(): void
reset(): void;
/**
* Loads an addon into this instance of xterm.js.
@@ -943,4 +923,117 @@ declare module 'xterm' {
*/
readonly width: number;
}
/**
* (EXPERIMENTAL) Data type to register a CSI, DCS or ESC callback in the parser
* in the form:
* ESC I..I F
* CSI Prefix P..P I..I F
* DCS Prefix P..P I..I F data_bytes ST
*
* with these rules/restrictions:
* - prefix can only be used with CSI and DCS
* - only one leading prefix byte is recognized by the parser
* before any other parameter bytes (P..P)
* - intermediate bytes are recognized up to 2
*
* For custom sequences make sure to read ECMA-48 and the resources at
* vt100.net to not clash with existing sequences or reserved address space.
* General recommendations:
* - use private address space (see ECMA-48)
* - use max one intermediate byte (technically not limited by the spec,
* in practice there are no sequences with more than one intermediate byte,
* thus parsers might get confused with more intermediates)
* - test against other common emulators to check whether they escape/ignore
* the sequence correctly
*
* Notes: OSC command registration is handled differently (see addOscHandler)
* APC, PM or SOS is currently not supported.
*/
export interface IFunctionIdentifier {
/**
* Optional prefix byte, must be in range \x3c .. \x3f.
* Usable in CSI and DCS.
*/
prefix?: string;
/**
* Optional intermediate bytes, must be in range \x20 .. \x2f.
* Usable in CSI, DCS and ESC.
*/
intermediates?: string;
/**
* Final byte, must be in range \x40 .. \x7e for CSI and DCS,
* \x30 .. \x7e for ESC.
*/
final: string;
}
/**
* (EXPERIMENTAL) Parser interface.
*/
export interface IParser {
/**
* Adds a handler for CSI escape sequences.
* @param id Specifies the function identifier under which the callback
* gets registered, e.g. {final: 'm'} for SGR.
* @param callback The function to handle the sequence. The callback is
* called with the numerical params. If the sequence has subparams the
* array will contain subarrays with their numercial values.
* Return true if the sequence was handled; false if we should try
* a previous handler (set by addCsiHandler or setCsiHandler).
* The most recently-added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable;
/**
* Adds a handler for DCS escape sequences.
* @param id Specifies the function identifier under which the callback
* gets registered, e.g. {intermediates: '$' final: 'q'} for DECRQSS.
* @param callback The function to handle the sequence. Note that the
* function will only be called once if the sequence finished sucessfully.
* There is currently no way to intercept smaller data chunks, data chunks
* will be stored up until the sequence is finished. Since DCS sequences
* are not limited by the amount of data this might impose a problem for
* big payloads. Currently xterm.js limits DCS payload to 10 MB
* which should give enough room for most use cases.
* The function gets the payload and numerical parameters as arguments.
* Return true if the sequence was handled; false if we should try
* a previous handler (set by addDcsHandler or setDcsHandler).
* The most recently-added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable;
/**
* Adds a handler for ESC escape sequences.
* @param id Specifies the function identifier under which the callback
* gets registered, e.g. {intermediates: '%' final: 'G'} for
* default charset selection.
* @param callback The function to handle the sequence.
* Return true if the sequence was handled; false if we should try
* a previous handler (set by addEscHandler or setEscHandler).
* The most recently-added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable;
/**
* Adds a handler for OSC escape sequences.
* @param ident The number (first parameter) of the sequence.
* @param callback The function to handle the sequence. Note that the
* function will only be called once if the sequence finished sucessfully.
* There is currently no way to intercept smaller data chunks, data chunks
* will be stored up until the sequence is finished. Since OSC sequences
* are not limited by the amount of data this might impose a problem for
* big payloads. Currently xterm.js limits OSC payload to 10 MB
* which should give enough room for most use cases.
* The callback is called with OSC data string.
* Return true if the sequence was handled; false if we should try
* a previous handler (set by addOscHandler or setOscHandler).
* The most recently-added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
}
}