Merge branch 'master' into feat/fast-scrolling
This commit is contained in:
@@ -3,13 +3,13 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService } from 'common/services/Services';
|
||||
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService } from 'common/services/Services';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { clone } from 'common/Clone';
|
||||
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
|
||||
import { IBufferSet, IBuffer } from 'common/buffer/Types';
|
||||
import { BufferSet } from 'common/buffer/BufferSet';
|
||||
import { IDecPrivateModes } from 'common/Types';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType } from 'common/Types';
|
||||
|
||||
export class MockBufferService implements IBufferService {
|
||||
serviceBrand: any;
|
||||
@@ -29,6 +29,19 @@ export class MockBufferService implements IBufferService {
|
||||
reset(): void {}
|
||||
}
|
||||
|
||||
export class MockCoreMouseService implements ICoreMouseService {
|
||||
activeEncoding: string = '';
|
||||
activeProtocol: string = '';
|
||||
addEncoding(name: string): void {}
|
||||
addProtocol(name: string): void {}
|
||||
reset(): void {}
|
||||
triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; }
|
||||
onProtocolChange: IEvent<CoreMouseEventType> = new EventEmitter<CoreMouseEventType>().event;
|
||||
explainEvents(events: CoreMouseEventType): {[event: string]: boolean} {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class MockCoreService implements ICoreService {
|
||||
serviceBrand: any;
|
||||
decPrivateModes: IDecPrivateModes = {} as any;
|
||||
|
||||
Vendored
+93
@@ -158,3 +158,96 @@ export interface IRowRange {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for mouse events in the core.
|
||||
*/
|
||||
export const enum CoreMouseButton {
|
||||
LEFT = 0,
|
||||
MIDDLE = 1,
|
||||
RIGHT = 2,
|
||||
NONE = 3,
|
||||
WHEEL = 4,
|
||||
// additional buttons 1..8
|
||||
// untested!
|
||||
AUX1 = 8,
|
||||
AUX2 = 9,
|
||||
AUX3 = 10,
|
||||
AUX4 = 11,
|
||||
AUX5 = 12,
|
||||
AUX6 = 13,
|
||||
AUX7 = 14,
|
||||
AUX8 = 15
|
||||
}
|
||||
|
||||
export const enum CoreMouseAction {
|
||||
UP = 0, // buttons, wheel
|
||||
DOWN = 1, // buttons, wheel
|
||||
LEFT = 2, // wheel only
|
||||
RIGHT = 3, // wheel only
|
||||
MOVE = 32 // buttons only
|
||||
}
|
||||
|
||||
export interface ICoreMouseEvent {
|
||||
/** column (zero based). */
|
||||
col: number;
|
||||
/** row (zero based). */
|
||||
row: number;
|
||||
/**
|
||||
* Button the action occured. Due to restrictions of the tracking protocols
|
||||
* it is not possible to report multiple buttons at once.
|
||||
* Wheel is treated as a button.
|
||||
* There are invalid combinations of buttons and actions possible
|
||||
* (like move + wheel), those are silently ignored by the CoreMouseService.
|
||||
*/
|
||||
button: CoreMouseButton;
|
||||
action: CoreMouseAction;
|
||||
/**
|
||||
* Modifier states.
|
||||
* Protocols will add/ignore those based on specific restrictions.
|
||||
*/
|
||||
ctrl?: boolean;
|
||||
alt?: boolean;
|
||||
shift?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* CoreMouseEventType
|
||||
* To be reported to the browser component which events a mouse
|
||||
* protocol wants to be catched and forwarded as an ICoreMouseEvent
|
||||
* to CoreMouseService.
|
||||
*/
|
||||
export const enum CoreMouseEventType {
|
||||
NONE = 0,
|
||||
/** any mousedown event */
|
||||
DOWN = 1,
|
||||
/** any mouseup event */
|
||||
UP = 2,
|
||||
/** any mousemove event while a button is held */
|
||||
DRAG = 4,
|
||||
/** any mousemove event without a button */
|
||||
MOVE = 8,
|
||||
/** any wheel event */
|
||||
WHEEL = 16
|
||||
}
|
||||
|
||||
/**
|
||||
* Mouse protocol interface.
|
||||
* A mouse protocol can be registered and activated at the CoreMouseService.
|
||||
* `events` should contain a list of needed events as a hint for the browser component
|
||||
* to install/remove the appropriate event handlers.
|
||||
* `restrict` applies further protocol specific restrictions like not allowed
|
||||
* modifiers or filtering invalid event types.
|
||||
*/
|
||||
export interface ICoreMouseProtocol {
|
||||
events: CoreMouseEventType;
|
||||
restrict: (e: ICoreMouseEvent) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* CoreMouseEncoding
|
||||
* The tracking encoding can be registered and activated at the CoreMouseService.
|
||||
* If a ICoreMouseEvent passes all procotol restrictions it will be encoded
|
||||
* with the active encoding and sent out.
|
||||
*/
|
||||
export type CoreMouseEncoding = (event: ICoreMouseEvent) => string;
|
||||
|
||||
@@ -1163,237 +1163,4 @@ describe('Buffer', () => {
|
||||
assert.equal(str3, '😁a');
|
||||
});
|
||||
});
|
||||
// describe('stringIndexToBufferIndex', () => {
|
||||
// let terminal: TestTerminal;
|
||||
|
||||
// beforeEach(() => {
|
||||
// terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
|
||||
// });
|
||||
|
||||
// it('multiline ascii', () => {
|
||||
// const input = 'This is ASCII text spanning multiple lines.';
|
||||
// terminal.writeSync(input);
|
||||
// const s = terminal.buffer.iterator(true).next().content;
|
||||
// assert.equal(input, s);
|
||||
// for (let i = 0; i < input.length; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
// assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
|
||||
// }
|
||||
// });
|
||||
|
||||
// it('combining e\u0301 in a sentence', () => {
|
||||
// const input = 'Sitting in the cafe\u0301 drinking coffee.';
|
||||
// terminal.writeSync(input);
|
||||
// const s = terminal.buffer.iterator(true).next().content;
|
||||
// assert.equal(input, s);
|
||||
// for (let i = 0; i < 19; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
// assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
|
||||
// }
|
||||
// // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index
|
||||
// assert.deepEqual(
|
||||
// terminal.buffer.stringIndexToBufferIndex(0, 18),
|
||||
// terminal.buffer.stringIndexToBufferIndex(0, 19));
|
||||
// // after the combining char every string index has an offset of -1
|
||||
// for (let i = 19; i < input.length; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
// assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
|
||||
// }
|
||||
// });
|
||||
|
||||
// it('multiline combining e\u0301', () => {
|
||||
// const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301';
|
||||
// terminal.writeSync(input);
|
||||
// const s = terminal.buffer.iterator(true).next().content;
|
||||
// assert.equal(input, s);
|
||||
// // every buffer cell index contains 2 string indices
|
||||
// for (let i = 0; i < input.length; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
// assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
|
||||
// }
|
||||
// });
|
||||
|
||||
// it('surrogate char in a sentence', () => {
|
||||
// const input = 'The 𝄞 is a clef widely used in modern notation.';
|
||||
// terminal.writeSync(input);
|
||||
// const s = terminal.buffer.iterator(true).next().content;
|
||||
// assert.equal(input, s);
|
||||
// for (let i = 0; i < 5; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
// assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
|
||||
// }
|
||||
// // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index
|
||||
// assert.deepEqual(
|
||||
// terminal.buffer.stringIndexToBufferIndex(0, 4),
|
||||
// terminal.buffer.stringIndexToBufferIndex(0, 5));
|
||||
// // after the combining char every string index has an offset of -1
|
||||
// for (let i = 5; i < input.length; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
// assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
|
||||
// }
|
||||
// });
|
||||
|
||||
// it('multiline surrogate char', () => {
|
||||
// const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞';
|
||||
// terminal.writeSync(input);
|
||||
// const s = terminal.buffer.iterator(true).next().content;
|
||||
// assert.equal(input, s);
|
||||
// // every buffer cell index contains 2 string indices
|
||||
// for (let i = 0; i < input.length; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
// assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
|
||||
// }
|
||||
// });
|
||||
|
||||
// it('surrogate char with combining', () => {
|
||||
// // eye of Ra with acute accent - string length of 3
|
||||
// const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.';
|
||||
// terminal.writeSync(input);
|
||||
// const s = terminal.buffer.iterator(true).next().content;
|
||||
// assert.equal(input, s);
|
||||
// // index 0..2 should map to 0
|
||||
// assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1));
|
||||
// assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2));
|
||||
// for (let i = 2; i < input.length; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
// assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex);
|
||||
// }
|
||||
// });
|
||||
|
||||
// it('multiline surrogate with combining', () => {
|
||||
// const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301';
|
||||
// terminal.writeSync(input);
|
||||
// const s = terminal.buffer.iterator(true).next().content;
|
||||
// assert.equal(input, s);
|
||||
// // every buffer cell index contains 3 string indices
|
||||
// for (let i = 0; i < input.length; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
// assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex);
|
||||
// }
|
||||
// });
|
||||
|
||||
// it('fullwidth chars', () => {
|
||||
// const input = 'These 123 are some fat numbers.';
|
||||
// terminal.writeSync(input);
|
||||
// const s = terminal.buffer.iterator(true).next().content;
|
||||
// assert.equal(input, s);
|
||||
// for (let i = 0; i < 6; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
// assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
|
||||
// }
|
||||
// // string index 6, 7, 8 take 2 cells
|
||||
// assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7));
|
||||
// assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8));
|
||||
// // rest of the string has offset of +3
|
||||
// for (let i = 9; i < input.length; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
// assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex);
|
||||
// }
|
||||
// });
|
||||
|
||||
// it('multiline fullwidth chars', () => {
|
||||
// const input = '12345678901234567890';
|
||||
// terminal.writeSync(input);
|
||||
// const s = terminal.buffer.iterator(true).next().content;
|
||||
// assert.equal(input, s);
|
||||
// for (let i = 9; i < input.length; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
// assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex);
|
||||
// }
|
||||
// });
|
||||
|
||||
// it('fullwidth combining with emoji - match emoji cell', () => {
|
||||
// const input = 'Lots of ¥\u0301 make me 😃.';
|
||||
// terminal.writeSync(input);
|
||||
// const s = terminal.buffer.iterator(true).next().content;
|
||||
// assert.equal(input, s);
|
||||
// const stringIndex = s.match(/😃/).index;
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex);
|
||||
// assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars(), '😃');
|
||||
// });
|
||||
|
||||
// it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => {
|
||||
// const input = 'a12345678901234567890';
|
||||
// // the 'a' at the beginning moves all fullwidth chars one to the right
|
||||
// // now the end of the line contains a dangling empty cell since
|
||||
// // the next fullwidth char has to wrap early
|
||||
// // the dangling last cell is wrongly added in the string
|
||||
// // --> fixable after resolving #1685
|
||||
// terminal.writeSync(input);
|
||||
// const s = terminal.buffer.iterator(true).next().content;
|
||||
// assert.equal(input, s);
|
||||
// for (let i = 10; i < input.length; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
|
||||
// const j = (i - 0) << 1;
|
||||
// assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex);
|
||||
// }
|
||||
// });
|
||||
|
||||
// it('test fully wrapped buffer up to last char', () => {
|
||||
// const input = Array(6).join('1234567890');
|
||||
// terminal.writeSync(input);
|
||||
// const s = terminal.buffer.iterator(true).next().content;
|
||||
// assert.equal(input, s);
|
||||
// for (let i = 0; i < input.length; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
|
||||
// assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars());
|
||||
// }
|
||||
// });
|
||||
|
||||
// it('test fully wrapped buffer up to last char with full width odd', () => {
|
||||
// const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'
|
||||
// + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301';
|
||||
// terminal.writeSync(input);
|
||||
// const s = terminal.buffer.iterator(true).next().content;
|
||||
// assert.equal(input, s);
|
||||
// for (let i = 0; i < input.length; ++i) {
|
||||
// const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
|
||||
// assert.equal(
|
||||
// (!(i % 3))
|
||||
// ? input[i]
|
||||
// : (i % 3 === 1)
|
||||
// ? input.substr(i, 2)
|
||||
// : input.substr(i - 1, 2),
|
||||
// terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars());
|
||||
// }
|
||||
// });
|
||||
|
||||
// it('should handle \t in lines correctly', () => {
|
||||
// const input = '\thttps://google.de';
|
||||
// terminal.writeSync(input);
|
||||
// const s = terminal.buffer.iterator(true).next().content;
|
||||
// assert.equal(s, Array(optionsService.options.tabStopWidth + 1).join(' ') + 'https://google.de');
|
||||
// });
|
||||
// });
|
||||
// describe('BufferStringIterator', function(): void {
|
||||
// it('iterator does not overflow buffer limits', function(): void {
|
||||
// const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
|
||||
// const data = [
|
||||
// 'aaaaaaaaaa',
|
||||
// 'aaaaaaaaa\n',
|
||||
// 'aaaaaaaaaa',
|
||||
// 'aaaaaaaaa\n',
|
||||
// 'aaaaaaaaaa',
|
||||
// 'aaaaaaaaaa',
|
||||
// 'aaaaaaaaaa',
|
||||
// 'aaaaaaaaa\n',
|
||||
// 'aaaaaaaaaa',
|
||||
// 'aaaaaaaaaa'
|
||||
// ];
|
||||
// terminal.writeSync(data.join(''));
|
||||
// // brute force test with insane values
|
||||
// expect(() => {
|
||||
// for (let overscan = 0; overscan < 20; ++overscan) {
|
||||
// for (let start = -10; start < 20; ++start) {
|
||||
// for (let end = -10; end < 20; ++end) {
|
||||
// const it = terminal.buffer.iterator(false, start, end, overscan, overscan);
|
||||
// while (it.hasNext()) {
|
||||
// it.next();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }).to.not.throw();
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
||||
Vendored
+1
-1
@@ -40,7 +40,7 @@ export interface IBuffer {
|
||||
nextStop(x?: number): number;
|
||||
prevStop(x?: number): number;
|
||||
getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine;
|
||||
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[];
|
||||
stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight?: boolean): number[];
|
||||
iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator;
|
||||
getNullCell(attr?: IAttributeData): ICellData;
|
||||
getWhitespaceCell(attr?: IAttributeData): ICellData;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, []);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,15 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDcsHandler, IParsingState, IParams, ParamsArray } from 'common/parser/Types';
|
||||
import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier } from 'common/parser/Types';
|
||||
import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser';
|
||||
import * as chai from 'chai';
|
||||
import { StringToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
|
||||
import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder';
|
||||
import { ParserState } from 'common/parser/Constants';
|
||||
import { Params } from 'common/parser/Params';
|
||||
import { OscHandler } from 'common/parser/OscParser';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { DcsHandler } from 'common/parser/DcsParser';
|
||||
|
||||
|
||||
function r(a: number, b: number): string[] {
|
||||
@@ -20,13 +23,46 @@ function r(a: number, b: number): string[] {
|
||||
return arr;
|
||||
}
|
||||
|
||||
class MockOscPutParser implements IOscParser {
|
||||
private _fallback: OscFallbackHandlerType = () => {};
|
||||
public data = '';
|
||||
public reset(): void {
|
||||
this.data = '';
|
||||
}
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
this.data += utf32ToString(data, start, end);
|
||||
}
|
||||
public dispose(): void { }
|
||||
public start(): void { }
|
||||
public end(success: boolean): void {
|
||||
this.data += `, success: ${success}`;
|
||||
const id = parseInt(this.data.slice(0, this.data.indexOf(';')));
|
||||
if (!isNaN(id)) {
|
||||
this._fallback(id, 'END', this.data.slice(this.data.indexOf(';') + 1));
|
||||
}
|
||||
}
|
||||
addHandler(ident: number, handler: IOscHandler): IDisposable {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
setHandler(ident: number, handler: IOscHandler): void {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
clearHandler(ident: number): void {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
setHandlerFallback(handler: OscFallbackHandlerType): void {
|
||||
this._fallback = handler;
|
||||
}
|
||||
}
|
||||
const oscPutParser = new MockOscPutParser();
|
||||
|
||||
// derived parser with access to internal states
|
||||
class TestEscapeSequenceParser extends EscapeSequenceParser {
|
||||
public get osc(): string {
|
||||
return this._osc;
|
||||
return (this._oscParser as MockOscPutParser).data;
|
||||
}
|
||||
public set osc(value: string) {
|
||||
this._osc = value;
|
||||
(this._oscParser as MockOscPutParser).data = value;
|
||||
}
|
||||
public get params(): ParamsArray {
|
||||
return this._params.toArray();
|
||||
@@ -38,13 +74,20 @@ class TestEscapeSequenceParser extends EscapeSequenceParser {
|
||||
return this._params;
|
||||
}
|
||||
public get collect(): string {
|
||||
return this._collect;
|
||||
return this.identToString(this._collect);
|
||||
}
|
||||
public set collect(value: string) {
|
||||
this._collect = value;
|
||||
this._collect = 0;
|
||||
for (let i = 0; i < value.length; ++i) {
|
||||
this._collect <<= 8;
|
||||
this._collect |= value.charCodeAt(i);
|
||||
}
|
||||
}
|
||||
public mockActiveDcsHandler(): void {
|
||||
this._activeDcsHandler = this._dcsHandlerFb;
|
||||
public mockOscParser(): void {
|
||||
this._oscParser = oscPutParser;
|
||||
}
|
||||
public identifier(id: IFunctionIdentifier): number {
|
||||
return this._identifier(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,34 +119,17 @@ const testTerminal: any = {
|
||||
actionESC: function (collect: string, flag: string): void {
|
||||
this.calls.push(['esc', collect, flag]);
|
||||
},
|
||||
actionDCSHook: function (collect: string, params: IParams, flag: string): void {
|
||||
this.calls.push(['dcs hook', collect, params.toArray(), flag]);
|
||||
actionDCSHook: function (params: IParams): void {
|
||||
this.calls.push(['dcs hook', params.toArray()]);
|
||||
},
|
||||
actionDCSPrint: function (data: Uint32Array, start: number, end: number): void {
|
||||
let s = '';
|
||||
for (let i = start; i < end; ++i) {
|
||||
s += stringFromCodePoint(data[i]);
|
||||
}
|
||||
actionDCSPrint: function (s: string): void {
|
||||
this.calls.push(['dcs put', s]);
|
||||
},
|
||||
actionDCSUnhook: function (): void {
|
||||
this.calls.push(['dcs unhook']);
|
||||
actionDCSUnhook: function (success: boolean): void {
|
||||
this.calls.push(['dcs unhook', success]);
|
||||
}
|
||||
};
|
||||
|
||||
// dcs handler to map dcs actions into the test object `testTerminal`
|
||||
class DcsTest implements IDcsHandler {
|
||||
hook(collect: string, params: IParams, flag: number): void {
|
||||
testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag));
|
||||
}
|
||||
put(data: Uint32Array, start: number, end: number): void {
|
||||
testTerminal.actionDCSPrint(data, start, end);
|
||||
}
|
||||
unhook(): void {
|
||||
testTerminal.actionDCSUnhook();
|
||||
}
|
||||
}
|
||||
|
||||
const states: number[] = [
|
||||
ParserState.GROUND,
|
||||
ParserState.ESCAPE,
|
||||
@@ -124,21 +150,35 @@ let state: any;
|
||||
|
||||
// parser with Uint8Array based transition table
|
||||
const testParser = new TestEscapeSequenceParser();
|
||||
testParser.mockOscParser();
|
||||
testParser.setPrintHandler(testTerminal.print.bind(testTerminal));
|
||||
testParser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => {
|
||||
testTerminal.actionCSI(collect, params, String.fromCharCode(flag));
|
||||
testParser.setCsiHandlerFallback((ident: number, params: IParams) => {
|
||||
const id = testParser.identToString(ident);
|
||||
testTerminal.actionCSI(id.slice(0, -1), params, id.slice(-1));
|
||||
});
|
||||
testParser.setEscHandlerFallback((collect: string, flag: number) => {
|
||||
testTerminal.actionESC(collect, String.fromCharCode(flag));
|
||||
testParser.setEscHandlerFallback((ident: number) => {
|
||||
const id = testParser.identToString(ident);
|
||||
testTerminal.actionESC(id.slice(0, -1), id.slice(-1));
|
||||
});
|
||||
testParser.setExecuteHandlerFallback((code: number) => {
|
||||
testTerminal.actionExecute(String.fromCharCode(code));
|
||||
});
|
||||
testParser.setOscHandlerFallback((identifier: number, data: string) => {
|
||||
testParser.setOscHandlerFallback((identifier, action, data) => {
|
||||
if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently
|
||||
else testTerminal.actionOSC('' + identifier + ';' + data);
|
||||
else if (action === 'END') testTerminal.actionOSC('' + identifier + ';' + data); // collect only data at END
|
||||
});
|
||||
testParser.setDcsHandlerFallback((collectAndFlag, action, payload) => {
|
||||
switch (action) {
|
||||
case 'HOOK':
|
||||
testTerminal.actionDCSHook(payload);
|
||||
break;
|
||||
case 'PUT':
|
||||
testTerminal.actionDCSPrint(payload);
|
||||
break;
|
||||
case 'UNHOOK':
|
||||
testTerminal.actionDCSUnhook(payload);
|
||||
}
|
||||
});
|
||||
testParser.setDcsHandlerFallback(new DcsTest());
|
||||
|
||||
|
||||
// translate string based parse calls into typed array based
|
||||
@@ -217,7 +257,8 @@ describe('EscapeSequenceParser', function (): void {
|
||||
'\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x99', '\x9a'
|
||||
];
|
||||
const exceptions: { [key: number]: { [key: string]: any[] } } = {
|
||||
8: { '\x18': [], '\x1a': [] } // simply abort osc state
|
||||
8: { '\x18': [], '\x1a': [] }, // abort OSC_STRING
|
||||
13: { '\x18': [['dcs unhook', false]], '\x1a': [['dcs unhook', false]] } // abort DCS_PASSTHROUGH
|
||||
};
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
@@ -241,12 +282,10 @@ describe('EscapeSequenceParser', function (): void {
|
||||
parser.reset();
|
||||
for (state in states) {
|
||||
parser.currentState = state;
|
||||
parser.osc = '#';
|
||||
parser.params = [23];
|
||||
parser.collect = '#';
|
||||
parse(parser, '\x1b');
|
||||
chai.expect(parser.currentState).equal(ParserState.ESCAPE);
|
||||
chai.expect(parser.osc).equal('');
|
||||
chai.expect(parser.params).eql([0]);
|
||||
chai.expect(parser.collect).equal('');
|
||||
parser.reset();
|
||||
@@ -358,24 +397,20 @@ describe('EscapeSequenceParser', function (): void {
|
||||
parser.reset();
|
||||
// C0
|
||||
parser.currentState = ParserState.ESCAPE;
|
||||
parser.osc = '#';
|
||||
parser.params = [123];
|
||||
parser.collect = '#';
|
||||
parse(parser, '[');
|
||||
chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY);
|
||||
chai.expect(parser.osc).equal('');
|
||||
chai.expect(parser.params).eql([0]);
|
||||
chai.expect(parser.collect).equal('');
|
||||
parser.reset();
|
||||
// C1
|
||||
for (state in states) {
|
||||
parser.currentState = state;
|
||||
parser.osc = '#';
|
||||
parser.params = [123];
|
||||
parser.collect = '#';
|
||||
parse(parser, '\x9b');
|
||||
chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY);
|
||||
chai.expect(parser.osc).equal('');
|
||||
chai.expect(parser.params).eql([0]);
|
||||
chai.expect(parser.collect).equal('');
|
||||
parser.reset();
|
||||
@@ -909,7 +944,7 @@ describe('EscapeSequenceParser', function (): void {
|
||||
parser.currentState = ParserState.DCS_ENTRY;
|
||||
parse(parser, collect[i]);
|
||||
chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH);
|
||||
testTerminal.compare([['dcs hook', '', [0], collect[i]]]);
|
||||
testTerminal.compare([['dcs hook', [0]]]);
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
}
|
||||
@@ -922,7 +957,7 @@ describe('EscapeSequenceParser', function (): void {
|
||||
parser.currentState = ParserState.DCS_PARAM;
|
||||
parse(parser, collect[i]);
|
||||
chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH);
|
||||
testTerminal.compare([['dcs hook', '', [0], collect[i]]]);
|
||||
testTerminal.compare([['dcs hook', [0]]]);
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
}
|
||||
@@ -935,7 +970,7 @@ describe('EscapeSequenceParser', function (): void {
|
||||
parser.currentState = ParserState.DCS_INTERMEDIATE;
|
||||
parse(parser, collect[i]);
|
||||
chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH);
|
||||
testTerminal.compare([['dcs hook', '', [0], collect[i]]]);
|
||||
testTerminal.compare([['dcs hook', [0]]]);
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
}
|
||||
@@ -949,7 +984,6 @@ describe('EscapeSequenceParser', function (): void {
|
||||
puts = puts.concat(r(0x20, 0x7f));
|
||||
for (let i = 0; i < puts.length; ++i) {
|
||||
parser.currentState = ParserState.DCS_PASSTHROUGH;
|
||||
parser.mockActiveDcsHandler();
|
||||
parse(parser, puts[i]);
|
||||
chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH);
|
||||
testTerminal.compare([['dcs put', puts[i]]]);
|
||||
@@ -990,33 +1024,33 @@ describe('EscapeSequenceParser', function (): void {
|
||||
});
|
||||
it('OSC', function (): void {
|
||||
test('\x1b]0;abc123€öäü\x07', [
|
||||
['osc', '0;abc123€öäü']
|
||||
['osc', '0;abc123€öäü, success: true']
|
||||
], null);
|
||||
});
|
||||
it('single DCS', function (): void {
|
||||
test('\x1bP1;2;3+$aäbc;däe\x9c', [
|
||||
['dcs hook', '+$', [1, 2, 3], 'a'],
|
||||
['dcs hook', [1, 2, 3]],
|
||||
['dcs put', 'äbc;däe'],
|
||||
['dcs unhook']
|
||||
['dcs unhook', true]
|
||||
], null);
|
||||
});
|
||||
it('multi DCS', function (): void {
|
||||
test('\x1bP1;2;3+$abc;de', [
|
||||
['dcs hook', '+$', [1, 2, 3], 'a'],
|
||||
['dcs hook', [1, 2, 3]],
|
||||
['dcs put', 'bc;de']
|
||||
], null);
|
||||
testTerminal.clear();
|
||||
test('abc\x9c', [
|
||||
['dcs put', 'abc'],
|
||||
['dcs unhook']
|
||||
['dcs unhook', true]
|
||||
], true);
|
||||
});
|
||||
it('print + DCS(C1)', function (): void {
|
||||
test('abc\x901;2;3+$abc;de\x9c', [
|
||||
['print', 'abc'],
|
||||
['dcs hook', '+$', [1, 2, 3], 'a'],
|
||||
['dcs hook', [1, 2, 3]],
|
||||
['dcs put', 'bc;de'],
|
||||
['dcs unhook']
|
||||
['dcs unhook', true]
|
||||
], null);
|
||||
});
|
||||
it('print + PM(C1) + print', function (): void {
|
||||
@@ -1026,9 +1060,9 @@ describe('EscapeSequenceParser', function (): void {
|
||||
], null);
|
||||
});
|
||||
it('print + OSC(C1) + print', function (): void {
|
||||
test('abc\x9d123tzf\x9cdefg', [
|
||||
test('abc\x9d123;tzf\x9cdefg', [
|
||||
['print', 'abc'],
|
||||
['osc', '123tzf'],
|
||||
['osc', '123;tzf, success: true'],
|
||||
['print', 'defg']
|
||||
], null);
|
||||
});
|
||||
@@ -1039,9 +1073,9 @@ describe('EscapeSequenceParser', function (): void {
|
||||
], null);
|
||||
});
|
||||
it('7bit ST should be swallowed', function (): void {
|
||||
test('abc\x9d123tzf\x1b\\defg', [
|
||||
test('abc\x9d123;tzf\x1b\\defg', [
|
||||
['print', 'abc'],
|
||||
['osc', '123tzf'],
|
||||
['osc', '123;tzf, success: true'],
|
||||
['print', 'defg']
|
||||
], null);
|
||||
});
|
||||
@@ -1057,9 +1091,35 @@ describe('EscapeSequenceParser', function (): void {
|
||||
it('colon notation in DCS params', function (): void {
|
||||
test('abc\x901;2::55;3+$abc;de\x9c', [
|
||||
['print', 'abc'],
|
||||
['dcs hook', '+$', [1, 2, [-1, 55], 3], 'a'],
|
||||
['dcs hook', [1, 2, [-1, 55], 3]],
|
||||
['dcs put', 'bc;de'],
|
||||
['dcs unhook']
|
||||
['dcs unhook', true]
|
||||
], null);
|
||||
});
|
||||
it('CAN should abort DCS', () => {
|
||||
test('abc\x901;2::55;3+$abc;de\x18', [
|
||||
['print', 'abc'],
|
||||
['dcs hook', [1, 2, [-1, 55], 3]],
|
||||
['dcs put', 'bc;de'],
|
||||
['dcs unhook', false] // false for abort
|
||||
], null);
|
||||
});
|
||||
it('SUB should abort DCS', () => {
|
||||
test('abc\x901;2::55;3+$abc;de\x1a', [
|
||||
['print', 'abc'],
|
||||
['dcs hook', [1, 2, [-1, 55], 3]],
|
||||
['dcs put', 'bc;de'],
|
||||
['dcs unhook', false] // false for abort
|
||||
], null);
|
||||
});
|
||||
it('CAN should abort OSC', () => {
|
||||
test('\x1b]0;abc123€öäü\x18', [
|
||||
['osc', '0;abc123€öäü, success: false']
|
||||
], null);
|
||||
});
|
||||
it('SUB should abort OSC', () => {
|
||||
test('\x1b]0;abc123€öäü\x1a', [
|
||||
['osc', '0;abc123€öäü, success: false']
|
||||
], null);
|
||||
});
|
||||
});
|
||||
@@ -1091,7 +1151,7 @@ describe('EscapeSequenceParser', function (): void {
|
||||
parser.currentState = ParserState.DCS_PASSTHROUGH;
|
||||
parse(parser, '\x901;2;3+$a€öäü');
|
||||
chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH);
|
||||
testTerminal.compare([['dcs hook', '+$', [1, 2, 3], 'a'], ['dcs put', '€öäü']]);
|
||||
testTerminal.compare([['dcs hook', [1, 2, 3]], ['dcs put', '€öäü']]);
|
||||
parser.reset();
|
||||
testTerminal.clear();
|
||||
});
|
||||
@@ -1143,32 +1203,83 @@ describe('EscapeSequenceParser', function (): void {
|
||||
chai.expect(print).equal('');
|
||||
});
|
||||
it('ESC handler', function (): void {
|
||||
parser2.setEscHandler('%G', function (): void {
|
||||
parser2.setEscHandler({intermediates: '%', final: 'G'}, function (): void {
|
||||
esc.push('%G');
|
||||
});
|
||||
parser2.setEscHandler('E', function (): void {
|
||||
parser2.setEscHandler({final: 'E'}, function (): void {
|
||||
esc.push('E');
|
||||
});
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(esc).eql(['%G', 'E']);
|
||||
parser2.clearEscHandler('%G');
|
||||
parser2.clearEscHandler('%G'); // should not throw
|
||||
parser2.clearEscHandler({intermediates: '%', final: 'G'});
|
||||
parser2.clearEscHandler({intermediates: '%', final: 'G'}); // should not throw
|
||||
clearAccu();
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(esc).eql(['E']);
|
||||
parser2.clearEscHandler('E');
|
||||
parser2.clearEscHandler({final: 'E'});
|
||||
clearAccu();
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(esc).eql([]);
|
||||
});
|
||||
describe('ESC custom handlers', () => {
|
||||
it('prevent fallback', () => {
|
||||
parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); });
|
||||
parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(esc).eql(['custom - %G']);
|
||||
});
|
||||
it('allow fallback', () => {
|
||||
parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); });
|
||||
parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return false; });
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(esc).eql(['custom - %G', 'default - %G']);
|
||||
});
|
||||
it('Multiple custom handlers fallback once', () => {
|
||||
parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); });
|
||||
parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
|
||||
parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return false; });
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(esc).eql(['custom2 - %G', 'custom - %G']);
|
||||
});
|
||||
it('Multiple custom handlers no fallback', () => {
|
||||
parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); });
|
||||
parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
|
||||
parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return true; });
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(esc).eql(['custom2 - %G']);
|
||||
});
|
||||
it('Execution order should go from latest handler down to the original', () => {
|
||||
const order: number[] = [];
|
||||
parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { order.push(1); });
|
||||
parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { order.push(2); return false; });
|
||||
parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { order.push(3); return false; });
|
||||
parse(parser2, '\x1b%G');
|
||||
chai.expect(order).eql([3, 2, 1]);
|
||||
});
|
||||
it('Dispose should work', () => {
|
||||
parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); });
|
||||
const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
|
||||
dispo.dispose();
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(esc).eql(['default - %G']);
|
||||
});
|
||||
it('Should not corrupt the parser when dispose is called twice', () => {
|
||||
parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); });
|
||||
const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
|
||||
dispo.dispose();
|
||||
dispo.dispose();
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(esc).eql(['default - %G']);
|
||||
});
|
||||
});
|
||||
it('CSI handler', function (): void {
|
||||
parser2.setCsiHandler('m', function (params: IParams, collect: string): void {
|
||||
csi.push(['m', params.toArray(), collect]);
|
||||
parser2.setCsiHandler({final: 'm'}, function (params: IParams): void {
|
||||
csi.push(['m', params.toArray(), '']);
|
||||
});
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]);
|
||||
parser2.clearCsiHandler('m');
|
||||
parser2.clearCsiHandler('m'); // should not throw
|
||||
parser2.clearCsiHandler({final: 'm'});
|
||||
parser2.clearCsiHandler({final: 'm'}); // should not throw
|
||||
clearAccu();
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(csi).eql([]);
|
||||
@@ -1176,16 +1287,16 @@ describe('EscapeSequenceParser', function (): void {
|
||||
describe('CSI custom handlers', () => {
|
||||
it('Prevent fallback', () => {
|
||||
const csiCustom: [string, ParamsArray, string][] = [];
|
||||
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect]));
|
||||
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; });
|
||||
parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); });
|
||||
parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(csi).eql([], 'Should not fallback to original handler');
|
||||
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
|
||||
});
|
||||
it('Allow fallback', () => {
|
||||
const csiCustom: [string, ParamsArray, string][] = [];
|
||||
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect]));
|
||||
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return false; });
|
||||
parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); });
|
||||
parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return false; });
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler');
|
||||
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
|
||||
@@ -1193,9 +1304,9 @@ describe('EscapeSequenceParser', function (): void {
|
||||
it('Multiple custom handlers fallback once', () => {
|
||||
const csiCustom: [string, ParamsArray, string][] = [];
|
||||
const csiCustom2: [string, ParamsArray, string][] = [];
|
||||
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect]));
|
||||
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; });
|
||||
parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params.toArray(), collect]); return false; });
|
||||
parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); });
|
||||
parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return false; });
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(csi).eql([], 'Should not fallback to original handler');
|
||||
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
|
||||
@@ -1204,9 +1315,9 @@ describe('EscapeSequenceParser', function (): void {
|
||||
it('Multiple custom handlers no fallback', () => {
|
||||
const csiCustom: [string, ParamsArray, string][] = [];
|
||||
const csiCustom2: [string, ParamsArray, string][] = [];
|
||||
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect]));
|
||||
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; });
|
||||
parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params.toArray(), collect]); return true; });
|
||||
parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); });
|
||||
parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return true; });
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(csi).eql([], 'Should not fallback to original handler');
|
||||
chai.expect(csiCustom).eql([], 'Should not fallback once');
|
||||
@@ -1214,16 +1325,16 @@ describe('EscapeSequenceParser', function (): void {
|
||||
});
|
||||
it('Execution order should go from latest handler down to the original', () => {
|
||||
const order: number[] = [];
|
||||
parser2.setCsiHandler('m', () => order.push(1));
|
||||
parser2.addCsiHandler('m', () => { order.push(2); return false; });
|
||||
parser2.addCsiHandler('m', () => { order.push(3); return false; });
|
||||
parser2.setCsiHandler({final: 'm'}, () => { order.push(1); });
|
||||
parser2.addCsiHandler({final: 'm'}, () => { order.push(2); return false; });
|
||||
parser2.addCsiHandler({final: 'm'}, () => { order.push(3); return false; });
|
||||
parse(parser2, '\x1b[0m');
|
||||
chai.expect(order).eql([3, 2, 1]);
|
||||
});
|
||||
it('Dispose should work', () => {
|
||||
const csiCustom: [string, ParamsArray, string][] = [];
|
||||
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect]));
|
||||
const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; });
|
||||
parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); });
|
||||
const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
customHandler.dispose();
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]);
|
||||
@@ -1231,8 +1342,8 @@ describe('EscapeSequenceParser', function (): void {
|
||||
});
|
||||
it('Should not corrupt the parser when dispose is called twice', () => {
|
||||
const csiCustom: [string, ParamsArray, string][] = [];
|
||||
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect]));
|
||||
const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; });
|
||||
parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); });
|
||||
const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
|
||||
customHandler.dispose();
|
||||
customHandler.dispose();
|
||||
parse(parser2, INPUT);
|
||||
@@ -1256,9 +1367,9 @@ describe('EscapeSequenceParser', function (): void {
|
||||
chai.expect(exe).eql(['\n']);
|
||||
});
|
||||
it('OSC handler', function (): void {
|
||||
parser2.setOscHandler(1, function (data: string): void {
|
||||
parser2.setOscHandler(1, new OscHandler(function (data: string): void {
|
||||
osc.push([1, data]);
|
||||
});
|
||||
}));
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(osc).eql([[1, 'foo=bar']]);
|
||||
parser2.clearOscHandler(1);
|
||||
@@ -1270,16 +1381,16 @@ describe('EscapeSequenceParser', function (): void {
|
||||
describe('OSC custom handlers', () => {
|
||||
it('Prevent fallback', () => {
|
||||
const oscCustom: [number, string][] = [];
|
||||
parser2.setOscHandler(1, data => osc.push([1, data]));
|
||||
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
|
||||
parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data])));
|
||||
parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; }));
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(osc).eql([], 'Should not fallback to original handler');
|
||||
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
|
||||
});
|
||||
it('Allow fallback', () => {
|
||||
const oscCustom: [number, string][] = [];
|
||||
parser2.setOscHandler(1, data => osc.push([1, data]));
|
||||
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; });
|
||||
parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data])));
|
||||
parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return false; }));
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler');
|
||||
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
|
||||
@@ -1287,9 +1398,9 @@ describe('EscapeSequenceParser', function (): void {
|
||||
it('Multiple custom handlers fallback once', () => {
|
||||
const oscCustom: [number, string][] = [];
|
||||
const oscCustom2: [number, string][] = [];
|
||||
parser2.setOscHandler(1, data => osc.push([1, data]));
|
||||
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
|
||||
parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return false; });
|
||||
parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data])));
|
||||
parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; }));
|
||||
parser2.addOscHandler(1, new OscHandler(data => { oscCustom2.push([1, data]); return false; }));
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(osc).eql([], 'Should not fallback to original handler');
|
||||
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
|
||||
@@ -1298,9 +1409,9 @@ describe('EscapeSequenceParser', function (): void {
|
||||
it('Multiple custom handlers no fallback', () => {
|
||||
const oscCustom: [number, string][] = [];
|
||||
const oscCustom2: [number, string][] = [];
|
||||
parser2.setOscHandler(1, data => osc.push([1, data]));
|
||||
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
|
||||
parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return true; });
|
||||
parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data])));
|
||||
parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; }));
|
||||
parser2.addOscHandler(1, new OscHandler(data => { oscCustom2.push([1, data]); return true; }));
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(osc).eql([], 'Should not fallback to original handler');
|
||||
chai.expect(oscCustom).eql([], 'Should not fallback once');
|
||||
@@ -1308,16 +1419,16 @@ describe('EscapeSequenceParser', function (): void {
|
||||
});
|
||||
it('Execution order should go from latest handler down to the original', () => {
|
||||
const order: number[] = [];
|
||||
parser2.setOscHandler(1, () => order.push(1));
|
||||
parser2.addOscHandler(1, () => { order.push(2); return false; });
|
||||
parser2.addOscHandler(1, () => { order.push(3); return false; });
|
||||
parser2.setOscHandler(1, new OscHandler(() => order.push(1)));
|
||||
parser2.addOscHandler(1, new OscHandler(() => { order.push(2); return false; }));
|
||||
parser2.addOscHandler(1, new OscHandler(() => { order.push(3); return false; }));
|
||||
parse(parser2, '\x1b]1;foo=bar\x1b\\');
|
||||
chai.expect(order).eql([3, 2, 1]);
|
||||
});
|
||||
it('Dispose should work', () => {
|
||||
const oscCustom: [number, string][] = [];
|
||||
parser2.setOscHandler(1, data => osc.push([1, data]));
|
||||
const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
|
||||
parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data])));
|
||||
const customHandler = parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; }));
|
||||
customHandler.dispose();
|
||||
parse(parser2, INPUT);
|
||||
chai.expect(osc).eql([[1, 'foo=bar']]);
|
||||
@@ -1325,8 +1436,8 @@ describe('EscapeSequenceParser', function (): void {
|
||||
});
|
||||
it('Should not corrupt the parser when dispose is called twice', () => {
|
||||
const oscCustom: [number, string][] = [];
|
||||
parser2.setOscHandler(1, data => osc.push([1, data]));
|
||||
const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
|
||||
parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data])));
|
||||
const customHandler = parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; }));
|
||||
customHandler.dispose();
|
||||
customHandler.dispose();
|
||||
parse(parser2, INPUT);
|
||||
@@ -1335,9 +1446,9 @@ describe('EscapeSequenceParser', function (): void {
|
||||
});
|
||||
});
|
||||
it('DCS handler', function (): void {
|
||||
parser2.setDcsHandler('+p', {
|
||||
hook: function (collect: string, params: IParams, flag: number): void {
|
||||
dcs.push(['hook', collect, params.toArray(), flag]);
|
||||
parser2.setDcsHandler({intermediates: '+', final: 'p'}, {
|
||||
hook: function (params: IParams): void {
|
||||
dcs.push(['hook', '', params.toArray(), 0]);
|
||||
},
|
||||
put: function (data: Uint32Array, start: number, end: number): void {
|
||||
let s = '';
|
||||
@@ -1353,17 +1464,75 @@ describe('EscapeSequenceParser', function (): void {
|
||||
parse(parser2, '\x1bP1;2;3+pabc');
|
||||
parse(parser2, ';de\x9c');
|
||||
chai.expect(dcs).eql([
|
||||
['hook', '+', [1, 2, 3], 'p'.charCodeAt(0)],
|
||||
['hook', '', [1, 2, 3], 0],
|
||||
['put', 'abc'], ['put', ';de'],
|
||||
['unhook']
|
||||
]);
|
||||
parser2.clearDcsHandler('+p');
|
||||
parser2.clearDcsHandler('+p'); // should not throw
|
||||
parser2.clearDcsHandler({intermediates: '+', final: 'p'});
|
||||
parser2.clearDcsHandler({intermediates: '+', final: 'p'}); // should not throw
|
||||
clearAccu();
|
||||
parse(parser2, '\x1bP1;2;3+pabc');
|
||||
parse(parser2, ';de\x9c');
|
||||
chai.expect(dcs).eql([]);
|
||||
});
|
||||
describe('DCS custom handlers', () => {
|
||||
const DCS_INPUT = '\x1bP1;2;3+pabc\x1b\\';
|
||||
it('Prevent fallback', () => {
|
||||
const dcsCustom: [string, (number | number[])[], string][] = [];
|
||||
parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data])));
|
||||
parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
parse(parser2, DCS_INPUT);
|
||||
chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc']]);
|
||||
});
|
||||
it('Allow fallback', () => {
|
||||
const dcsCustom: [string, (number | number[])[], string][] = [];
|
||||
parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data])));
|
||||
parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; }));
|
||||
parse(parser2, DCS_INPUT);
|
||||
chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc'], ['A', [1, 2, 3], 'abc']]);
|
||||
});
|
||||
it('Multiple custom handlers fallback once', () => {
|
||||
const dcsCustom: [string, (number | number[])[], string][] = [];
|
||||
parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data])));
|
||||
parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; }));
|
||||
parse(parser2, DCS_INPUT);
|
||||
chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc'], ['B', [1, 2, 3], 'abc']]);
|
||||
});
|
||||
it('Multiple custom handlers no fallback', () => {
|
||||
const dcsCustom: [string, (number | number[])[], string][] = [];
|
||||
parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data])));
|
||||
parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; }));
|
||||
parse(parser2, DCS_INPUT);
|
||||
chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc']]);
|
||||
});
|
||||
it('Execution order should go from latest handler down to the original', () => {
|
||||
const order: number[] = [];
|
||||
parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => order.push(1)));
|
||||
parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(2); return false; }));
|
||||
parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(3); return false; }));
|
||||
parse(parser2, DCS_INPUT);
|
||||
chai.expect(order).eql([3, 2, 1]);
|
||||
});
|
||||
it('Dispose should work', () => {
|
||||
const dcsCustom: [string, (number | number[])[], string][] = [];
|
||||
parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data])));
|
||||
const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
dispo.dispose();
|
||||
parse(parser2, DCS_INPUT);
|
||||
chai.expect(dcsCustom).eql([['A', [1, 2, 3], 'abc']]);
|
||||
});
|
||||
it('Should not corrupt the parser when dispose is called twice', () => {
|
||||
const dcsCustom: [string, (number | number[])[], string][] = [];
|
||||
parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data])));
|
||||
const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
|
||||
dispo.dispose();
|
||||
dispo.dispose();
|
||||
parse(parser2, DCS_INPUT);
|
||||
chai.expect(dcsCustom).eql([['A', [1, 2, 3], 'abc']]);
|
||||
});
|
||||
});
|
||||
it('ERROR handler', function (): void {
|
||||
let errorState: IParsingState | null = null;
|
||||
parser2.setErrorHandler(function (state: IParsingState): IParsingState {
|
||||
@@ -1375,8 +1544,7 @@ describe('EscapeSequenceParser', function (): void {
|
||||
position: 6,
|
||||
code: '€'.charCodeAt(0),
|
||||
currentState: ParserState.CSI_PARAM,
|
||||
osc: '',
|
||||
collect: '',
|
||||
collect: 0,
|
||||
params: Params.fromArray([1, 2, 0]), // extra zero here
|
||||
abort: false
|
||||
});
|
||||
@@ -1387,5 +1555,110 @@ describe('EscapeSequenceParser', function (): void {
|
||||
chai.expect(errorState).eql(null);
|
||||
});
|
||||
});
|
||||
describe('function identifiers', () => {
|
||||
describe('registration limits', () => {
|
||||
it('prefix range 0x3c .. 0x3f, one byte', () => {
|
||||
for (let i = 0x3c; i <= 0x3f; ++i) {
|
||||
const c = String.fromCharCode(i);
|
||||
chai.expect(parser.identToString(parser.identifier({prefix: c, final: 'z'}))).eql(c + 'z');
|
||||
}
|
||||
chai.assert.throws(() => { parser.identifier({prefix: '\x3b', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f');
|
||||
chai.assert.throws(() => { parser.identifier({prefix: '\x40', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f');
|
||||
chai.assert.throws(() => { parser.identifier({prefix: '??', final: 'z'}); }, 'only one byte as prefix supported');
|
||||
});
|
||||
it('intermediates range 0x20 .. 0x2f, up to two bytes', () => {
|
||||
for (let i = 0x20; i <= 0x2f; ++i) {
|
||||
const c = String.fromCharCode(i);
|
||||
chai.expect(parser.identToString(parser.identifier({intermediates: c + c, final: 'z'}))).eql(c + c + 'z');
|
||||
}
|
||||
chai.assert.throws(() => { parser.identifier({intermediates: '\x1f', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f');
|
||||
chai.assert.throws(() => { parser.identifier({intermediates: '\x30', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f');
|
||||
chai.assert.throws(() => { parser.identifier({intermediates: '!!!', final: 'z'}); }, 'only two bytes as intermediates are supported');
|
||||
});
|
||||
it('final CSI/DCS range 0x40 .. 0x7e (default), one byte', () => {
|
||||
for (let i = 0x40; i <= 0x7e; ++i) {
|
||||
const c = String.fromCharCode(i);
|
||||
chai.expect(parser.identToString(parser.identifier({final: c}))).eql(c);
|
||||
}
|
||||
chai.assert.throws(() => { parser.identifier({final: '\x3f'}); }, 'final must be in range 64 .. 126');
|
||||
chai.assert.throws(() => { parser.identifier({final: '\x7f'}); }, 'final must be in range 64 .. 126');
|
||||
chai.assert.throws(() => { parser.identifier({final: 'zz'}); }, 'final must be a single byte');
|
||||
});
|
||||
it('final ESC range 0x30 .. 0x7e, one byte', () => {
|
||||
for (let i = 0x30; i <= 0x7e; ++i) {
|
||||
const c = String.fromCharCode(i);
|
||||
let handler: IDisposable | undefined;
|
||||
chai.assert.doesNotThrow(() => { handler = parser.addEscHandler({final: c}, () => {}); }, 'final must be in range 48 .. 126');
|
||||
if (handler) handler.dispose();
|
||||
}
|
||||
chai.assert.throws(() => { parser.addEscHandler({final: '\x2f'}, () => {}); }, 'final must be in range 48 .. 126');
|
||||
chai.assert.throws(() => { parser.addEscHandler({final: '\x7f'}, () => {}); }, 'final must be in range 48 .. 126');
|
||||
});
|
||||
it('id calculation - should stacking prefix -> intermediate -> final', () => {
|
||||
chai.expect(parser.identToString(parser.identifier({final: 'z'}))).eql('z');
|
||||
chai.expect(parser.identToString(parser.identifier({prefix: '?', final: 'z'}))).eql('?z');
|
||||
chai.expect(parser.identToString(parser.identifier({intermediates: '!', final: 'z'}))).eql('!z');
|
||||
chai.expect(parser.identToString(parser.identifier({prefix: '?', intermediates: '!', final: 'z'}))).eql('?!z');
|
||||
chai.expect(parser.identToString(parser.identifier({prefix: '?', intermediates: '!!', final: 'z'}))).eql('?!!z');
|
||||
});
|
||||
});
|
||||
describe('identifier invocation', () => {
|
||||
it('ESC', () => {
|
||||
const callstack: string[] = [];
|
||||
const h1 = parser.addEscHandler({final: 'z'}, () => { callstack.push('z'); });
|
||||
const h2 = parser.addEscHandler({intermediates: '!', final: 'z'}, () => { callstack.push('!z'); });
|
||||
const h3 = parser.addEscHandler({intermediates: '!!', final: 'z'}, () => { callstack.push('!!z'); });
|
||||
parse(parser, '\x1bz\x1b!z\x1b!!z');
|
||||
h1.dispose();
|
||||
h2.dispose();
|
||||
h3.dispose();
|
||||
parse(parser, '\x1bz\x1b!z\x1b!!z');
|
||||
chai.expect(callstack).eql(['z', '!z', '!!z']);
|
||||
});
|
||||
it('CSI', () => {
|
||||
const callstack: any[] = [];
|
||||
const h1 = parser.addCsiHandler({final: 'z'}, params => { callstack.push(['z', params.toArray()]); });
|
||||
const h2 = parser.addCsiHandler({intermediates: '!', final: 'z'}, params => { callstack.push(['!z', params.toArray()]); });
|
||||
const h3 = parser.addCsiHandler({intermediates: '!!', final: 'z'}, params => { callstack.push(['!!z', params.toArray()]); });
|
||||
const h4 = parser.addCsiHandler({prefix: '?', final: 'z'}, params => { callstack.push(['?z', params.toArray()]); });
|
||||
const h5 = parser.addCsiHandler({prefix: '?', intermediates: '!', final: 'z'}, params => { callstack.push(['?!z', params.toArray()]); });
|
||||
const h6 = parser.addCsiHandler({prefix: '?', intermediates: '!!', final: 'z'}, params => { callstack.push(['?!!z', params.toArray()]); });
|
||||
parse(parser, '\x1b[1;z\x1b[1;!z\x1b[1;!!z\x1b[?1;z\x1b[?1;!z\x1b[?1;!!z');
|
||||
h1.dispose();
|
||||
h2.dispose();
|
||||
h3.dispose();
|
||||
h4.dispose();
|
||||
h5.dispose();
|
||||
h6.dispose();
|
||||
parse(parser, '\x1b[1;z\x1b[1;!z\x1b[1;!!z\x1b[?1;z\x1b[?1;!z\x1b[?1;!!z');
|
||||
chai.expect(callstack).eql([['z', [1, 0]], ['!z', [1, 0]], ['!!z', [1, 0]], ['?z', [1, 0]], ['?!z', [1, 0]], ['?!!z', [1, 0]]]);
|
||||
});
|
||||
it('DCS', () => {
|
||||
const callstack: any[] = [];
|
||||
const h1 = parser.addDcsHandler({final: 'z'}, new DcsHandler((data, params) => { callstack.push(['z', params.toArray(), data]); }));
|
||||
const h2 = parser.addDcsHandler({intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!z', params.toArray(), data]); }));
|
||||
const h3 = parser.addDcsHandler({intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!!z', params.toArray(), data]); }));
|
||||
const h4 = parser.addDcsHandler({prefix: '?', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?z', params.toArray(), data]); }));
|
||||
const h5 = parser.addDcsHandler({prefix: '?', intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!z', params.toArray(), data]); }));
|
||||
const h6 = parser.addDcsHandler({prefix: '?', intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!!z', params.toArray(), data]); }));
|
||||
parse(parser, '\x1bP1;zAB\x1b\\\x1bP1;!zAB\x1b\\\x1bP1;!!zAB\x1b\\\x1bP?1;zAB\x1b\\\x1bP?1;!zAB\x1b\\\x1bP?1;!!zAB\x1b\\');
|
||||
h1.dispose();
|
||||
h2.dispose();
|
||||
h3.dispose();
|
||||
h4.dispose();
|
||||
h5.dispose();
|
||||
h6.dispose();
|
||||
parse(parser, '\x1bP1;zAB\x1b\\\x1bP1;!zAB\x1b\\\x1bP1;!!zAB\x1b\\\x1bP?1;zAB\x1b\\\x1bP?1;!zAB\x1b\\\x1bP?1;!!zAB\x1b\\');
|
||||
chai.expect(callstack).eql([
|
||||
['z', [1, 0], 'AB'],
|
||||
['!z', [1, 0], 'AB'],
|
||||
['!!z', [1, 0], 'AB'],
|
||||
['?z', [1, 0], 'AB'],
|
||||
['?!z', [1, 0], 'AB'],
|
||||
['?!!z', [1, 0], 'AB']
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
// TODO: error conditions and error recovery (not implemented yet in parser)
|
||||
});
|
||||
|
||||
@@ -3,20 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams } from 'common/parser/Types';
|
||||
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType } from 'common/parser/Types';
|
||||
import { ParserState, ParserAction } from 'common/parser/Constants';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { utf32ToString } from 'common/input/TextDecoder';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { Params } from 'common/parser/Params';
|
||||
|
||||
interface IHandlerCollection<T> {
|
||||
[key: string]: T[];
|
||||
}
|
||||
|
||||
type CsiHandler = (params: IParams, collect: string) => boolean | void;
|
||||
type OscHandler = (data: string) => boolean | void;
|
||||
import { OscParser } from 'common/parser/OscParser';
|
||||
import { DcsParser } from 'common/parser/DcsParser';
|
||||
|
||||
/**
|
||||
* Table values are generated like this:
|
||||
@@ -193,7 +187,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
|
||||
table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
|
||||
table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);
|
||||
table.addMany([0x1b, 0x9c], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);
|
||||
table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);
|
||||
// special handling of unicode chars
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
|
||||
@@ -203,14 +197,6 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
return table;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Dummy DCS handler as default fallback.
|
||||
*/
|
||||
class DcsDummy implements IDcsHandler {
|
||||
hook(collect: string, params: IParams, flag: number): void { }
|
||||
put(data: Uint32Array, start: number, end: number): void { }
|
||||
unhook(): void { }
|
||||
}
|
||||
|
||||
/**
|
||||
* EscapeSequenceParser.
|
||||
@@ -220,17 +206,25 @@ class DcsDummy implements IDcsHandler {
|
||||
* To implement custom ANSI compliant escape sequences it is not needed to
|
||||
* alter this parser, instead consider registering a custom handler.
|
||||
* For non ANSI compliant sequences change the transition table with
|
||||
* the optional `transitions` contructor argument and
|
||||
* the optional `transitions` constructor argument and
|
||||
* reimplement the `parse` method.
|
||||
*
|
||||
* This parser is currently hardcoded to operate in ZDM (Zero Default Mode)
|
||||
* as suggested by the original parser, thus empty parameters are set to 0.
|
||||
* This this is not in line with the latest ECMA specification
|
||||
* This this is not in line with the latest ECMA-48 specification
|
||||
* (ZDM was part of the early specs and got completely removed later on).
|
||||
*
|
||||
* Other than the original parser from vt100.net this parser supports
|
||||
* sub parameters in digital parameters separated by colons. Empty sub parameters
|
||||
* are set to -1.
|
||||
* are set to -1 (no ZDM for sub parameters).
|
||||
*
|
||||
* About prefix and intermediate bytes:
|
||||
* This parser follows the assumptions of the vt100.net parser with these restrictions:
|
||||
* - only one prefix byte is allowed as first parameter byte, byte range 0x3c .. 0x3f
|
||||
* - max. two intermediates are respected, byte range 0x20 .. 0x2f
|
||||
* Note that this is not in line with ECMA-48 which does not limit either of those.
|
||||
* Furthermore ECMA-48 allows the prefix byte range at any param byte position. Currently
|
||||
* there are no known sequences that follow the broader definition of the specification.
|
||||
*
|
||||
* TODO: implement error recovery hook via error handler return values
|
||||
*/
|
||||
@@ -240,27 +234,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
public precedingCodepoint: number;
|
||||
|
||||
// buffers over several parse calls
|
||||
protected _osc: string;
|
||||
protected _params: Params;
|
||||
protected _collect: string;
|
||||
protected _collect: number;
|
||||
|
||||
// handler lookup containers
|
||||
protected _printHandler: (data: Uint32Array, start: number, end: number) => void;
|
||||
protected _executeHandlers: any;
|
||||
protected _csiHandlers: IHandlerCollection<CsiHandler>;
|
||||
protected _escHandlers: any;
|
||||
protected _oscHandlers: IHandlerCollection<OscHandler>;
|
||||
protected _dcsHandlers: any;
|
||||
protected _activeDcsHandler: IDcsHandler | null;
|
||||
protected _printHandler: PrintHandlerType;
|
||||
protected _executeHandlers: {[flag: number]: ExecuteHandlerType};
|
||||
protected _csiHandlers: IHandlerCollection<CsiHandlerType>;
|
||||
protected _escHandlers: IHandlerCollection<EscHandlerType>;
|
||||
protected _oscParser: IOscParser;
|
||||
protected _dcsParser: IDcsParser;
|
||||
protected _errorHandler: (state: IParsingState) => IParsingState;
|
||||
|
||||
// fallback handlers
|
||||
protected _printHandlerFb: (data: Uint32Array, start: number, end: number) => void;
|
||||
protected _executeHandlerFb: (code: number) => void;
|
||||
protected _csiHandlerFb: (collect: string, params: IParams, flag: number) => void;
|
||||
protected _escHandlerFb: (collect: string, flag: number) => void;
|
||||
protected _oscHandlerFb: (identifier: number, data: string) => void;
|
||||
protected _dcsHandlerFb: IDcsHandler;
|
||||
protected _printHandlerFb: PrintFallbackHandlerType;
|
||||
protected _executeHandlerFb: ExecuteFallbackHandlerType;
|
||||
protected _csiHandlerFb: CsiFallbackHandlerType;
|
||||
protected _escHandlerFb: EscFallbackHandlerType;
|
||||
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
|
||||
|
||||
constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) {
|
||||
@@ -268,145 +258,197 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
|
||||
this.initialState = ParserState.GROUND;
|
||||
this.currentState = this.initialState;
|
||||
this._osc = '';
|
||||
this._params = new Params(); // defaults to 32 storable params/subparams
|
||||
this._params.addParam(0); // ZDM
|
||||
this._collect = '';
|
||||
this._collect = 0;
|
||||
this.precedingCodepoint = 0;
|
||||
|
||||
// set default fallback handlers and handler lookup containers
|
||||
this._printHandlerFb = (data, start, end): void => { };
|
||||
this._executeHandlerFb = (code: number): void => { };
|
||||
this._csiHandlerFb = (collect: string, params: IParams, flag: number): void => { };
|
||||
this._escHandlerFb = (collect: string, flag: number): void => { };
|
||||
this._oscHandlerFb = (identifier: number, data: string): void => { };
|
||||
this._dcsHandlerFb = new DcsDummy();
|
||||
this._csiHandlerFb = (ident: number, params: IParams): void => { };
|
||||
this._escHandlerFb = (ident: number): void => { };
|
||||
this._errorHandlerFb = (state: IParsingState): IParsingState => state;
|
||||
this._printHandler = this._printHandlerFb;
|
||||
this._executeHandlers = Object.create(null);
|
||||
this._csiHandlers = Object.create(null);
|
||||
this._escHandlers = Object.create(null);
|
||||
this._oscHandlers = Object.create(null);
|
||||
this._dcsHandlers = Object.create(null);
|
||||
this._activeDcsHandler = null;
|
||||
this._oscParser = new OscParser();
|
||||
this._dcsParser = new DcsParser();
|
||||
this._errorHandler = this._errorHandlerFb;
|
||||
|
||||
// swallow 7bit ST (ESC+\)
|
||||
this.setEscHandler('\\', () => {});
|
||||
this.setEscHandler({final: '\\'}, () => {});
|
||||
}
|
||||
|
||||
protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): 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 (finalRange[0] > finalCode || finalCode > finalRange[1]) {
|
||||
throw new Error(`final must be in range ${finalRange[0]} .. ${finalRange[1]}`);
|
||||
}
|
||||
res <<= 8;
|
||||
res |= finalCode;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public identToString(ident: number): string {
|
||||
const res: string[] = [];
|
||||
while (ident) {
|
||||
res.push(String.fromCharCode(ident & 0xFF));
|
||||
ident >>= 8;
|
||||
}
|
||||
return res.reverse().join('');
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._executeHandlers = null;
|
||||
this._escHandlers = null;
|
||||
this._dcsHandlers = null;
|
||||
this._activeDcsHandler = null;
|
||||
this._csiHandlers = Object.create(null);
|
||||
this._executeHandlers = Object.create(null);
|
||||
this._escHandlers = Object.create(null);
|
||||
this._oscParser.dispose();
|
||||
this._dcsParser.dispose();
|
||||
}
|
||||
|
||||
setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void {
|
||||
this._printHandler = callback;
|
||||
public setPrintHandler(handler: PrintHandlerType): void {
|
||||
this._printHandler = handler;
|
||||
}
|
||||
clearPrintHandler(): void {
|
||||
public clearPrintHandler(): void {
|
||||
this._printHandler = this._printHandlerFb;
|
||||
}
|
||||
|
||||
setExecuteHandler(flag: string, callback: () => void): void {
|
||||
this._executeHandlers[flag.charCodeAt(0)] = callback;
|
||||
}
|
||||
clearExecuteHandler(flag: string): void {
|
||||
if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)];
|
||||
}
|
||||
setExecuteHandlerFallback(callback: (code: number) => void): void {
|
||||
this._executeHandlerFb = callback;
|
||||
}
|
||||
|
||||
addCsiHandler(flag: string, callback: CsiHandler): IDisposable {
|
||||
const index = flag.charCodeAt(0);
|
||||
if (this._csiHandlers[index] === undefined) {
|
||||
this._csiHandlers[index] = [];
|
||||
public addEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable {
|
||||
const ident = this._identifier(id, [0x30, 0x7e]);
|
||||
if (this._escHandlers[ident] === undefined) {
|
||||
this._escHandlers[ident] = [];
|
||||
}
|
||||
const handlerList = this._csiHandlers[index];
|
||||
handlerList.push(callback);
|
||||
const handlerList = this._escHandlers[ident];
|
||||
handlerList.push(handler);
|
||||
return {
|
||||
dispose: () => {
|
||||
const handlerIndex = handlerList.indexOf(callback);
|
||||
const handlerIndex = handlerList.indexOf(handler);
|
||||
if (handlerIndex !== -1) {
|
||||
handlerList.splice(handlerIndex, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void {
|
||||
this._csiHandlers[flag.charCodeAt(0)] = [callback];
|
||||
public setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void {
|
||||
this._escHandlers[this._identifier(id, [0x30, 0x7e])] = [handler];
|
||||
}
|
||||
clearCsiHandler(flag: string): void {
|
||||
if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)];
|
||||
public clearEscHandler(id: IFunctionIdentifier): void {
|
||||
if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])];
|
||||
}
|
||||
setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void {
|
||||
public setEscHandlerFallback(handler: EscFallbackHandlerType): void {
|
||||
this._escHandlerFb = handler;
|
||||
}
|
||||
|
||||
public setExecuteHandler(flag: string, handler: ExecuteHandlerType): void {
|
||||
this._executeHandlers[flag.charCodeAt(0)] = handler;
|
||||
}
|
||||
public clearExecuteHandler(flag: string): void {
|
||||
if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)];
|
||||
}
|
||||
public setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void {
|
||||
this._executeHandlerFb = handler;
|
||||
}
|
||||
|
||||
public addCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable {
|
||||
const ident = this._identifier(id);
|
||||
if (this._csiHandlers[ident] === undefined) {
|
||||
this._csiHandlers[ident] = [];
|
||||
}
|
||||
const handlerList = this._csiHandlers[ident];
|
||||
handlerList.push(handler);
|
||||
return {
|
||||
dispose: () => {
|
||||
const handlerIndex = handlerList.indexOf(handler);
|
||||
if (handlerIndex !== -1) {
|
||||
handlerList.splice(handlerIndex, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
public setCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): void {
|
||||
this._csiHandlers[this._identifier(id)] = [handler];
|
||||
}
|
||||
public clearCsiHandler(id: IFunctionIdentifier): void {
|
||||
if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)];
|
||||
}
|
||||
public setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void {
|
||||
this._csiHandlerFb = callback;
|
||||
}
|
||||
|
||||
setEscHandler(collectAndFlag: string, callback: () => void): void {
|
||||
this._escHandlers[collectAndFlag] = callback;
|
||||
public addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable {
|
||||
return this._dcsParser.addHandler(this._identifier(id), handler);
|
||||
}
|
||||
clearEscHandler(collectAndFlag: string): void {
|
||||
if (this._escHandlers[collectAndFlag]) delete this._escHandlers[collectAndFlag];
|
||||
public setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void {
|
||||
this._dcsParser.setHandler(this._identifier(id), handler);
|
||||
}
|
||||
setEscHandlerFallback(callback: (collect: string, flag: number) => void): void {
|
||||
this._escHandlerFb = callback;
|
||||
public clearDcsHandler(id: IFunctionIdentifier): void {
|
||||
this._dcsParser.clearHandler(this._identifier(id));
|
||||
}
|
||||
public setDcsHandlerFallback(handler: DcsFallbackHandlerType): void {
|
||||
this._dcsParser.setHandlerFallback(handler);
|
||||
}
|
||||
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
if (this._oscHandlers[ident] === undefined) {
|
||||
this._oscHandlers[ident] = [];
|
||||
}
|
||||
const handlerList = this._oscHandlers[ident];
|
||||
handlerList.push(callback);
|
||||
return {
|
||||
dispose: () => {
|
||||
const handlerIndex = handlerList.indexOf(callback);
|
||||
if (handlerIndex !== -1) {
|
||||
handlerList.splice(handlerIndex, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
public addOscHandler(ident: number, handler: IOscHandler): IDisposable {
|
||||
return this._oscParser.addHandler(ident, handler);
|
||||
}
|
||||
setOscHandler(ident: number, callback: (data: string) => void): void {
|
||||
this._oscHandlers[ident] = [callback];
|
||||
public setOscHandler(ident: number, handler: IOscHandler): void {
|
||||
this._oscParser.setHandler(ident, handler);
|
||||
}
|
||||
clearOscHandler(ident: number): void {
|
||||
if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
|
||||
public clearOscHandler(ident: number): void {
|
||||
this._oscParser.clearHandler(ident);
|
||||
}
|
||||
setOscHandlerFallback(callback: (identifier: number, data: string) => void): void {
|
||||
this._oscHandlerFb = callback;
|
||||
public setOscHandlerFallback(handler: OscFallbackHandlerType): void {
|
||||
this._oscParser.setHandlerFallback(handler);
|
||||
}
|
||||
|
||||
setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void {
|
||||
this._dcsHandlers[collectAndFlag] = handler;
|
||||
}
|
||||
clearDcsHandler(collectAndFlag: string): void {
|
||||
if (this._dcsHandlers[collectAndFlag]) delete this._dcsHandlers[collectAndFlag];
|
||||
}
|
||||
setDcsHandlerFallback(handler: IDcsHandler): void {
|
||||
this._dcsHandlerFb = handler;
|
||||
}
|
||||
|
||||
setErrorHandler(callback: (state: IParsingState) => IParsingState): void {
|
||||
public setErrorHandler(callback: (state: IParsingState) => IParsingState): void {
|
||||
this._errorHandler = callback;
|
||||
}
|
||||
clearErrorHandler(): void {
|
||||
public clearErrorHandler(): void {
|
||||
this._errorHandler = this._errorHandlerFb;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
public reset(): void {
|
||||
this.currentState = this.initialState;
|
||||
this._osc = '';
|
||||
this._oscParser.reset();
|
||||
this._dcsParser.reset();
|
||||
this._params.reset();
|
||||
this._params.addParam(0); // ZDM
|
||||
this._collect = '';
|
||||
this._activeDcsHandler = null;
|
||||
this._collect = 0;
|
||||
this.precedingCodepoint = 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Parse UTF32 codepoints in `data` up to `length`.
|
||||
*
|
||||
@@ -421,16 +463,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
* - OSC_STRING:OSC_PUT
|
||||
* - DCS_PASSTHROUGH:DCS_PUT
|
||||
*/
|
||||
parse(data: Uint32Array, length: number): void {
|
||||
public parse(data: Uint32Array, length: number): void {
|
||||
let code = 0;
|
||||
let transition = 0;
|
||||
let currentState = this.currentState;
|
||||
let osc = this._osc;
|
||||
const osc = this._oscParser;
|
||||
const dcs = this._dcsParser;
|
||||
let collect = this._collect;
|
||||
const params = this._params;
|
||||
const table: Uint8Array = this.TRANSITIONS.table;
|
||||
let dcsHandler: IDcsHandler | null = this._activeDcsHandler;
|
||||
let callback: Function | null = null;
|
||||
|
||||
// process input string
|
||||
for (let i = 0; i < length; ++i) {
|
||||
@@ -466,8 +507,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
}
|
||||
break;
|
||||
case ParserAction.EXECUTE:
|
||||
callback = this._executeHandlers[code];
|
||||
if (callback) callback();
|
||||
if (this._executeHandlers[code]) this._executeHandlers[code]();
|
||||
else this._executeHandlerFb(code);
|
||||
this.precedingCodepoint = 0;
|
||||
break;
|
||||
@@ -479,7 +519,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
position: i,
|
||||
code,
|
||||
currentState,
|
||||
osc,
|
||||
collect,
|
||||
params,
|
||||
abort: false
|
||||
@@ -489,142 +528,107 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
break;
|
||||
case ParserAction.CSI_DISPATCH:
|
||||
// Trigger CSI Handler
|
||||
const handlers = this._csiHandlers[code];
|
||||
const handlers = this._csiHandlers[collect << 8 | code];
|
||||
let j = handlers ? handlers.length - 1 : -1;
|
||||
for (; j >= 0; j--) {
|
||||
// undefined or true means success and to stop bubbling
|
||||
if (handlers[j](params, collect) !== false) {
|
||||
if (handlers[j](params) !== false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j < 0) {
|
||||
this._csiHandlerFb(collect, params, code);
|
||||
this._csiHandlerFb(collect << 8 | code, params);
|
||||
}
|
||||
this.precedingCodepoint = 0;
|
||||
break;
|
||||
case ParserAction.PARAM:
|
||||
// inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)
|
||||
let isSub = false;
|
||||
do {
|
||||
switch (code) {
|
||||
case 0x3b:
|
||||
params.addParam(0); // ZDM
|
||||
isSub = false;
|
||||
break;
|
||||
case 0x3a:
|
||||
params.addSubParam(-1);
|
||||
isSub = true;
|
||||
break;
|
||||
default: // 0x30 - 0x39
|
||||
if (isSub) params.addSubParamDigit(code - 48);
|
||||
else params.addParamDigit(code - 48);
|
||||
params.addDigit(code - 48);
|
||||
}
|
||||
} while (++i < length && (code = data[i]) > 0x2f && code < 0x3c);
|
||||
i--;
|
||||
break;
|
||||
case ParserAction.COLLECT:
|
||||
collect += String.fromCharCode(code);
|
||||
collect <<= 8;
|
||||
collect |= code;
|
||||
break;
|
||||
case ParserAction.ESC_DISPATCH:
|
||||
callback = this._escHandlers[collect + String.fromCharCode(code)];
|
||||
if (callback) callback(collect, code);
|
||||
else this._escHandlerFb(collect, code);
|
||||
const handlersEsc = this._escHandlers[collect << 8 | code];
|
||||
let jj = handlersEsc ? handlersEsc.length - 1 : -1;
|
||||
for (; jj >= 0; jj--) {
|
||||
// undefined or true means success and to stop bubbling
|
||||
if (handlersEsc[jj]() !== false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (jj < 0) {
|
||||
this._escHandlerFb(collect << 8 | code);
|
||||
}
|
||||
this.precedingCodepoint = 0;
|
||||
break;
|
||||
case ParserAction.CLEAR:
|
||||
osc = '';
|
||||
params.reset();
|
||||
params.addParam(0); // ZDM
|
||||
collect = '';
|
||||
collect = 0;
|
||||
break;
|
||||
case ParserAction.DCS_HOOK:
|
||||
dcsHandler = this._dcsHandlers[collect + String.fromCharCode(code)];
|
||||
if (!dcsHandler) dcsHandler = this._dcsHandlerFb;
|
||||
dcsHandler.hook(collect, params, code);
|
||||
dcs.hook(collect << 8 | code, params);
|
||||
break;
|
||||
case ParserAction.DCS_PUT:
|
||||
// inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f
|
||||
// unhook triggered by: 0x1b, 0x9c
|
||||
// unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort)
|
||||
for (let j = i + 1; ; ++j) {
|
||||
if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
|
||||
if (dcsHandler) {
|
||||
dcsHandler.put(data, i, j);
|
||||
}
|
||||
dcs.put(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ParserAction.DCS_UNHOOK:
|
||||
if (dcsHandler) {
|
||||
dcsHandler.unhook();
|
||||
dcsHandler = null;
|
||||
}
|
||||
dcs.unhook(code !== 0x18 && code !== 0x1a);
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
osc = '';
|
||||
params.reset();
|
||||
params.addParam(0); // ZDM
|
||||
collect = '';
|
||||
collect = 0;
|
||||
this.precedingCodepoint = 0;
|
||||
break;
|
||||
case ParserAction.OSC_START:
|
||||
osc = '';
|
||||
osc.start();
|
||||
break;
|
||||
case ParserAction.OSC_PUT:
|
||||
// inner loop: 0x20 (SP) included, 0x7F (DEL) included
|
||||
for (let j = i + 1; ; j++) {
|
||||
if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code <= 0x9f)) {
|
||||
osc += utf32ToString(data, i, j);
|
||||
osc.put(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ParserAction.OSC_END:
|
||||
if (osc && code !== 0x18 && code !== 0x1a) {
|
||||
// NOTE: OSC subparsing is not part of the original parser
|
||||
// we do basic identifier parsing here to offer a jump table for OSC as well
|
||||
const idx = osc.indexOf(';');
|
||||
if (idx === -1) {
|
||||
this._oscHandlerFb(-1, osc); // this is an error (malformed OSC)
|
||||
} else {
|
||||
// Note: NaN is not handled here
|
||||
// either catch it with the fallback handler
|
||||
// or with an explicit NaN OSC handler
|
||||
const identifier = parseInt(osc.substring(0, idx));
|
||||
const content = osc.substring(idx + 1);
|
||||
// Trigger OSC Handler
|
||||
const handlers = this._oscHandlers[identifier];
|
||||
let j = handlers ? handlers.length - 1 : -1;
|
||||
for (; j >= 0; j--) {
|
||||
// undefined or true means success and to stop bubbling
|
||||
if (handlers[j](content) !== false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j < 0) {
|
||||
this._oscHandlerFb(identifier, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
osc.end(code !== 0x18 && code !== 0x1a);
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
osc = '';
|
||||
params.reset();
|
||||
params.addParam(0); // ZDM
|
||||
collect = '';
|
||||
collect = 0;
|
||||
this.precedingCodepoint = 0;
|
||||
break;
|
||||
}
|
||||
currentState = transition & TableAccess.TRANSITION_STATE_MASK;
|
||||
}
|
||||
|
||||
// save non pushable buffers
|
||||
this._osc = osc;
|
||||
// save collected intermediates
|
||||
this._collect = collect;
|
||||
this._params = params;
|
||||
|
||||
// save active dcs handler reference
|
||||
this._activeDcsHandler = dcsHandler;
|
||||
|
||||
// save state
|
||||
this.currentState = currentState;
|
||||
|
||||
@@ -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, []);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -16,27 +16,29 @@ class TestParams extends Params {
|
||||
}
|
||||
|
||||
/** `Params` parser shim */
|
||||
function parse(params: Params, s: string): void {
|
||||
function parse(params: Params, s: string | string[]): void {
|
||||
params.reset();
|
||||
params.addParam(0);
|
||||
let isSub = false;
|
||||
for (let i = 0; i < s.length; ++i) {
|
||||
let code = s.charCodeAt(i);
|
||||
do {
|
||||
switch (code) {
|
||||
case 0x3b:
|
||||
params.addParam(0);
|
||||
isSub = false;
|
||||
break;
|
||||
case 0x3a:
|
||||
params.addSubParam(-1);
|
||||
isSub = true;
|
||||
break;
|
||||
default: // 0x30 - 0x39
|
||||
if (isSub) params.addSubParamDigit(code - 48);
|
||||
else params.addParamDigit(code - 48);
|
||||
}
|
||||
} while (++i < s.length && (code = s.charCodeAt(i)) > 0x2f && code < 0x3c);
|
||||
if (typeof s === 'string') {
|
||||
s = [s];
|
||||
}
|
||||
for (const chunk of s) {
|
||||
for (let i = 0; i < chunk.length; ++i) {
|
||||
let code = chunk.charCodeAt(i);
|
||||
do {
|
||||
switch (code) {
|
||||
case 0x3b:
|
||||
params.addParam(0);
|
||||
break;
|
||||
case 0x3a:
|
||||
params.addSubParam(-1);
|
||||
break;
|
||||
default: // 0x30 - 0x39
|
||||
params.addDigit(code - 48);
|
||||
}
|
||||
} while (++i < s.length && (code = chunk.charCodeAt(i)) > 0x2f && code < 0x3c);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,4 +219,19 @@ describe('Params', () => {
|
||||
assert.deepEqual(params.toArray(), [0, [0x7FFFFFFF]]);
|
||||
});
|
||||
});
|
||||
describe('issue 2389', () => {
|
||||
it('should cancel subdigits if beyond params limit', () => {
|
||||
const params = new Params();
|
||||
parse(params, ';;;;;;;;;10;;;;;;;;;;20;;;;;;;;;;30;31;32;33;34;35::::::::');
|
||||
assert.deepEqual(params.toArray(), [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 10,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 20,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 31, 32]);
|
||||
});
|
||||
it('should carry forward isSub state', () => {
|
||||
const params = new Params();
|
||||
parse(params, ['1:22:33', '44']);
|
||||
assert.deepEqual(params.toArray(), [1, [22, 3344]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+18
-22
@@ -41,6 +41,7 @@ export class Params implements IParams {
|
||||
private _subParamsIdx: Uint16Array;
|
||||
private _rejectDigits: boolean;
|
||||
private _rejectSubDigits: boolean;
|
||||
private _digitIsSub: boolean;
|
||||
|
||||
/**
|
||||
* Create a `Params` type from JS array representation.
|
||||
@@ -79,6 +80,7 @@ export class Params implements IParams {
|
||||
this._subParamsIdx = new Uint16Array(maxLength);
|
||||
this._rejectDigits = false;
|
||||
this._rejectSubDigits = false;
|
||||
this._digitIsSub = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,6 +93,9 @@ export class Params implements IParams {
|
||||
newParams._subParams.set(this._subParams);
|
||||
newParams._subParamsLength = this._subParamsLength;
|
||||
newParams._subParamsIdx.set(this._subParamsIdx);
|
||||
newParams._rejectDigits = this._rejectDigits;
|
||||
newParams._rejectSubDigits = this._rejectSubDigits;
|
||||
newParams._digitIsSub = this._digitIsSub;
|
||||
return newParams;
|
||||
}
|
||||
|
||||
@@ -121,6 +126,7 @@ export class Params implements IParams {
|
||||
this._subParamsLength = 0;
|
||||
this._rejectDigits = false;
|
||||
this._rejectSubDigits = false;
|
||||
this._digitIsSub = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,6 +137,7 @@ export class Params implements IParams {
|
||||
* store up to 30.
|
||||
*/
|
||||
public addParam(value: number): void {
|
||||
this._digitIsSub = false;
|
||||
if (this.length >= this.maxLength) {
|
||||
this._rejectDigits = true;
|
||||
return;
|
||||
@@ -150,10 +157,11 @@ export class Params implements IParams {
|
||||
* sub parameter will be ignored.
|
||||
*/
|
||||
public addSubParam(value: number): void {
|
||||
this._digitIsSub = true;
|
||||
if (!this.length) {
|
||||
return;
|
||||
}
|
||||
if (this._subParamsLength >= this.maxSubParamsLength) {
|
||||
if (this._rejectDigits || this._subParamsLength >= this.maxSubParamsLength) {
|
||||
this._rejectSubDigits = true;
|
||||
return;
|
||||
}
|
||||
@@ -204,30 +212,18 @@ export class Params implements IParams {
|
||||
/**
|
||||
* Add a single digit value to current parameter.
|
||||
* This is used by the parser to account digits on a char by char basis.
|
||||
* Do not use this method directly, consider using `addParam` instead.
|
||||
*/
|
||||
public addParamDigit(value: number): void {
|
||||
if (this._rejectDigits) {
|
||||
public addDigit(value: number): void {
|
||||
let length;
|
||||
if (this._rejectDigits
|
||||
|| !(length = this._digitIsSub ? this._subParamsLength : this.length)
|
||||
|| (this._digitIsSub && this._rejectSubDigits)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const v = this.params[this.length - 1] * 10 + value;
|
||||
this.params[this.length - 1] = v > MAX_VALUE ? MAX_VALUE : v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single digit value to current sub parameter.
|
||||
* This is used by the parser to account digits on a char by char basis.
|
||||
* Do not use this method directly, consider using `addSubParam` instead.
|
||||
*/
|
||||
public addSubParamDigit(value: number): void {
|
||||
if (!this._subParamsLength || this._rejectDigits || this._rejectSubDigits) {
|
||||
return;
|
||||
}
|
||||
if (this._subParams[this._subParamsLength - 1] === -1) {
|
||||
this._subParams[this._subParamsLength - 1] = value;
|
||||
} else {
|
||||
const v = this._subParams[this._subParamsLength - 1] * 10 + value;
|
||||
this._subParams[this._subParamsLength - 1] = v > MAX_VALUE ? MAX_VALUE : v;
|
||||
}
|
||||
const store = this._digitIsSub ? this._subParams : this.params;
|
||||
const cur = store[length - 1];
|
||||
store[length - 1] = ~cur ? Math.min(cur * 10 + value, MAX_VALUE) : value;
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+145
-43
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { CoreMouseService } from 'common/services/CoreMouseService';
|
||||
import { MockCoreService, MockBufferService } from 'common/TestUtils.test';
|
||||
import { assert } from 'chai';
|
||||
import { ICoreMouseEvent, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
|
||||
|
||||
// needed mock services
|
||||
const bufferService = new MockBufferService(300, 100);
|
||||
const coreService = new MockCoreService();
|
||||
|
||||
function toBytes(s: string | undefined): number[] {
|
||||
if (!s) {
|
||||
return [];
|
||||
}
|
||||
const res: number[] = [];
|
||||
for (let i = 0; i < s.length; ++i) {
|
||||
res.push(s.charCodeAt(i));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
describe('CoreMouseService', () => {
|
||||
it('init', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
assert.equal(cms.activeEncoding, 'DEFAULT');
|
||||
assert.equal(cms.activeProtocol, 'NONE');
|
||||
});
|
||||
it('default protocols - NONE, X10, VT200, DRAG, ANY', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
assert.deepEqual(Object.keys((cms as any)._protocols), ['NONE', 'X10', 'VT200', 'DRAG', 'ANY']);
|
||||
});
|
||||
it('default encodings - DEFAULT, UTF8, SGR, URXVT', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'UTF8', 'SGR', 'URXVT']);
|
||||
});
|
||||
it('protocol/encoding setter, reset', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
cms.activeEncoding = 'SGR';
|
||||
cms.activeProtocol = 'ANY';
|
||||
assert.equal(cms.activeEncoding, 'SGR');
|
||||
assert.equal(cms.activeProtocol, 'ANY');
|
||||
cms.reset();
|
||||
assert.equal(cms.activeEncoding, 'DEFAULT');
|
||||
assert.equal(cms.activeProtocol, 'NONE');
|
||||
assert.throws(() => { cms.activeEncoding = 'xyz'; }, 'unknown encoding "xyz"');
|
||||
assert.throws(() => { cms.activeProtocol = 'xyz'; }, 'unknown protocol "xyz"');
|
||||
});
|
||||
it('addEncoding', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
cms.addEncoding('XYZ', (e: ICoreMouseEvent) => '');
|
||||
cms.activeEncoding = 'XYZ';
|
||||
assert.equal(cms.activeEncoding, 'XYZ');
|
||||
});
|
||||
it('addProtocol', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
cms.addProtocol('XYZ', { events: CoreMouseEventType.NONE, restrict: (e: ICoreMouseEvent) => false });
|
||||
cms.activeProtocol = 'XYZ';
|
||||
assert.equal(cms.activeProtocol, 'XYZ');
|
||||
});
|
||||
it('onProtocolChange', () => {
|
||||
const cms = new CoreMouseService(bufferService, coreService);
|
||||
const wantedEvents: CoreMouseEventType[] = [];
|
||||
cms.onProtocolChange(events => wantedEvents.push(events));
|
||||
cms.activeProtocol = 'NONE';
|
||||
assert.deepEqual(wantedEvents, [CoreMouseEventType.NONE]);
|
||||
cms.activeProtocol = 'ANY';
|
||||
assert.deepEqual(wantedEvents, [
|
||||
CoreMouseEventType.NONE,
|
||||
CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE
|
||||
]);
|
||||
});
|
||||
describe('triggerMouseEvent', () => {
|
||||
let cms: CoreMouseService;
|
||||
let reports: string[];
|
||||
beforeEach(() => {
|
||||
cms = new CoreMouseService(bufferService, coreService);
|
||||
reports = [];
|
||||
coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data);
|
||||
});
|
||||
it('NONE', () => {
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
|
||||
});
|
||||
it('X10', () => {
|
||||
cms.activeProtocol = 'X10';
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
|
||||
});
|
||||
it('VT200', () => {
|
||||
cms.activeProtocol = 'VT200';
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
|
||||
});
|
||||
it('DRAG', () => {
|
||||
cms.activeProtocol = 'DRAG';
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
|
||||
});
|
||||
it('ANY', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), true);
|
||||
// should not report in any case
|
||||
// invalid button + action combinations
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.MOVE }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.UP }), false);
|
||||
// invalid coords
|
||||
assert.equal(cms.triggerMouseEvent({ col: -1, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 500, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: -1, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 500, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
|
||||
});
|
||||
describe('coords', () => {
|
||||
it('DEFAULT encoding', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
for (let i = 0; i < bufferService.cols; ++i) {
|
||||
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
// capped at 95
|
||||
if (i < 95) {
|
||||
assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]);
|
||||
} else {
|
||||
assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, 0x7f, 0x21]);
|
||||
}
|
||||
}
|
||||
});
|
||||
it('UTF8 encoding', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
cms.activeEncoding = 'UTF8';
|
||||
for (let i = 0; i < bufferService.cols; ++i) {
|
||||
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]);
|
||||
}
|
||||
});
|
||||
it('SGR encoding', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
cms.activeEncoding = 'SGR';
|
||||
for (let i = 0; i < bufferService.cols; ++i) {
|
||||
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.deepEqual(reports.pop(), `\x1b[<0;${i + 1};1M`);
|
||||
}
|
||||
});
|
||||
it('URXVT', () => {
|
||||
cms.activeProtocol = 'ANY';
|
||||
cms.activeEncoding = 'URXVT';
|
||||
for (let i = 0; i < bufferService.cols; ++i) {
|
||||
assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
assert.deepEqual(reports.pop(), `\x1b[32;${i + 1};1M`);
|
||||
}
|
||||
});
|
||||
});
|
||||
it('eventCodes with modifiers (DEFAULT encoding)', () => {
|
||||
// TODO: implement AUX button tests
|
||||
cms.activeProtocol = 'ANY';
|
||||
cms.activeEncoding = 'DEFAULT';
|
||||
// all buttons + down + no modifer
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.deepEqual(reports, ['\x1b[M !!', '\x1b[M!!!', '\x1b[M"!!', '\x1b[Ma!!']);
|
||||
while (reports.pop()) { }
|
||||
|
||||
// all buttons + up + no modifier
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.deepEqual(reports, ['\x1b[M#!!', '\x1b[M#!!', '\x1b[M#!!', '\x1b[M`!!']);
|
||||
while (reports.pop()) { }
|
||||
|
||||
// all buttons + move + no modifier
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
|
||||
assert.deepEqual(reports, ['\x1b[M@!!', '\x1b[MA!!', '\x1b[MB!!', '\x1b[MC!!']);
|
||||
while (reports.pop()) { }
|
||||
|
||||
// button none + move + modifiers
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: false, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: true }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: false }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: true }), true);
|
||||
assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: true }), true);
|
||||
assert.deepEqual(reports, ['\x1b[MS!!', '\x1b[MK!!', '\x1b[MG!!', '\x1b[M[!!', '\x1b[MO!!', '\x1b[M_!!']);
|
||||
while (reports.pop()) { }
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,315 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
|
||||
|
||||
/**
|
||||
* Supported default protocols.
|
||||
*/
|
||||
const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = {
|
||||
/**
|
||||
* NONE
|
||||
* Events: none
|
||||
* Modifiers: none
|
||||
*/
|
||||
NONE: {
|
||||
events: CoreMouseEventType.NONE,
|
||||
restrict: () => false
|
||||
},
|
||||
/**
|
||||
* X10
|
||||
* Events: mousedown
|
||||
* Modifiers: none
|
||||
*/
|
||||
X10: {
|
||||
events: CoreMouseEventType.DOWN,
|
||||
restrict: (e: ICoreMouseEvent) => {
|
||||
// no wheel, no move, no up
|
||||
if (e.button === CoreMouseButton.WHEEL || e.action !== CoreMouseAction.DOWN) {
|
||||
return false;
|
||||
}
|
||||
// no modifiers
|
||||
e.ctrl = false;
|
||||
e.alt = false;
|
||||
e.shift = false;
|
||||
return true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* VT200
|
||||
* Events: mousedown / mouseup / wheel
|
||||
* Modifiers: all
|
||||
*/
|
||||
VT200: {
|
||||
events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL,
|
||||
restrict: (e: ICoreMouseEvent) => {
|
||||
// no move
|
||||
if (e.action === CoreMouseAction.MOVE) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* DRAG
|
||||
* Events: mousedown / mouseup / wheel / mousedrag
|
||||
* Modifiers: all
|
||||
*/
|
||||
DRAG: {
|
||||
events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG,
|
||||
restrict: (e: ICoreMouseEvent) => {
|
||||
// no move without button
|
||||
if (e.action === CoreMouseAction.MOVE && e.button === CoreMouseButton.NONE) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* ANY
|
||||
* Events: all mouse related events
|
||||
* Modifiers: all
|
||||
*/
|
||||
ANY: {
|
||||
events:
|
||||
CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL
|
||||
| CoreMouseEventType.DRAG | CoreMouseEventType.MOVE,
|
||||
restrict: (e: ICoreMouseEvent) => true
|
||||
}
|
||||
};
|
||||
|
||||
const enum Modifiers {
|
||||
SHIFT = 4,
|
||||
ALT = 8,
|
||||
CTRL = 16
|
||||
}
|
||||
|
||||
// helper for default encoders to generate the event code.
|
||||
function eventCode(e: ICoreMouseEvent, isSGR: boolean): number {
|
||||
let code = (e.ctrl ? Modifiers.CTRL : 0) | (e.shift ? Modifiers.SHIFT : 0) | (e.alt ? Modifiers.ALT : 0);
|
||||
if (e.button === CoreMouseButton.WHEEL) {
|
||||
code |= 64;
|
||||
code |= e.action;
|
||||
} else {
|
||||
code |= e.button & 3;
|
||||
if (e.button & 4) {
|
||||
code |= 64;
|
||||
}
|
||||
if (e.button & 8) {
|
||||
code |= 128;
|
||||
}
|
||||
if (e.action === CoreMouseAction.MOVE) {
|
||||
code |= CoreMouseAction.MOVE;
|
||||
} else if (e.action === CoreMouseAction.UP && !isSGR) {
|
||||
// special case - only SGR can report button on release
|
||||
// all others have to go with NONE
|
||||
code |= CoreMouseButton.NONE;
|
||||
}
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
const S = String.fromCharCode;
|
||||
|
||||
/**
|
||||
* Supported default encodings.
|
||||
*/
|
||||
const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = {
|
||||
/**
|
||||
* DEFAULT - CSI M Pb Px Py
|
||||
* Single byte encoding for coords and event code.
|
||||
* Can encode values up to 223. The Encoding of higher
|
||||
* values is not UTF-8 compatible (and currently limited
|
||||
* to 95 in xterm.js).
|
||||
*/
|
||||
DEFAULT: (e: ICoreMouseEvent) => {
|
||||
let params = [eventCode(e, false) + 32, e.col + 32, e.row + 32];
|
||||
// FIXME: we are currently limited to ASCII range
|
||||
params = params.map(v => (v > 127) ? 127 : v);
|
||||
// FIXED: params = params.map(v => (v > 255) ? 0 : value);
|
||||
return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`;
|
||||
},
|
||||
/**
|
||||
* UTF8 - CSI M Pb Px Py
|
||||
* Same as DEFAULT, but with optional 2-byte UTF8
|
||||
* encoding for values > 223 (can encode up to 2015).
|
||||
*/
|
||||
UTF8: (e: ICoreMouseEvent) => {
|
||||
let params = [eventCode(e, false) + 32, e.col + 32, e.row + 32];
|
||||
// limit to 2-byte UTF8
|
||||
params = params.map(v => (v > 2047) ? 0 : v);
|
||||
return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`;
|
||||
},
|
||||
/**
|
||||
* SGR - CSI < Pb ; Px ; Py M|m
|
||||
* No encoding limitation.
|
||||
* Can report button on release and works with a well formed sequence.
|
||||
*/
|
||||
SGR: (e: ICoreMouseEvent) => {
|
||||
const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';
|
||||
return `\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`;
|
||||
},
|
||||
/**
|
||||
* URXVT - CSI Pb ; Px ; Py M
|
||||
* Same button encoding as default, decimal encoding for coords.
|
||||
* Ambiguity with other sequences, should not be used.
|
||||
*/
|
||||
URXVT: (e: ICoreMouseEvent) => {
|
||||
return `\x1b[${eventCode(e, false) + 32};${e.col};${e.row}M`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* CoreMouseService
|
||||
*
|
||||
* Provides mouse tracking reports with different protocols and encodings.
|
||||
* - protocols: NONE (default), X10, VT200, DRAG, ANY
|
||||
* - encodings: DEFAULT, SGR, UTF8, URXVT
|
||||
*
|
||||
* Custom protocols/encodings can be added by `addProtocol` / `addEncoding`.
|
||||
* To activate a protocol/encoding, set `activeProtocol` / `activeEncoding`.
|
||||
* Switching a protocol will send a notification event `onProtocolChange`
|
||||
* with a list of needed events to track.
|
||||
*
|
||||
* The service handles the mouse tracking state and decides whether to send
|
||||
* a tracking report to the backend based on protocol and encoding limitations.
|
||||
* To send a mouse event call `triggerMouseEvent`.
|
||||
*/
|
||||
export class CoreMouseService implements ICoreMouseService {
|
||||
private _protocols: {[name: string]: ICoreMouseProtocol} = {};
|
||||
private _encodings: {[name: string]: CoreMouseEncoding} = {};
|
||||
private _activeProtocol: string = '';
|
||||
private _activeEncoding: string = '';
|
||||
private _onProtocolChange = new EventEmitter<CoreMouseEventType>();
|
||||
private _lastEvent: ICoreMouseEvent | null = null;
|
||||
|
||||
constructor(
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ICoreService private readonly _coreService: ICoreService
|
||||
) {
|
||||
// register default protocols and encodings
|
||||
Object.keys(DEFAULT_PROTOCOLS).forEach(name => this.addProtocol(name, DEFAULT_PROTOCOLS[name]));
|
||||
Object.keys(DEFAULT_ENCODINGS).forEach(name => this.addEncoding(name, DEFAULT_ENCODINGS[name]));
|
||||
// call reset to set defaults
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public addProtocol(name: string, protocol: ICoreMouseProtocol): void {
|
||||
this._protocols[name] = protocol;
|
||||
}
|
||||
|
||||
public addEncoding(name: string, encoding: CoreMouseEncoding): void {
|
||||
this._encodings[name] = encoding;
|
||||
}
|
||||
|
||||
public get activeProtocol(): string {
|
||||
return this._activeProtocol;
|
||||
}
|
||||
|
||||
public set activeProtocol(name: string) {
|
||||
if (!this._protocols[name]) {
|
||||
throw new Error(`unknown protocol "${name}"`);
|
||||
}
|
||||
this._activeProtocol = name;
|
||||
this._onProtocolChange.fire(this._protocols[name].events);
|
||||
}
|
||||
|
||||
public get activeEncoding(): string {
|
||||
return this._activeEncoding;
|
||||
}
|
||||
|
||||
public set activeEncoding(name: string) {
|
||||
if (!this._encodings[name]) {
|
||||
throw new Error(`unknown encoding "${name}"`);
|
||||
}
|
||||
this._activeEncoding = name;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this.activeProtocol = 'NONE';
|
||||
this.activeEncoding = 'DEFAULT';
|
||||
this._lastEvent = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event to announce changes in mouse tracking.
|
||||
*/
|
||||
public get onProtocolChange(): IEvent<CoreMouseEventType> {
|
||||
return this._onProtocolChange.event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers a mouse event to be sent.
|
||||
*
|
||||
* Returns true if the event passed all protocol restrictions and a report
|
||||
* was sent, otherwise false. The return value may be used to decide whether
|
||||
* the default event action in the bowser component should be omitted.
|
||||
*
|
||||
* Note: The method will change values of the given event object
|
||||
* to fullfill protocol and encoding restrictions.
|
||||
*/
|
||||
public triggerMouseEvent(e: ICoreMouseEvent): boolean {
|
||||
// range check for col/row
|
||||
if (e.col < 0 || e.col >= this._bufferService.cols
|
||||
|| e.row < 0 || e.row >= this._bufferService.rows) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// filter nonsense combinations of button + action
|
||||
if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) {
|
||||
return false;
|
||||
}
|
||||
if (e.button === CoreMouseButton.NONE && e.action !== CoreMouseAction.MOVE) {
|
||||
return false;
|
||||
}
|
||||
if (e.button !== CoreMouseButton.WHEEL && (e.action === CoreMouseAction.LEFT || e.action === CoreMouseAction.RIGHT)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// report 1-based coords
|
||||
e.col++;
|
||||
e.row++;
|
||||
|
||||
// debounce move at grid level
|
||||
if (e.action === CoreMouseAction.MOVE && this._lastEvent && this._compareEvents(this._lastEvent, e)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// apply protocol restrictions
|
||||
if (!this._protocols[this._activeProtocol].restrict(e)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// encode report and send
|
||||
const report = this._encodings[this._activeEncoding](e);
|
||||
this._coreService.triggerDataEvent(report, true);
|
||||
|
||||
this._lastEvent = e;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} {
|
||||
return {
|
||||
DOWN: !!(events & CoreMouseEventType.DOWN),
|
||||
UP: !!(events & CoreMouseEventType.UP),
|
||||
DRAG: !!(events & CoreMouseEventType.DRAG),
|
||||
MOVE: !!(events & CoreMouseEventType.MOVE),
|
||||
WHEEL: !!(events & CoreMouseEventType.WHEEL)
|
||||
};
|
||||
}
|
||||
|
||||
private _compareEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent): boolean {
|
||||
if (e1.col !== e2.col) return false;
|
||||
if (e1.row !== e2.row) return false;
|
||||
if (e1.button !== e2.button) return false;
|
||||
if (e1.action !== e2.action) return false;
|
||||
if (e1.ctrl !== e2.ctrl) return false;
|
||||
if (e1.alt !== e2.alt) return false;
|
||||
if (e1.shift !== e2.shift) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { IBuffer, IBufferSet } from 'common/buffer/Types';
|
||||
import { IDecPrivateModes } from 'common/Types';
|
||||
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType } from 'common/Types';
|
||||
import { createDecorator } from 'common/services/ServiceRegistry';
|
||||
|
||||
export const IBufferService = createDecorator<IBufferService>('BufferService');
|
||||
@@ -23,6 +23,37 @@ export interface IBufferService {
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
export const ICoreMouseService = createDecorator<ICoreMouseService>('CoreMouseService');
|
||||
export interface ICoreMouseService {
|
||||
activeProtocol: string;
|
||||
activeEncoding: string;
|
||||
addProtocol(name: string, protocol: ICoreMouseProtocol): void;
|
||||
addEncoding(name: string, encoding: CoreMouseEncoding): void;
|
||||
reset(): void;
|
||||
|
||||
/**
|
||||
* Triggers a mouse event to be sent.
|
||||
*
|
||||
* Returns true if the event passed all protocol restrictions and a report
|
||||
* was sent, otherwise false. The return value may be used to decide whether
|
||||
* the default event action in the bowser component should be omitted.
|
||||
*
|
||||
* Note: The method will change values of the given event object
|
||||
* to fullfill protocol and encoding restrictions.
|
||||
*/
|
||||
triggerMouseEvent(event: ICoreMouseEvent): boolean;
|
||||
|
||||
/**
|
||||
* Event to announce changes in mouse tracking.
|
||||
*/
|
||||
onProtocolChange: IEvent<CoreMouseEventType>;
|
||||
|
||||
/**
|
||||
* Human readable version of mouse events.
|
||||
*/
|
||||
explainEvents(events: CoreMouseEventType): {[event: string]: boolean};
|
||||
}
|
||||
|
||||
export const ICoreService = createDecorator<ICoreService>('CoreService');
|
||||
export interface ICoreService {
|
||||
serviceBrand: any;
|
||||
|
||||
Reference in New Issue
Block a user