Merge branch 'master' into feat/fast-scrolling

This commit is contained in:
Daniel Imms
2019-09-07 13:49:42 -07:00
committed by GitHub
43 changed files with 4735 additions and 2094 deletions
+15 -2
View File
@@ -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;
+93
View File
@@ -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;
-233
View File
@@ -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();
// });
// });
});
+1 -1
View File
@@ -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;
+13
View File
@@ -43,3 +43,16 @@ export const enum ParserAction {
DCS_PUT = 13,
DCS_UNHOOK = 14
}
/**
* Internal states of OscParser.
*/
export const enum OscState {
START = 0,
ID = 1,
PAYLOAD = 2,
ABORT = 3
}
// payload limit for OSC and DCS
export const PAYLOAD_LIMIT = 10000000;
+253
View File
@@ -0,0 +1,253 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { DcsParser, DcsHandler } from 'common/parser/DcsParser';
import { IDcsHandler, IParams, IFunctionIdentifier } from 'common/parser/Types';
import { utf32ToString, StringToUtf32 } from 'common/input/TextDecoder';
import { Params } from 'common/parser/Params';
import { PAYLOAD_LIMIT } from 'common/parser/Constants';
function toUtf32(s: string): Uint32Array {
const utf32 = new Uint32Array(s.length);
const decoder = new StringToUtf32();
const length = decoder.decode(s, utf32);
return utf32.subarray(0, length);
}
function identifier(id: IFunctionIdentifier): number {
let res = 0;
if (id.prefix) {
if (id.prefix.length > 1) {
throw new Error('only one byte as prefix supported');
}
res = id.prefix.charCodeAt(0);
if (res && 0x3c > res || res > 0x3f) {
throw new Error('prefix must be in range 0x3c .. 0x3f');
}
}
if (id.intermediates) {
if (id.intermediates.length > 2) {
throw new Error('only two bytes as intermediates are supported');
}
for (let i = 0; i < id.intermediates.length; ++i) {
const intermediate = id.intermediates.charCodeAt(i);
if (0x20 > intermediate || intermediate > 0x2f) {
throw new Error('intermediate must be in range 0x20 .. 0x2f');
}
res <<= 8;
res |= intermediate;
}
}
if (id.final.length !== 1) {
throw new Error('final must be a single byte');
}
const finalCode = id.final.charCodeAt(0);
if (0x40 > finalCode || finalCode > 0x7e) {
throw new Error('final must be in range 0x40 .. 0x7e');
}
res <<= 8;
res |= finalCode;
return res;
}
class TestHandler implements IDcsHandler {
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
hook(params: IParams): void {
this.output.push([this.msg, 'HOOK', params.toArray()]);
}
put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
}
unhook(success: boolean): void | boolean {
this.output.push([this.msg, 'UNHOOK', success]);
if (this.returnFalse) {
return false;
}
}
}
describe('DcsParser', () => {
let parser: DcsParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new DcsParser();
parser.setHandlerFallback((id, action, data) => {
if (action === 'HOOK') {
data = data.toArray();
}
reports.push([id, action, data]);
});
});
describe('handler registration', () => {
it('setDcsHandler', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
// messages from TestHandler
['th', 'HOOK', [1, 2, 3]],
['th', 'PUT', 'Here comes'],
['th', 'PUT', 'the mouse!'],
['th', 'UNHOOK', true]
]);
});
it('clearDcsHandler', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
parser.clearHandler(identifier({intermediates: '+', final: 'p'}));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
// messages from fallback handler
[identifier({intermediates: '+', final: 'p'}), 'HOOK', [1, 2, 3]],
[identifier({intermediates: '+', final: 'p'}), 'PUT', 'Here comes'],
[identifier({intermediates: '+', final: 'p'}), 'PUT', 'the mouse!'],
[identifier({intermediates: '+', final: 'p'}), 'UNHOOK', true]
]);
});
it('addDcsHandler', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2'));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
['th2', 'HOOK', [1, 2, 3]],
['th1', 'HOOK', [1, 2, 3]],
['th2', 'PUT', 'Here comes'],
['th1', 'PUT', 'Here comes'],
['th2', 'PUT', 'the mouse!'],
['th1', 'PUT', 'the mouse!'],
['th2', 'UNHOOK', true],
['th1', 'UNHOOK', false] // false due being already handled by th2!
]);
});
it('addDcsHandler with return false', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
['th2', 'HOOK', [1, 2, 3]],
['th1', 'HOOK', [1, 2, 3]],
['th2', 'PUT', 'Here comes'],
['th1', 'PUT', 'Here comes'],
['th2', 'PUT', 'the mouse!'],
['th1', 'PUT', 'the mouse!'],
['th2', 'UNHOOK', true],
['th1', 'UNHOOK', true] // true since th2 indicated to keep bubbling
]);
});
it('dispose handlers', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
const dispo = parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
dispo.dispose();
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [
['th1', 'HOOK', [1, 2, 3]],
['th1', 'PUT', 'Here comes'],
['th1', 'PUT', 'the mouse!'],
['th1', 'UNHOOK', true]
]);
});
});
describe('DcsHandlerFactory', () => {
it('should be called once on end(true)', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]);
});
it('should not be called on end(false)', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.unhook(false);
assert.deepEqual(reports, []);
});
it('should be disposable', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data])));
const dispo = parser.addHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['two', params.toArray(), data])));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]);
dispo.dispose();
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
data = toUtf32('some other');
parser.put(data, 0, data.length);
data = toUtf32(' data');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]);
});
it('should respect return false', () => {
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data])));
parser.addHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return false; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]);
});
it('should work up to payload limit', function(): void {
this.timeout(10000);
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
const data = toUtf32('A'.repeat(1000));
for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
parser.put(data, 0, data.length);
}
parser.unhook(true);
assert.deepEqual(reports, [[[1, 2, 3], 'A'.repeat(PAYLOAD_LIMIT)]]);
});
it('should abort for payload limit +1', function(): void {
this.timeout(10000);
parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('A'.repeat(1000));
for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
parser.put(data, 0, data.length);
}
data = toUtf32('A');
parser.put(data, 0, data.length);
parser.unhook(true);
assert.deepEqual(reports, []);
});
});
});
+146
View File
@@ -0,0 +1,146 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'common/Types';
import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType } from 'common/parser/Types';
import { utf32ToString } from 'common/input/TextDecoder';
import { Params } from 'common/parser/Params';
import { PAYLOAD_LIMIT } from 'common/parser/Constants';
const EMPTY_HANDLERS: IDcsHandler[] = [];
export class DcsParser implements IDcsParser {
private _handlers: IHandlerCollection<IDcsHandler> = Object.create(null);
private _active: IDcsHandler[] = EMPTY_HANDLERS;
private _ident: number = 0;
private _handlerFb: DcsFallbackHandlerType = () => {};
public dispose(): void {
this._handlers = Object.create(null);
this._handlerFb = () => {};
}
public addHandler(ident: number, handler: IDcsHandler): IDisposable {
if (this._handlers[ident] === undefined) {
this._handlers[ident] = [];
}
const handlerList = this._handlers[ident];
handlerList.push(handler);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(handler);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
public setHandler(ident: number, handler: IDcsHandler): void {
this._handlers[ident] = [handler];
}
public clearHandler(ident: number): void {
if (this._handlers[ident]) delete this._handlers[ident];
}
public setHandlerFallback(handler: DcsFallbackHandlerType): void {
this._handlerFb = handler;
}
public reset(): void {
if (this._active.length) {
this.unhook(false);
}
this._active = EMPTY_HANDLERS;
this._ident = 0;
}
public hook(ident: number, params: IParams): void {
// always reset leftover handlers
this.reset();
this._ident = ident;
this._active = this._handlers[ident] || EMPTY_HANDLERS;
if (!this._active.length) {
this._handlerFb(this._ident, 'HOOK', params);
} else {
for (let j = this._active.length - 1; j >= 0; j--) {
this._active[j].hook(params);
}
}
}
public put(data: Uint32Array, start: number, end: number): void {
if (!this._active.length) {
this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));
} else {
for (let j = this._active.length - 1; j >= 0; j--) {
this._active[j].put(data, start, end);
}
}
}
public unhook(success: boolean): void {
if (!this._active.length) {
this._handlerFb(this._ident, 'UNHOOK', success);
} else {
let j = this._active.length - 1;
for (; j >= 0; j--) {
if (this._active[j].unhook(success) !== false) {
break;
}
}
j--;
// cleanup left over handlers
for (; j >= 0; j--) {
this._active[j].unhook(false);
}
}
this._active = EMPTY_HANDLERS;
this._ident = 0;
}
}
/**
* Convenient class to create a DCS handler from a single callback function.
* Note: The payload is currently limited to 50 MB (hardcoded).
*/
export class DcsHandler implements IDcsHandler {
private _data = '';
private _params: IParams | undefined;
private _hitLimit: boolean = false;
constructor(private _handler: (data: string, params: IParams) => any) {}
public hook(params: IParams): void {
this._params = params.clone();
this._data = '';
this._hitLimit = false;
}
public put(data: Uint32Array, start: number, end: number): void {
if (this._hitLimit) {
return;
}
this._data += utf32ToString(data, start, end);
if (this._data.length > PAYLOAD_LIMIT) {
this._data = '';
this._hitLimit = true;
}
}
public unhook(success: boolean): any {
let ret;
if (this._hitLimit) {
ret = false;
} else if (success) {
ret = this._handler(this._data, this._params ? this._params : new Params());
}
this._params = undefined;
this._data = '';
this._hitLimit = false;
return ret;
}
}
+388 -115
View File
@@ -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)
});
+191 -187
View File
@@ -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;
+251
View File
@@ -0,0 +1,251 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { OscParser, OscHandler } from 'common/parser/OscParser';
import { StringToUtf32, utf32ToString } from 'common/input/TextDecoder';
import { IOscHandler } from 'common/parser/Types';
import { PAYLOAD_LIMIT } from 'common/parser/Constants';
function toUtf32(s: string): Uint32Array {
const utf32 = new Uint32Array(s.length);
const decoder = new StringToUtf32();
const length = decoder.decode(s, utf32);
return utf32.subarray(0, length);
}
class TestHandler implements IOscHandler {
constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {}
start(): void {
this.output.push([this.msg, this.id, 'START']);
}
put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
}
end(success: boolean): void | boolean {
this.output.push([this.msg, this.id, 'END', success]);
if (this.returnFalse) {
return false;
}
}
}
describe('OscParser', () => {
let parser: OscParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new OscParser();
parser.setHandlerFallback((id, action, data) => {
reports.push([id, action, data]);
});
});
describe('identifier parsing', () => {
it('no report for illegal ids', () => {
const data = toUtf32('hello world!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, []);
});
it('no payload', () => {
parser.start();
let data = toUtf32('12');
parser.put(data, 0, data.length);
data = toUtf32('34');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [[1234, 'START', undefined], [1234, 'END', true]]);
});
it('with payload', () => {
parser.start();
let data = toUtf32('12');
parser.put(data, 0, data.length);
data = toUtf32('34');
parser.put(data, 0, data.length);
data = toUtf32(';h');
parser.put(data, 0, data.length);
data = toUtf32('ello');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
[1234, 'START', undefined],
[1234, 'PUT', 'h'],
[1234, 'PUT', 'ello'],
[1234, 'END', true]
]);
});
});
describe('handler registration', () => {
it('setOscHandler', () => {
parser.setHandler(1234, new TestHandler(1234, reports, 'th'));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
// messages from TestHandler
['th', 1234, 'START'],
['th', 1234, 'PUT', 'Here comes'],
['th', 1234, 'PUT', 'the mouse!'],
['th', 1234, 'END', true]
]);
});
it('clearOscHandler', () => {
parser.setHandler(1234, new TestHandler(1234, reports, 'th'));
parser.clearHandler(1234);
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
// messages from fallback handler
[1234, 'START', undefined],
[1234, 'PUT', 'Here comes'],
[1234, 'PUT', 'the mouse!'],
[1234, 'END', true]
]);
});
it('addOscHandler', () => {
parser.setHandler(1234, new TestHandler(1234, reports, 'th1'));
parser.addHandler(1234, new TestHandler(1234, reports, 'th2'));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
['th2', 1234, 'START'],
['th1', 1234, 'START'],
['th2', 1234, 'PUT', 'Here comes'],
['th1', 1234, 'PUT', 'Here comes'],
['th2', 1234, 'PUT', 'the mouse!'],
['th1', 1234, 'PUT', 'the mouse!'],
['th2', 1234, 'END', true],
['th1', 1234, 'END', false] // false due being already handled by th2!
]);
});
it('addOscHandler with return false', () => {
parser.setHandler(1234, new TestHandler(1234, reports, 'th1'));
parser.addHandler(1234, new TestHandler(1234, reports, 'th2', true));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
['th2', 1234, 'START'],
['th1', 1234, 'START'],
['th2', 1234, 'PUT', 'Here comes'],
['th1', 1234, 'PUT', 'Here comes'],
['th2', 1234, 'PUT', 'the mouse!'],
['th1', 1234, 'PUT', 'the mouse!'],
['th2', 1234, 'END', true],
['th1', 1234, 'END', true] // true since th2 indicated to keep bubbling
]);
});
it('dispose handlers', () => {
parser.setHandler(1234, new TestHandler(1234, reports, 'th1'));
const dispo = parser.addHandler(1234, new TestHandler(1234, reports, 'th2', true));
dispo.dispose();
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [
['th1', 1234, 'START'],
['th1', 1234, 'PUT', 'Here comes'],
['th1', 1234, 'PUT', 'the mouse!'],
['th1', 1234, 'END', true]
]);
});
});
describe('OscHandlerFactory', () => {
it('should be called once on end(true)', () => {
parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]);
});
it('should not be called on end(false)', () => {
parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(false);
assert.deepEqual(reports, []);
});
it('should be disposable', () => {
parser.setHandler(1234, new OscHandler(data => reports.push(['one', data])));
const dispo = parser.addHandler(1234, new OscHandler(data => reports.push(['two', data])));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
dispo.dispose();
parser.start();
data = toUtf32('1234;some other');
parser.put(data, 0, data.length);
data = toUtf32(' data');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
});
it('should respect return false', () => {
parser.setHandler(1234, new OscHandler(data => reports.push(['one', data])));
parser.addHandler(1234, new OscHandler(data => { reports.push(['two', data]); return false; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
});
it('should work up to payload limit', function(): void {
this.timeout(10000);
parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
parser.start();
let data = toUtf32('1234;');
parser.put(data, 0, data.length);
data = toUtf32('A'.repeat(1000));
for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
parser.put(data, 0, data.length);
}
parser.end(true);
assert.deepEqual(reports, [[1234, 'A'.repeat(PAYLOAD_LIMIT)]]);
});
it('should abort for payload limit +1', function(): void {
this.timeout(10000);
parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
parser.start();
let data = toUtf32('1234;');
parser.put(data, 0, data.length);
data = toUtf32('A'.repeat(1000));
for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
parser.put(data, 0, data.length);
}
data = toUtf32('A');
parser.put(data, 0, data.length);
parser.end(true);
assert.deepEqual(reports, []);
});
});
});
+203
View File
@@ -0,0 +1,203 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser } from 'common/parser/Types';
import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants';
import { utf32ToString } from 'common/input/TextDecoder';
import { IDisposable } from 'common/Types';
export class OscParser implements IOscParser {
private _state = OscState.START;
private _id = -1;
private _handlers: IHandlerCollection<IOscHandler> = Object.create(null);
private _handlerFb: OscFallbackHandlerType = () => { };
public addHandler(ident: number, handler: IOscHandler): IDisposable {
if (this._handlers[ident] === undefined) {
this._handlers[ident] = [];
}
const handlerList = this._handlers[ident];
handlerList.push(handler);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(handler);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
public setHandler(ident: number, handler: IOscHandler): void {
this._handlers[ident] = [handler];
}
public clearHandler(ident: number): void {
if (this._handlers[ident]) delete this._handlers[ident];
}
public setHandlerFallback(handler: OscFallbackHandlerType): void {
this._handlerFb = handler;
}
public dispose(): void {
this._handlers = Object.create(null);
this._handlerFb = () => {};
}
public reset(): void {
// cleanup handlers if payload was already sent
if (this._state === OscState.PAYLOAD) {
this.end(false);
}
this._id = -1;
this._state = OscState.START;
}
private _start(): void {
const handlers = this._handlers[this._id];
if (!handlers) {
this._handlerFb(this._id, 'START');
} else {
for (let j = handlers.length - 1; j >= 0; j--) {
handlers[j].start();
}
}
}
private _put(data: Uint32Array, start: number, end: number): void {
const handlers = this._handlers[this._id];
if (!handlers) {
this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));
} else {
for (let j = handlers.length - 1; j >= 0; j--) {
handlers[j].put(data, start, end);
}
}
}
private _end(success: boolean): void {
// other than the old code we always have to call .end
// to keep the bubbling we use `success` to indicate
// whether a handler should execute
const handlers = this._handlers[this._id];
if (!handlers) {
this._handlerFb(this._id, 'END', success);
} else {
let j = handlers.length - 1;
for (; j >= 0; j--) {
if (handlers[j].end(success) !== false) {
break;
}
}
j--;
// cleanup left over handlers
for (; j >= 0; j--) {
handlers[j].end(false);
}
}
}
public start(): void {
// always reset leftover handlers
this.reset();
this._id = -1;
this._state = OscState.ID;
}
/**
* Put data to current OSC command.
* Expects the identifier of the OSC command in the form
* OSC id ; payload ST/BEL
* Payload chunks are not further processed and get
* directly passed to the handlers.
*/
public put(data: Uint32Array, start: number, end: number): void {
if (this._state === OscState.ABORT) {
return;
}
if (this._state === OscState.ID) {
while (start < end) {
const code = data[start++];
if (code === 0x3b) {
this._state = OscState.PAYLOAD;
this._start();
break;
}
if (code < 0x30 || 0x39 < code) {
this._state = OscState.ABORT;
return;
}
if (this._id === -1) {
this._id = 0;
}
this._id = this._id * 10 + code - 48;
}
}
if (this._state === OscState.PAYLOAD && end - start > 0) {
this._put(data, start, end);
}
}
/**
* Indicates end of an OSC command.
* Whether the OSC got aborted or finished normally
* is indicated by `success`.
*/
public end(success: boolean): void {
if (this._state === OscState.START) {
return;
}
// do nothing if command was faulty
if (this._state !== OscState.ABORT) {
// if we are still in ID state and get an early end
// means that the command has no payload thus we still have
// to announce START and send END right after
if (this._state === OscState.ID) {
this._start();
}
this._end(success);
}
this._id = -1;
this._state = OscState.START;
}
}
/**
* Convenient class to allow attaching string based handler functions
* as OSC handlers.
*/
export class OscHandler implements IOscHandler {
private _data = '';
private _hitLimit: boolean = false;
constructor(private _handler: (data: string) => any) {}
public start(): void {
this._data = '';
this._hitLimit = false;
}
public put(data: Uint32Array, start: number, end: number): void {
if (this._hitLimit) {
return;
}
this._data += utf32ToString(data, start, end);
if (this._data.length > PAYLOAD_LIMIT) {
this._data = '';
this._hitLimit = true;
}
}
public end(success: boolean): any {
let ret;
if (this._hitLimit) {
ret = false;
} else if (success) {
ret = this._handler(this._data);
}
this._data = '';
this._hitLimit = false;
return ret;
}
}
+36 -19
View File
@@ -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
View File
@@ -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;
}
}
+145 -43
View File
@@ -53,10 +53,8 @@ export interface IParsingState {
code: number;
// current parser state
currentState: ParserState;
// osc string buffer
osc: string;
// collect buffer with intermediate characters
collect: string;
collect: number;
// params buffer
params: IParams;
// should abort (default: false)
@@ -64,31 +62,83 @@ export interface IParsingState {
}
/**
* DCS handler signature for EscapeSequenceParser.
* EscapeSequenceParser handles DCS commands via separate
* subparsers that get hook/unhooked and can handle
* arbitrary amount of data.
*
* On entering a DSC sequence `hook` is called by
* `EscapeSequenceParser`. Use it to initialize or reset
* states needed to handle the current DCS sequence.
* Note: A DCS parser is only instantiated once, therefore
* you cannot rely on the ctor to reinitialize state.
*
* EscapeSequenceParser will call `put` several times if the
* parsed data got split, therefore you might have to collect
* `data` until `unhook` is called.
* Note: `data` is borrowed, if you cannot process the data
* in chunks you have to copy it, doing otherwise will lead to
* data losses or corruption.
*
* `unhook` marks the end of the current DCS sequence.
*/
* Command handler interfaces.
*/
/**
* CSI handler types.
* Note: `params` is borrowed.
*/
export type CsiHandlerType = (params: IParams) => boolean | void;
export type CsiFallbackHandlerType = (ident: number, params: IParams) => void;
/**
* DCS handler types.
*/
export interface IDcsHandler {
hook(collect: string, params: IParams, flag: number): void;
/**
* Called when a DCS command starts.
* Prepare needed data structures here.
* Note: `params` is borrowed.
*/
hook(params: IParams): void;
/**
* Incoming payload chunk.
* Note: `params` is borrowed.
*/
put(data: Uint32Array, start: number, end: number): void;
unhook(): void;
/**
* End of DCS command. `success` indicates whether the
* command finished normally or got aborted, thus final
* execution of the command should depend on `success`.
* To save memory also cleanup data structures here.
*/
unhook(success: boolean): void | boolean;
}
export type DcsFallbackHandlerType = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void;
/**
* ESC handler types.
*/
export type EscHandlerType = () => boolean | void;
export type EscFallbackHandlerType = (identifier: number) => void;
/**
* EXECUTE handler types.
*/
export type ExecuteHandlerType = () => boolean | void;
export type ExecuteFallbackHandlerType = (ident: number) => void;
/**
* OSC handler types.
*/
export interface IOscHandler {
/**
* Announces start of this OSC command.
* Prepare needed data structures here.
*/
start(): void;
/**
* Incoming data chunk.
* Note: Data is borrowed.
*/
put(data: Uint32Array, start: number, end: number): void;
/**
* End of OSC command. `success` indicates whether the
* command finished normally or got aborted, thus final
* execution of the command should depend on `success`.
* To save memory also cleanup data structures here.
*/
end(success: boolean): void | boolean;
}
export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
/**
* PRINT handler types.
*/
export type PrintHandlerType = (data: Uint32Array, start: number, end: number) => void;
export type PrintFallbackHandlerType = PrintHandlerType;
/**
* EscapeSequenceParser interface.
@@ -112,31 +162,83 @@ export interface IEscapeSequenceParser extends IDisposable {
*/
parse(data: Uint32Array, length: number): void;
setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void;
/**
* Get string from numercial function identifier `ident`.
* Useful in fallback handlers which expose the low level
* numcerical function identifier for debugging purposes.
* Note: A full back translation to `IFunctionIdentifier`
* is not implemented.
*/
identToString(ident: number): string;
setPrintHandler(handler: PrintHandlerType): void;
clearPrintHandler(): void;
setExecuteHandler(flag: string, callback: () => void): void;
setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void;
clearEscHandler(id: IFunctionIdentifier): void;
setEscHandlerFallback(handler: EscFallbackHandlerType): void;
addEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable;
setExecuteHandler(flag: string, handler: ExecuteHandlerType): void;
clearExecuteHandler(flag: string): void;
setExecuteHandlerFallback(callback: (code: number) => void): void;
setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void;
setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void;
clearCsiHandler(flag: string): void;
setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void;
addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
setCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): void;
clearCsiHandler(id: IFunctionIdentifier): void;
setCsiHandlerFallback(callback: CsiFallbackHandlerType): void;
addCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable;
setEscHandler(collectAndFlag: string, callback: () => void): void;
clearEscHandler(collectAndFlag: string): void;
setEscHandlerFallback(callback: (collect: string, flag: number) => void): void;
setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void;
clearDcsHandler(id: IFunctionIdentifier): void;
setDcsHandlerFallback(handler: DcsFallbackHandlerType): void;
addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable;
setOscHandler(ident: number, callback: (data: string) => void): void;
setOscHandler(ident: number, handler: IOscHandler): void;
clearOscHandler(ident: number): void;
setOscHandlerFallback(callback: (identifier: number, data: string) => void): void;
setOscHandlerFallback(handler: OscFallbackHandlerType): void;
addOscHandler(ident: number, handler: IOscHandler): IDisposable;
setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void;
clearDcsHandler(collectAndFlag: string): void;
setDcsHandlerFallback(handler: IDcsHandler): void;
setErrorHandler(callback: (state: IParsingState) => IParsingState): void;
setErrorHandler(handler: (state: IParsingState) => IParsingState): void;
clearErrorHandler(): void;
}
/**
* Subparser interfaces.
* The subparsers are instantiated in `EscapeSequenceParser` and
* called during `EscapeSequenceParser.parse`.
*/
export interface ISubParser<T, U> extends IDisposable {
reset(): void;
addHandler(ident: number, handler: T): IDisposable;
setHandler(ident: number, handler: T): void;
clearHandler(ident: number): void;
setHandlerFallback(handler: U): void;
put(data: Uint32Array, start: number, end: number): void;
}
export interface IOscParser extends ISubParser<IOscHandler, OscFallbackHandlerType> {
start(): void;
end(success: boolean): void;
}
export interface IDcsParser extends ISubParser<IDcsHandler, DcsFallbackHandlerType> {
hook(ident: number, params: IParams): void;
unhook(success: boolean): void;
}
/**
* Interface to denote a specific ESC, CSI or DCS handler slot.
* The values are used to create an integer respresentation during handler
* regristation before passed to the subparsers as `ident`.
* The integer translation is made to allow a faster handler access
* in `EscapeSequenceParser.parse`.
*/
export interface IFunctionIdentifier {
prefix?: string;
intermediates?: string;
final: string;
}
export interface IHandlerCollection<T> {
[key: string]: T[];
}
@@ -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()) { }
});
});
});
+315
View File
@@ -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;
}
}
+32 -1
View File
@@ -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;