Merge pull request #2241 from jerch/parser_colon_notation

colon notation for the parser
This commit is contained in:
jerch
2019-07-06 20:04:00 +02:00
committed by GitHub
15 changed files with 1058 additions and 338 deletions
+4 -2
View File
@@ -37,14 +37,16 @@ function startServer() {
});
app.post('/terminals', function (req, res) {
const env = Object.assign({}, process.env);
env['COLORTERM'] = 'truecolor';
var cols = parseInt(req.query.cols),
rows = parseInt(req.query.rows),
term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], {
name: 'xterm-256color',
cols: cols || 80,
rows: rows || 24,
cwd: process.env.PWD,
env: process.env,
cwd: env.PWD,
env: env,
encoding: USE_BINARY_UTF8 ? null : 'utf8'
});
+178 -27
View File
@@ -12,6 +12,7 @@ import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { Attributes } from 'common/buffer/Constants';
import { AttributeData } from 'common/buffer/AttributeData';
import { Params } from 'common/parser/Params';
import { MockCoreService } from 'common/TestUtils.test';
describe('InputHandler', () => {
@@ -23,7 +24,7 @@ describe('InputHandler', () => {
terminal.curAttrData.fg = 3;
const inputHandler = new InputHandler(terminal, new MockCoreService());
// Save cursor position
inputHandler.saveCursor([]);
inputHandler.saveCursor();
assert.equal(terminal.buffer.x, 1);
assert.equal(terminal.buffer.y, 2);
assert.equal(terminal.curAttrData.fg, 3);
@@ -32,7 +33,7 @@ describe('InputHandler', () => {
terminal.buffer.y = 20;
terminal.curAttrData.fg = 30;
// Restore cursor position
inputHandler.restoreCursor([]);
inputHandler.restoreCursor();
assert.equal(terminal.buffer.x, 1);
assert.equal(terminal.buffer.y, 2);
assert.equal(terminal.curAttrData.fg, 3);
@@ -43,37 +44,37 @@ describe('InputHandler', () => {
const inputHandler = new InputHandler(terminal, new MockCoreService());
const collect = ' ';
inputHandler.setCursorStyle([0], collect);
inputHandler.setCursorStyle(Params.fromArray([0]), collect);
assert.equal(terminal.options['cursorStyle'], 'block');
assert.equal(terminal.options['cursorBlink'], true);
terminal.options = {};
inputHandler.setCursorStyle([1], collect);
inputHandler.setCursorStyle(Params.fromArray([1]), collect);
assert.equal(terminal.options['cursorStyle'], 'block');
assert.equal(terminal.options['cursorBlink'], true);
terminal.options = {};
inputHandler.setCursorStyle([2], collect);
inputHandler.setCursorStyle(Params.fromArray([2]), collect);
assert.equal(terminal.options['cursorStyle'], 'block');
assert.equal(terminal.options['cursorBlink'], false);
terminal.options = {};
inputHandler.setCursorStyle([3], collect);
inputHandler.setCursorStyle(Params.fromArray([3]), collect);
assert.equal(terminal.options['cursorStyle'], 'underline');
assert.equal(terminal.options['cursorBlink'], true);
terminal.options = {};
inputHandler.setCursorStyle([4], collect);
inputHandler.setCursorStyle(Params.fromArray([4]), collect);
assert.equal(terminal.options['cursorStyle'], 'underline');
assert.equal(terminal.options['cursorBlink'], false);
terminal.options = {};
inputHandler.setCursorStyle([5], collect);
inputHandler.setCursorStyle(Params.fromArray([5]), collect);
assert.equal(terminal.options['cursorStyle'], 'bar');
assert.equal(terminal.options['cursorBlink'], true);
terminal.options = {};
inputHandler.setCursorStyle([6], collect);
inputHandler.setCursorStyle(Params.fromArray([6]), collect);
assert.equal(terminal.options['cursorStyle'], 'bar');
assert.equal(terminal.options['cursorBlink'], false);
});
@@ -85,10 +86,10 @@ describe('InputHandler', () => {
terminal.bracketedPasteMode = false;
const inputHandler = new InputHandler(terminal, new MockCoreService());
// Set bracketed paste mode
inputHandler.setMode([2004], collect);
inputHandler.setMode(Params.fromArray([2004]), collect);
assert.equal(terminal.bracketedPasteMode, true);
// Reset bracketed paste mode
inputHandler.resetMode([2004], collect);
inputHandler.resetMode(Params.fromArray([2004]), collect);
assert.equal(terminal.bracketedPasteMode, false);
});
});
@@ -114,25 +115,25 @@ describe('InputHandler', () => {
// insert one char from params = [0]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.insertChars([0]);
inputHandler.insertChars(Params.fromArray([0]));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 123456789');
// insert one char from params = [1]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.insertChars([1]);
inputHandler.insertChars(Params.fromArray([1]));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 12345678');
// insert two chars from params = [2]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.insertChars([2]);
inputHandler.insertChars(Params.fromArray([2]));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 123456');
// insert 10 chars from params = [10]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.insertChars([10]);
inputHandler.insertChars(Params.fromArray([10]));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a'));
});
@@ -151,28 +152,28 @@ describe('InputHandler', () => {
// delete one char from params = [0]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.deleteChars([0]);
inputHandler.deleteChars(Params.fromArray([0]));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '234567890 ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '234567890');
// insert one char from params = [1]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.deleteChars([1]);
inputHandler.deleteChars(Params.fromArray([1]));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '34567890 ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '34567890');
// insert two chars from params = [2]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.deleteChars([2]);
inputHandler.deleteChars(Params.fromArray([2]));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '567890 ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '567890');
// insert 10 chars from params = [10]
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.deleteChars([10]);
inputHandler.deleteChars(Params.fromArray([10]));
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' ');
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a'));
});
@@ -188,19 +189,19 @@ describe('InputHandler', () => {
// params[0] - right erase
term.buffer.y = 0;
term.buffer.x = 70;
inputHandler.eraseInLine([0]);
inputHandler.eraseInLine(Params.fromArray([0]));
expect(term.buffer.lines.get(0).translateToString(false)).equals(Array(71).join('a') + ' ');
// params[1] - left erase
term.buffer.y = 1;
term.buffer.x = 70;
inputHandler.eraseInLine([1]);
inputHandler.eraseInLine(Params.fromArray([1]));
expect(term.buffer.lines.get(1).translateToString(false)).equals(Array(71).join(' ') + ' aaaaaaaaa');
// params[1] - left erase
term.buffer.y = 2;
term.buffer.x = 70;
inputHandler.eraseInLine([2]);
inputHandler.eraseInLine(Params.fromArray([2]));
expect(term.buffer.lines.get(2).translateToString(false)).equals(Array(term.cols + 1).join(' '));
});
@@ -214,7 +215,7 @@ describe('InputHandler', () => {
// params [0] - right and below erase
term.buffer.y = 5;
term.buffer.x = 40;
inputHandler.eraseInDisplay([0]);
inputHandler.eraseInDisplay(Params.fromArray([0]));
expect(termContent(term, false)).eql([
Array(term.cols + 1).join('a'),
Array(term.cols + 1).join('a'),
@@ -242,7 +243,7 @@ describe('InputHandler', () => {
// params [1] - left and above
term.buffer.y = 5;
term.buffer.x = 40;
inputHandler.eraseInDisplay([1]);
inputHandler.eraseInDisplay(Params.fromArray([1]));
expect(termContent(term, false)).eql([
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
@@ -270,7 +271,7 @@ describe('InputHandler', () => {
// params [2] - whole screen
term.buffer.y = 5;
term.buffer.x = 40;
inputHandler.eraseInDisplay([2]);
inputHandler.eraseInDisplay(Params.fromArray([2]));
expect(termContent(term, false)).eql([
Array(term.cols + 1).join(' '),
Array(term.cols + 1).join(' '),
@@ -302,7 +303,7 @@ describe('InputHandler', () => {
expect(term.buffer.lines.get(2).isWrapped).true;
term.buffer.y = 2;
term.buffer.x = 40;
inputHandler.eraseInDisplay([1]);
inputHandler.eraseInDisplay(Params.fromArray([1]));
expect(term.buffer.lines.get(2).isWrapped).false;
// reset and add a wrapped line
@@ -317,7 +318,7 @@ describe('InputHandler', () => {
expect(term.buffer.lines.get(2).isWrapped).true;
term.buffer.y = 1;
term.buffer.x = 90; // Cursor is beyond last column
inputHandler.eraseInDisplay([1]);
inputHandler.eraseInDisplay(Params.fromArray([1]));
expect(term.buffer.lines.get(2).isWrapped).false;
});
});
@@ -546,4 +547,154 @@ describe('InputHandler', () => {
assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [5, 0, 0]);
});
});
describe('colon notation', () => {
let termColon: TestTerminal;
let termSemicolon: TestTerminal;
beforeEach(() => {
termColon = new TestTerminal();
termSemicolon = new TestTerminal();
});
describe('should equal to semicolon', () => {
it('CSI 38:2::50:100:150 m', () => {
termColon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.writeSync('\x1b[38;2;50;100;150m');
termColon.writeSync('\x1b[38:2::50:100:150m');
assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
it('CSI 38:2::50:100: m', () => {
termColon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.writeSync('\x1b[38;2;50;100;m');
termColon.writeSync('\x1b[38:2::50:100:m');
assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
it('CSI 38:2::50:: m', () => {
termColon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.writeSync('\x1b[38;2;50;;m');
termColon.writeSync('\x1b[38:2::50::m');
assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 0 << 8 | 0);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
it('CSI 38:2:::: m', () => {
termColon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.writeSync('\x1b[38;2;;;m');
termColon.writeSync('\x1b[38:2::::m');
assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 0 << 16 | 0 << 8 | 0);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
it('CSI 38;2::50:100:150 m', () => {
termColon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.writeSync('\x1b[38;2;50;100;150m');
termColon.writeSync('\x1b[38;2::50:100:150m');
assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
it('CSI 38;2;50:100:150 m', () => {
termColon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.writeSync('\x1b[38;2;50;100;150m');
termColon.writeSync('\x1b[38;2;50:100:150m');
assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
it('CSI 38;2;50;100:150 m', () => {
termColon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.writeSync('\x1b[38;2;50;100;150m');
termColon.writeSync('\x1b[38;2;50;100:150m');
assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
it('CSI 38:5:50 m', () => {
termColon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.writeSync('\x1b[38;5;50m');
termColon.writeSync('\x1b[38:5:50m');
assert.equal(termSemicolon.curAttrData.fg & 0xFF, 50);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
it('CSI 38:5: m', () => {
termColon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.writeSync('\x1b[38;5;m');
termColon.writeSync('\x1b[38:5:m');
assert.equal(termSemicolon.curAttrData.fg & 0xFF, 0);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
it('CSI 38;5:50 m', () => {
termColon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.writeSync('\x1b[38;5;50m');
termColon.writeSync('\x1b[38;5:50m');
assert.equal(termSemicolon.curAttrData.fg & 0xFF, 50);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
});
describe('should fill early sequence end with default of 0', () => {
it('CSI 38:2 m', () => {
termColon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.writeSync('\x1b[38;2m');
termColon.writeSync('\x1b[38:2m');
assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 0 << 16 | 0 << 8 | 0);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
it('CSI 38:5 m', () => {
termColon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.curAttrData.fg = 0xFFFFFFFF;
termSemicolon.writeSync('\x1b[38;5m');
termColon.writeSync('\x1b[38:5m');
assert.equal(termSemicolon.curAttrData.fg & 0xFF, 0);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
});
describe('should not interfere with leading/following SGR attrs', () => {
it('CSI 1 ; 38:2::50:100:150 ; 4 m', () => {
termSemicolon.writeSync('\x1b[1;38;2;50;100;150;4m');
termColon.writeSync('\x1b[1;38:2::50:100:150;4m');
assert.equal(!!termSemicolon.curAttrData.isBold(), true);
assert.equal(!!termSemicolon.curAttrData.isUnderline(), true);
assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
it('CSI 1 ; 38:2::50:100: ; 4 m', () => {
termSemicolon.writeSync('\x1b[1;38;2;50;100;;4m');
termColon.writeSync('\x1b[1;38:2::50:100:;4m');
assert.equal(!!termSemicolon.curAttrData.isBold(), true);
assert.equal(!!termSemicolon.curAttrData.isUnderline(), true);
assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
it('CSI 1 ; 38:2::50:100 ; 4 m', () => {
termSemicolon.writeSync('\x1b[1;38;2;50;100;;4m');
termColon.writeSync('\x1b[1;38:2::50:100;4m');
assert.equal(!!termSemicolon.curAttrData.isBold(), true);
assert.equal(!!termSemicolon.curAttrData.isUnderline(), true);
assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
it('CSI 1 ; 38:2:: ; 4 m', () => {
termSemicolon.writeSync('\x1b[1;38;2;;;;4m');
termColon.writeSync('\x1b[1;38:2::;4m');
assert.equal(!!termSemicolon.curAttrData.isBold(), true);
assert.equal(!!termSemicolon.curAttrData.isUnderline(), true);
assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 0);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
it('CSI 1 ; 38;2:: ; 4 m', () => {
termSemicolon.writeSync('\x1b[1;38;2;;;;4m');
termColon.writeSync('\x1b[1;38;2::;4m');
assert.equal(!!termSemicolon.curAttrData.isBold(), true);
assert.equal(!!termSemicolon.curAttrData.isUnderline(), true);
assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 0);
assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg);
});
});
});
});
+180 -180
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -56,6 +56,7 @@ import { Disposable } from 'common/Lifecycle';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
import { Attributes } from 'common/buffer/Constants';
import { MouseService } from 'browser/services/MouseService';
import { IParams } from 'common/parser/Types';
import { CoreService } from 'common/services/CoreService';
// Let it work inside Node.js for automated testing purposes.
@@ -1413,7 +1414,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
}
/** Add handler for CSI escape sequence. See xterm.d.ts for details. */
public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable {
return this._inputHandler.addCsiHandler(flag, callback);
}
/** Add handler for OSC escape sequence. See xterm.d.ts for details. */
+2 -1
View File
@@ -15,6 +15,7 @@ import { AttributeData } from 'common/buffer/AttributeData';
import { IColorManager, IColorSet } from 'browser/Types';
import { IOptionsService } from 'common/services/Services';
import { EventEmitter } from 'common/EventEmitter';
import { IParams } from 'common/parser/Types';
import { ISelectionService } from 'browser/services/Services';
export class TestTerminal extends Terminal {
@@ -71,7 +72,7 @@ export class MockTerminal implements ITerminal {
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
throw new Error('Method not implemented.');
}
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable {
throw new Error('Method not implemented.');
}
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
+38 -37
View File
@@ -9,6 +9,7 @@ import { IEvent, IEventEmitter } from 'common/EventEmitter';
import { IColorSet } from 'browser/Types';
import { IOptionsService } from 'common/services/Services';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IParams } from 'common/parser/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
@@ -109,42 +110,42 @@ export interface IInputHandler {
/** C0 SO */ shiftOut(): void;
/** C0 SI */ shiftIn(): void;
/** CSI @ */ insertChars(params?: number[]): void;
/** CSI A */ cursorUp(params?: number[]): void;
/** CSI B */ cursorDown(params?: number[]): void;
/** CSI C */ cursorForward(params?: number[]): void;
/** CSI D */ cursorBackward(params?: number[]): void;
/** CSI E */ cursorNextLine(params?: number[]): void;
/** CSI F */ cursorPrecedingLine(params?: number[]): void;
/** CSI G */ cursorCharAbsolute(params?: number[]): void;
/** CSI H */ cursorPosition(params?: number[]): void;
/** CSI I */ cursorForwardTab(params?: number[]): void;
/** CSI J */ eraseInDisplay(params?: number[]): void;
/** CSI K */ eraseInLine(params?: number[]): void;
/** CSI L */ insertLines(params?: number[]): void;
/** CSI M */ deleteLines(params?: number[]): void;
/** CSI P */ deleteChars(params?: number[]): void;
/** CSI S */ scrollUp(params?: number[]): void;
/** CSI T */ scrollDown(params?: number[], collect?: string): void;
/** CSI X */ eraseChars(params?: number[]): void;
/** CSI Z */ cursorBackwardTab(params?: number[]): void;
/** CSI ` */ charPosAbsolute(params?: number[]): void;
/** CSI a */ hPositionRelative(params?: number[]): void;
/** CSI b */ repeatPrecedingCharacter(params?: number[]): void;
/** CSI c */ sendDeviceAttributes(params?: number[], collect?: string): void;
/** CSI d */ linePosAbsolute(params?: number[]): void;
/** CSI e */ vPositionRelative(params?: number[]): void;
/** CSI f */ hVPosition(params?: number[]): void;
/** CSI g */ tabClear(params?: number[]): void;
/** CSI h */ setMode(params?: number[], collect?: string): void;
/** CSI l */ resetMode(params?: number[], collect?: string): void;
/** CSI m */ charAttributes(params?: number[]): void;
/** CSI n */ deviceStatus(params?: number[], collect?: string): void;
/** CSI p */ softReset(params?: number[], collect?: string): void;
/** CSI q */ setCursorStyle(params?: number[], collect?: string): void;
/** CSI r */ setScrollRegion(params?: number[], collect?: string): void;
/** CSI s */ saveCursor(params?: number[]): void;
/** CSI u */ restoreCursor(params?: number[]): void;
/** CSI @ */ insertChars(params: IParams): void;
/** CSI A */ cursorUp(params: IParams): void;
/** CSI B */ cursorDown(params: IParams): void;
/** CSI C */ cursorForward(params: IParams): void;
/** CSI D */ cursorBackward(params: IParams): void;
/** CSI E */ cursorNextLine(params: IParams): void;
/** CSI F */ cursorPrecedingLine(params: IParams): void;
/** CSI G */ cursorCharAbsolute(params: IParams): void;
/** CSI H */ cursorPosition(params: IParams): void;
/** CSI I */ cursorForwardTab(params: IParams): void;
/** CSI J */ eraseInDisplay(params: IParams): void;
/** CSI K */ eraseInLine(params: IParams): void;
/** CSI L */ insertLines(params: IParams): void;
/** CSI M */ deleteLines(params: IParams): void;
/** CSI P */ deleteChars(params: IParams): void;
/** CSI S */ scrollUp(params: IParams): void;
/** CSI T */ scrollDown(params: IParams, collect?: string): void;
/** CSI X */ eraseChars(params: IParams): void;
/** CSI Z */ cursorBackwardTab(params: IParams): void;
/** CSI ` */ charPosAbsolute(params: IParams): void;
/** CSI a */ hPositionRelative(params: IParams): void;
/** CSI b */ repeatPrecedingCharacter(params: IParams): void;
/** CSI c */ sendDeviceAttributes(params: IParams, collect?: string): void;
/** CSI d */ linePosAbsolute(params: IParams): void;
/** CSI e */ vPositionRelative(params: IParams): void;
/** CSI f */ hVPosition(params: IParams): void;
/** CSI g */ tabClear(params: IParams): void;
/** CSI h */ setMode(params: IParams, collect?: string): void;
/** CSI l */ resetMode(params: IParams, collect?: string): void;
/** CSI m */ charAttributes(params: IParams): void;
/** CSI n */ deviceStatus(params: IParams, collect?: string): void;
/** CSI p */ softReset(params: IParams, collect?: string): void;
/** CSI q */ setCursorStyle(params: IParams, collect?: string): void;
/** CSI r */ setScrollRegion(params: IParams, collect?: string): void;
/** CSI s */ saveCursor(params: IParams): void;
/** CSI u */ restoreCursor(params: IParams): void;
/** OSC 0
OSC 2 */ setTitle(data: string): void;
/** ESC E */ nextLine(): void;
@@ -239,7 +240,7 @@ export interface IPublicTerminal extends IDisposable {
writeln(data: string): void;
open(parent: HTMLElement): void;
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): void;
+80 -46
View File
@@ -3,11 +3,13 @@
* @license MIT
*/
import { IDcsHandler, IParsingState } from 'common/parser/Types';
import { IDcsHandler, IParsingState, IParams, ParamsArray } 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 { ParserState } from 'common/parser/Constants';
import { Params } from 'common/parser/Params';
function r(a: number, b: number): string[] {
let c = b - a;
@@ -26,11 +28,14 @@ class TestEscapeSequenceParser extends EscapeSequenceParser {
public set osc(value: string) {
this._osc = value;
}
public get params(): number[] {
return this._params;
public get params(): ParamsArray {
return this._params.toArray();
}
public set params(value: number[]) {
this._params = value;
public set params(value: ParamsArray) {
this._params = Params.fromArray(value);
}
public get realParams(): IParams {
return this._params;
}
public get collect(): string {
return this._collect;
@@ -65,14 +70,14 @@ const testTerminal: any = {
actionExecute: function (flag: string): void {
this.calls.push(['exe', flag]);
},
actionCSI: function (collect: string, params: number[], flag: string): void {
this.calls.push(['csi', collect, params, flag]);
actionCSI: function (collect: string, params: IParams, flag: string): void {
this.calls.push(['csi', collect, params.toArray(), flag]);
},
actionESC: function (collect: string, flag: string): void {
this.calls.push(['esc', collect, flag]);
},
actionDCSHook: function (collect: string, params: number[], flag: string): void {
this.calls.push(['dcs hook', collect, params, flag]);
actionDCSHook: function (collect: string, params: IParams, flag: string): void {
this.calls.push(['dcs hook', collect, params.toArray(), flag]);
},
actionDCSPrint: function (data: Uint32Array, start: number, end: number): void {
let s = '';
@@ -88,7 +93,7 @@ const testTerminal: any = {
// dcs handler to map dcs actions into the test object `testTerminal`
class DcsTest implements IDcsHandler {
hook(collect: string, params: number[], flag: number): void {
hook(collect: string, params: IParams, flag: number): void {
testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag));
}
put(data: Uint32Array, start: number, end: number): void {
@@ -120,7 +125,7 @@ let state: any;
// parser with Uint8Array based transition table
const testParser = new TestEscapeSequenceParser();
testParser.setPrintHandler(testTerminal.print.bind(testTerminal));
testParser.setCsiHandlerFallback((collect: string, params: number[], flag: number) => {
testParser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => {
testTerminal.actionCSI(collect, params, String.fromCharCode(flag));
});
testParser.setEscHandlerFallback((collect: string, flag: number) => {
@@ -562,16 +567,16 @@ describe('EscapeSequenceParser', function (): void {
testTerminal.clear();
}
});
it('trans CSI_ENTRY --> CSI_IGNORE', function (): void {
it('trans CSI_ENTRY --> CSI_PARAM for ":" (0x3a)', function (): void {
parser.reset();
parser.currentState = ParserState.CSI_ENTRY;
parse(parser, '\x3a');
chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE);
chai.expect(parser.currentState).equal(ParserState.CSI_PARAM);
parser.reset();
});
it('trans CSI_PARAM --> CSI_IGNORE', function (): void {
parser.reset();
const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f'];
const chars = ['\x3c', '\x3d', '\x3e', '\x3f'];
for (let i = 0; i < chars.length; ++i) {
parser.currentState = ParserState.CSI_PARAM;
parse(parser, '\x3b' + chars[i]);
@@ -580,6 +585,18 @@ describe('EscapeSequenceParser', function (): void {
parser.reset();
}
});
it('trans CSI_PARAM --> CSI_IGNORE', function (): void {
parser.reset();
const chars = ['\x3c', '\x3d', '\x3e', '\x3f'];
for (let i = 0; i < chars.length; ++i) {
chai.expect(parser.params).eql([0]);
parser.currentState = ParserState.CSI_PARAM;
parse(parser, '\x3b' + chars[i]);
chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE);
chai.expect(parser.params).eql([0, 0]);
parser.reset();
}
});
it('trans CSI_INTERMEDIATE --> CSI_IGNORE', function (): void {
parser.reset();
const chars = r(0x30, 0x40);
@@ -785,16 +802,16 @@ describe('EscapeSequenceParser', function (): void {
chai.expect(parser.params).eql([0, 0]);
parser.reset();
});
it('trans DCS_ENTRY --> DCS_IGNORE', function (): void {
it('trans DCS_ENTRY --> DCS_PARAM for ":" (0x3a)', function (): void {
parser.reset();
parser.currentState = ParserState.DCS_ENTRY;
parse(parser, '\x3a');
chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE);
chai.expect(parser.currentState).equal(ParserState.DCS_PARAM);
parser.reset();
});
it('trans DCS_PARAM --> DCS_IGNORE', function (): void {
parser.reset();
const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f'];
const chars = ['\x3c', '\x3d', '\x3e', '\x3f'];
for (let i = 0; i < chars.length; ++i) {
parser.currentState = ParserState.DCS_PARAM;
parse(parser, '\x3b' + chars[i]);
@@ -1028,6 +1045,23 @@ describe('EscapeSequenceParser', function (): void {
['print', 'defg']
], null);
});
it('colon notation in CSI params', () => {
test('\x1b[<31;5::123:;8mHello World! öäü€\nabc',
[
['csi', '<', [31, 5, [-1, 123, -1], 8], 'm'],
['print', 'Hello World! öäü€'],
['exe', '\n'],
['print', 'abc']
], null);
});
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 put', 'bc;de'],
['dcs unhook']
], null);
});
});
describe('coverage tests', function (): void {
@@ -1078,10 +1112,10 @@ describe('EscapeSequenceParser', function (): void {
let parser2: TestEscapeSequenceParser;
let print = '';
const esc: string[] = [];
const csi: [string, number[], string][] = [];
const csi: [string, ParamsArray, string][] = [];
const exe: string[] = [];
const osc: [number, string][] = [];
const dcs: ([string] | [string, string] | [string, string, number[], number])[] = [];
const dcs: ([string] | [string, string] | [string, string, ParamsArray, number])[] = [];
function clearAccu(): void {
print = '';
esc.length = 0;
@@ -1128,8 +1162,8 @@ describe('EscapeSequenceParser', function (): void {
chai.expect(esc).eql([]);
});
it('CSI handler', function (): void {
parser2.setCsiHandler('m', function (params: number[], collect: string): void {
csi.push(['m', params, collect]);
parser2.setCsiHandler('m', function (params: IParams, collect: string): void {
csi.push(['m', params.toArray(), collect]);
});
parse(parser2, INPUT);
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]);
@@ -1141,38 +1175,38 @@ describe('EscapeSequenceParser', function (): void {
});
describe('CSI custom handlers', () => {
it('Prevent fallback', () => {
const csiCustom: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
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; });
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, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return false; });
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; });
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], '']]);
});
it('Multiple custom handlers fallback once', () => {
const csiCustom: [string, number[], string][] = [];
const csiCustom2: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return false; });
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; });
parse(parser2, INPUT);
chai.expect(csi).eql([], 'Should not fallback to original handler');
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]);
});
it('Multiple custom handlers no fallback', () => {
const csiCustom: [string, number[], string][] = [];
const csiCustom2: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return true; });
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; });
parse(parser2, INPUT);
chai.expect(csi).eql([], 'Should not fallback to original handler');
chai.expect(csiCustom).eql([], 'Should not fallback once');
@@ -1187,18 +1221,18 @@ describe('EscapeSequenceParser', function (): void {
chai.expect(order).eql([3, 2, 1]);
});
it('Dispose should work', () => {
const csiCustom: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
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; });
customHandler.dispose();
parse(parser2, INPUT);
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]);
chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed');
});
it('Should not corrupt the parser when dispose is called twice', () => {
const csiCustom: [string, number[], string][] = [];
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
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; });
customHandler.dispose();
customHandler.dispose();
parse(parser2, INPUT);
@@ -1302,8 +1336,8 @@ describe('EscapeSequenceParser', function (): void {
});
it('DCS handler', function (): void {
parser2.setDcsHandler('+p', {
hook: function (collect: string, params: number[], flag: number): void {
dcs.push(['hook', collect, params, flag]);
hook: function (collect: string, params: IParams, flag: number): void {
dcs.push(['hook', collect, params.toArray(), flag]);
},
put: function (data: Uint32Array, start: number, end: number): void {
let s = '';
@@ -1343,7 +1377,7 @@ describe('EscapeSequenceParser', function (): void {
currentState: ParserState.CSI_PARAM,
osc: '',
collect: '',
params: [1, 2, 0], // extra zero here
params: Params.fromArray([1, 2, 0]), // extra zero here
abort: false
});
parser2.clearErrorHandler();
+56 -32
View File
@@ -3,18 +3,19 @@
* @license MIT
*/
import { IParsingState, IDcsHandler, IEscapeSequenceParser } from 'common/parser/Types';
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams } 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: number[], collect: string) => boolean | void;
type CsiHandler = (params: IParams, collect: string) => boolean | void;
type OscHandler = (data: string) => boolean | void;
/**
@@ -143,17 +144,14 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
// csi entries
table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);
table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND);
table.addMany(r(0x30, 0x3a), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);
table.add(0x3b, ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);
table.addMany(r(0x30, 0x3c), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM);
table.addMany(r(0x30, 0x3a), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);
table.add(0x3b, ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);
table.addMany(r(0x30, 0x3c), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);
table.addMany(r(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND);
table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany(r(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany(r(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND);
table.add(0x3a, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany(r(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
table.addMany(r(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
table.addMany(r(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
@@ -173,9 +171,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
table.addMany(r(0x1c, 0x20), ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
table.add(0x3a, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x30, 0x3a), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);
table.add(0x3b, ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);
table.addMany(r(0x30, 0x3c), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM);
table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
@@ -183,9 +179,8 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
table.addMany(r(0x1c, 0x20), ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
table.addMany(r(0x30, 0x3a), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);
table.add(0x3b, ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);
table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x30, 0x3c), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
@@ -212,7 +207,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
* Dummy DCS handler as default fallback.
*/
class DcsDummy implements IDcsHandler {
hook(collect: string, params: number[], flag: number): void { }
hook(collect: string, params: IParams, flag: number): void { }
put(data: Uint32Array, start: number, end: number): void { }
unhook(): void { }
}
@@ -221,12 +216,22 @@ class DcsDummy implements IDcsHandler {
* EscapeSequenceParser.
* This class implements the ANSI/DEC compatible parser described by
* Paul Williams (https://vt100.net/emu/dec_ansi_parser).
*
* 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
* reimplement the `parse` method.
* NOTE: The parameter element notation is currently not supported.
*
* 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
* (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.
*
* TODO: implement error recovery hook via error handler return values
*/
export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser {
@@ -236,7 +241,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// buffers over several parse calls
protected _osc: string;
protected _params: number[];
protected _params: Params;
protected _collect: string;
// handler lookup containers
@@ -252,7 +257,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// fallback handlers
protected _printHandlerFb: (data: Uint32Array, start: number, end: number) => void;
protected _executeHandlerFb: (code: number) => void;
protected _csiHandlerFb: (collect: string, params: number[], flag: 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;
@@ -264,14 +269,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this.initialState = ParserState.GROUND;
this.currentState = this.initialState;
this._osc = '';
this._params = [0];
this._params = new Params(); // defaults to 32 storable params/subparams
this._params.addParam(0); // ZDM
this._collect = '';
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: number[], flag: 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();
@@ -329,13 +335,13 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}
};
}
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void {
setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void {
this._csiHandlers[flag.charCodeAt(0)] = [callback];
}
clearCsiHandler(flag: string): void {
if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)];
}
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void {
setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void {
this._csiHandlerFb = callback;
}
@@ -394,7 +400,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
reset(): void {
this.currentState = this.initialState;
this._osc = '';
this._params = [0];
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = '';
this._activeDcsHandler = null;
this.precedingCodepoint = 0;
@@ -402,10 +409,12 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
/**
* Parse UTF32 codepoints in `data` up to `length`.
*
* Note: For several actions with high data load the parsing is optimized
* by using local read ahead loops with hardcoded conditions to
* avoid costly table lookups. Make sure that any change of table values
* will be reflected in the loop conditions as well. Affected states/actions:
* will be reflected in the loop conditions as well and vice versa.
* Affected states/actions:
* - GROUND:PRINT
* - CSI_PARAM:PARAM
* - DCS_PARAM:PARAM
@@ -418,7 +427,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
let currentState = this.currentState;
let osc = this._osc;
let collect = this._collect;
let params = this._params;
const params = this._params;
const table: Uint8Array = this.TRANSITIONS.table;
let dcsHandler: IDcsHandler | null = this._activeDcsHandler;
let callback: Function | null = null;
@@ -497,11 +506,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}
break;
case ParserAction.PARAM:
// inner loop: digits (0x30 - 0x39) and ; (0x3b)
// inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)
let isSub = false;
do {
if (code === 0x3b) params.push(0);
else params[params.length - 1] = params[params.length - 1] * 10 + code - 48;
} while (++i < length && (code = data[i]) > 0x2f && (code < 0x3a || code === 0x3b));
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);
}
} while (++i < length && (code = data[i]) > 0x2f && code < 0x3c);
i--;
break;
case ParserAction.COLLECT:
@@ -515,7 +536,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
break;
case ParserAction.CLEAR:
osc = '';
params = [0];
params.reset();
params.addParam(0); // ZDM
collect = '';
break;
case ParserAction.DCS_HOOK:
@@ -544,7 +566,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
osc = '';
params = [0];
params.reset();
params.addParam(0); // ZDM
collect = '';
break;
case ParserAction.OSC_START:
@@ -590,7 +613,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
osc = '';
params = [0];
params.reset();
params.addParam(0); // ZDM
collect = '';
break;
}
+220
View File
@@ -0,0 +1,220 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { Params } from 'common/parser/Params';
import { ParamsArray } from 'common/parser/Types';
class TestParams extends Params {
public get subParams(): Int32Array {
return this._subParams;
}
public get subParamsLength(): number {
return this._subParamsLength;
}
}
/** `Params` parser shim */
function parse(params: Params, s: 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);
}
}
describe('Params', () => {
it('should respect ctor args', () => {
const params = new TestParams(12, 23);
assert.equal(params.params.length, 12);
assert.equal(params.subParams.length, 23);
assert.deepEqual(params.toArray(), []);
});
it('addParam', () => {
const params = new TestParams();
params.addParam(1);
assert.equal(params.length, 1);
assert.deepEqual(Array.prototype.slice.call(params.params, 0, params.length), [1]);
assert.deepEqual(params.toArray(), [1]);
params.addParam(23);
assert.equal(params.length, 2);
assert.deepEqual(Array.prototype.slice.call(params.params, 0, params.length), [1, 23]);
assert.deepEqual(params.toArray(), [1, 23]);
assert.equal(params.subParamsLength, 0);
});
it('addSubParam', () => {
const params = new TestParams();
params.addParam(1);
params.addSubParam(2);
params.addSubParam(3);
assert.equal(params.length, 1);
assert.equal(params.subParamsLength, 2);
assert.deepEqual(params.toArray(), [1, [2, 3]]);
params.addParam(12345);
params.addSubParam(-1);
assert.equal(params.length, 2);
assert.equal(params.subParamsLength, 3);
assert.deepEqual(params.toArray(), [1, [2, 3], 12345, [-1]]);
});
it('should not add sub params without previous param', () => {
const params = new TestParams();
params.addSubParam(2);
params.addSubParam(3);
assert.equal(params.length, 0);
assert.equal(params.subParamsLength, 0);
assert.deepEqual(params.toArray(), []);
params.addParam(1);
params.addSubParam(2);
params.addSubParam(3);
assert.equal(params.length, 1);
assert.equal(params.subParamsLength, 2);
assert.deepEqual(params.toArray(), [1, [2, 3]]);
});
it('reset', () => {
const params = new TestParams();
params.addParam(1);
params.addSubParam(2);
params.addSubParam(3);
params.addParam(12345);
params.addSubParam(-1);
params.reset();
assert.equal(params.length, 0);
assert.equal(params.subParamsLength, 0);
assert.deepEqual(params.toArray(), []);
params.addParam(1);
params.addSubParam(2);
params.addSubParam(3);
params.addParam(12345);
params.addSubParam(-1);
assert.equal(params.length, 2);
assert.equal(params.subParamsLength, 3);
assert.deepEqual(params.toArray(), [1, [2, 3], 12345, [-1]]);
});
it('Params.fromArray --> toArray', () => {
let data: ParamsArray = [];
assert.deepEqual(Params.fromArray(data).toArray(), data);
data = [1, [2, 3], 12345, [-1]];
assert.deepEqual(Params.fromArray(data).toArray(), data);
data = [38, 2, 50, 100, 150];
assert.deepEqual(Params.fromArray(data).toArray(), data);
data = [38, 2, 50, 100, [150]];
assert.deepEqual(Params.fromArray(data).toArray(), data);
data = [38, [2, 50, 100, 150]];
assert.deepEqual(Params.fromArray(data).toArray(), data);
// strip empty sub params
data = [38, [2, 50, 100, 150], 5, [], 6];
assert.deepEqual(Params.fromArray(data).toArray(), [38, [2, 50, 100, 150], 5, 6]);
});
it('clone', () => {
const params = Params.fromArray([38, [2, 50, 100, 150], 5, [], 6, 1, [2, 3], 12345, [-1]]);
assert.deepEqual(params.clone(), params);
});
it('hasSubParams / getSubParams', () => {
const params = Params.fromArray([38, [2, 50, 100, 150], 5, [], 6]);
assert.equal(params.hasSubParams(0), true);
assert.deepEqual(params.getSubParams(0), new Int32Array([2, 50, 100, 150]));
assert.equal(params.hasSubParams(1), false);
assert.deepEqual(params.getSubParams(1), null);
assert.equal(params.hasSubParams(2), false);
assert.deepEqual(params.getSubParams(2), null);
});
it('getSubParamsAll', () => {
const params = Params.fromArray([1, [2, 3], 7, 12345, [-1]]);
assert.deepEqual(params.getSubParamsAll(), {0: new Int32Array([2, 3]), 2: new Int32Array([-1])});
});
describe('parse tests', () => {
it('param defaults to 0 (ZDM - zero default mode)', () => {
const params = new Params();
parse(params, '');
assert.deepEqual(params.toArray(), [0]);
});
it('sub param defaults to -1', () => {
const params = new Params();
parse(params, ':');
assert.deepEqual(params.toArray(), [0, [-1]]);
});
it('should correctly reset on new sequence', () => {
const params = new Params();
parse(params, '1;2;3');
assert.deepEqual(params.toArray(), [1, 2, 3]);
parse(params, '4');
assert.deepEqual(params.toArray(), [4]);
parse(params, '4::123:5;6;7');
assert.deepEqual(params.toArray(), [4, [-1, 123, 5], 6, 7]);
parse(params, '');
assert.deepEqual(params.toArray(), [0]);
});
it('should handle length restrictions correctly', () => {
// restrict to 3 params and 3 sub params
const params = new Params(3, 3);
parse(params, '1;2;3');
assert.deepEqual(params.toArray(), [1, 2, 3]);
parse(params, '4');
assert.deepEqual(params.toArray(), [4]);
parse(params, '4::123:5;6;7');
assert.deepEqual(params.toArray(), [4, [-1, 123, 5], 6, 7]);
parse(params, '');
assert.deepEqual(params.toArray(), [0]);
// overlong params
parse(params, '1;2;3;4;5;6;7');
assert.deepEqual(params.toArray(), [1, 2, 3]);
// overlong sub params
parse(params, '4;38:2::50:100:150;48:5:22');
assert.deepEqual(params.toArray(), [4, 38, [2, -1, 50], 48]);
});
it('typical sequences', () => {
const params = new Params();
// SGR with semicolon syntax
parse(params, '0;4;38;2;50;100;150;48;5;22');
assert.deepEqual(params.toArray(), [0, 4, 38, 2, 50, 100, 150, 48, 5, 22]);
// SGR mixed style (partly wrong)
parse(params, '0;4;38;2;50:100:150;48;5:22');
assert.deepEqual(params.toArray(), [0, 4, 38, 2, 50, [100, 150], 48, 5, [22]]);
// SGR colon style
parse(params, '0;4;38:2::50:100:150;48:5:22');
assert.deepEqual(params.toArray(), [0, 4, 38, [2, -1, 50, 100, 150], 48, [5, 22]]);
});
});
describe('should not overflow to negative', () => {
it('reject params lesser -1', () => {
const params = new Params();
params.addParam(-1);
assert.throws(() => params.addParam(-2), 'values lesser than -1 are not allowed');
});
it('reject subparams lesser -1', () => {
const params = new Params();
params.addParam(-1);
params.addSubParam(-1);
assert.throws(() => params.addSubParam(-2), 'values lesser than -1 are not allowed');
assert.deepEqual(params.toArray(), [-1, [-1]]);
});
it('clamp parsed params', () => {
const params = new Params();
parse(params, '2147483648');
assert.deepEqual(params.toArray(), [0x7FFFFFFF]);
});
it('clamp parsed subparams', () => {
const params = new Params();
parse(params, ':2147483648');
assert.deepEqual(params.toArray(), [0, [0x7FFFFFFF]]);
});
});
});
+233
View File
@@ -0,0 +1,233 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IParams, ParamsArray } from 'common/parser/Types';
// max value supported for a single param/subparam (clamped to positive int32 range)
const MAX_VALUE = 0x7FFFFFFF;
// max allowed subparams for a single sequence (hardcoded limitation)
const MAX_SUBPARAMS = 256;
/**
* Params storage class.
* This type is used by the parser to accumulate sequence parameters and sub parameters
* and transmit them to the input handler actions.
*
* NOTES:
* - params object for action handlers is borrowed, use `.toArray` or `.clone` to get a copy
* - never read beyond `params.length - 1` (likely to contain arbitrary data)
* - `.getSubParams` returns a borrowed typed array, use `.getSubParamsAll` for cloned sub params
* - hardcoded limitations:
* - max. value for a single (sub) param is 2^31 - 1 (greater values are clamped to that)
* - max. 256 sub params possible
* - negative values are not allowed beside -1 (placeholder for default value)
*
* About ZDM (Zero Default Mode):
* ZDM is not orchestrated by this class. If the parser is in ZDM,
* it should add 0 for empty params, otherwise -1. This does not apply
* to subparams, empty subparams should always be added with -1.
*/
export class Params implements IParams {
// params store and length
public params: Int32Array;
public length: number;
// sub params store and length
protected _subParams: Int32Array;
protected _subParamsLength: number;
// sub params offsets from param: param idx --> [start, end] offset
private _subParamsIdx: Uint16Array;
private _rejectDigits: boolean;
private _rejectSubDigits: boolean;
/**
* Create a `Params` type from JS array representation.
*/
public static fromArray(values: ParamsArray): Params {
const params = new Params();
if (!values.length) {
return params;
}
// skip leading sub params
for (let i = (values[0] instanceof Array) ? 1 : 0; i < values.length; ++i) {
const value = values[i];
if (value instanceof Array) {
for (let k = 0; k < value.length; ++k) {
params.addSubParam(value[k]);
}
} else {
params.addParam(value);
}
}
return params;
}
/**
* @param maxLength max length of storable parameters
* @param maxSubParamsLength max length of storable sub parameters
*/
constructor(public maxLength: number = 32, public maxSubParamsLength: number = 32) {
if (maxSubParamsLength > MAX_SUBPARAMS) {
throw new Error('maxSubParamsLength must not be greater than 256');
}
this.params = new Int32Array(maxLength);
this.length = 0;
this._subParams = new Int32Array(maxSubParamsLength);
this._subParamsLength = 0;
this._subParamsIdx = new Uint16Array(maxLength);
this._rejectDigits = false;
this._rejectSubDigits = false;
}
/**
* Clone object.
*/
public clone(): Params {
const newParams = new Params(this.maxLength, this.maxSubParamsLength);
newParams.params.set(this.params);
newParams.length = this.length;
newParams._subParams.set(this._subParams);
newParams._subParamsLength = this._subParamsLength;
newParams._subParamsIdx.set(this._subParamsIdx);
return newParams;
}
/**
* Get a JS array representation of the current parameters and sub parameters.
* The array is structured as follows:
* sequence: "1;2:3:4;5::6"
* array : [1, 2, [3, 4], 5, [-1, 6]]
*/
public toArray(): ParamsArray {
const res: ParamsArray = [];
for (let i = 0; i < this.length; ++i) {
res.push(this.params[i]);
const start = this._subParamsIdx[i] >> 8;
const end = this._subParamsIdx[i] & 0xFF;
if (end - start > 0) {
res.push(Array.prototype.slice.call(this._subParams, start, end));
}
}
return res;
}
/**
* Reset to initial empty state.
*/
public reset(): void {
this.length = 0;
this._subParamsLength = 0;
this._rejectDigits = false;
this._rejectSubDigits = false;
}
/**
* Add a parameter value.
* `Params` only stores up to `maxLength` parameters, any later
* parameter will be ignored.
* Note: VT devices only stored up to 16 values, xterm seems to
* store up to 30.
*/
public addParam(value: number): void {
if (this.length >= this.maxLength) {
this._rejectDigits = true;
return;
}
if (value < -1) {
throw new Error('values lesser than -1 are not allowed');
}
this._subParamsIdx[this.length] = this._subParamsLength << 8 | this._subParamsLength;
this.params[this.length++] = value > MAX_VALUE ? MAX_VALUE : value;
}
/**
* Add a sub parameter value.
* The sub parameter is automatically associated with the last parameter value.
* Thus it is not possible to add a subparameter without any parameter added yet.
* `Params` only stores up to `subParamsLength` sub parameters, any later
* sub parameter will be ignored.
*/
public addSubParam(value: number): void {
if (!this.length) {
return;
}
if (this._subParamsLength >= this.maxSubParamsLength) {
this._rejectSubDigits = true;
return;
}
if (value < -1) {
throw new Error('values lesser than -1 are not allowed');
}
this._subParams[this._subParamsLength++] = value > MAX_VALUE ? MAX_VALUE : value;
this._subParamsIdx[this.length - 1]++;
}
/**
* Whether parameter at index `idx` has sub parameters.
*/
public hasSubParams(idx: number): boolean {
return ((this._subParamsIdx[idx] & 0xFF) - (this._subParamsIdx[idx] >> 8) > 0);
}
/**
* Return sub parameters for parameter at index `idx`.
* Note: The values are borrowed, thus you need to copy
* the values if you need to hold them in nonlocal scope.
*/
public getSubParams(idx: number): Int32Array | null {
const start = this._subParamsIdx[idx] >> 8;
const end = this._subParamsIdx[idx] & 0xFF;
if (end - start > 0) {
return this._subParams.subarray(start, end);
}
return null;
}
/**
* Return all sub parameters as {idx: subparams} mapping.
* Note: The values are not borrowed.
*/
public getSubParamsAll(): {[idx: number]: Int32Array} {
const result: {[idx: number]: Int32Array} = {};
for (let i = 0; i < this.length; ++i) {
const start = this._subParamsIdx[i] >> 8;
const end = this._subParamsIdx[i] & 0xFF;
if (end - start > 0) {
result[i] = this._subParams.slice(start, end);
}
}
return result;
}
/**
* 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) {
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;
}
}
}
+39 -6
View File
@@ -6,6 +6,38 @@
import { IDisposable } from 'common/Types';
import { ParserState } from 'common/parser/Constants';
/** sequence params serialized to js arrays */
export type ParamsArray = (number | number[])[];
/** Params constructor type. */
export interface IParamsConstructor {
new(maxLength: number, maxSubParamsLength: number): IParams;
/** create params from ParamsArray */
fromArray(values: ParamsArray): IParams;
}
/** Interface of Params storage class. */
export interface IParams {
/** from ctor */
maxLength: number;
maxSubParamsLength: number;
/** param values and its length */
params: Int32Array;
length: number;
/** methods */
clone(): IParams;
toArray(): ParamsArray;
reset(): void;
addParam(value: number): void;
addSubParam(value: number): void;
hasSubParams(idx: number): boolean;
getSubParams(idx: number): Int32Array | null;
getSubParamsAll(): {[idx: number]: Int32Array};
}
/**
* Internal state of EscapeSequenceParser.
* Used as argument of the error handler to allow
@@ -26,7 +58,7 @@ export interface IParsingState {
// collect buffer with intermediate characters
collect: string;
// params buffer
params: number[];
params: IParams;
// should abort (default: false)
abort: boolean;
}
@@ -53,7 +85,7 @@ export interface IParsingState {
* `unhook` marks the end of the current DCS sequence.
*/
export interface IDcsHandler {
hook(collect: string, params: number[], flag: number): void;
hook(collect: string, params: IParams, flag: number): void;
put(data: Uint32Array, start: number, end: number): void;
unhook(): void;
}
@@ -68,13 +100,14 @@ export interface IEscapeSequenceParser extends IDisposable {
* It gets reset by the parser for any valid sequence beside REP itself.
*/
precedingCodepoint: number;
/**
* Reset the parser to its initial state (handlers are kept).
*/
reset(): void;
/**
* Parse string `data`.
* Parse UTF32 codepoints in `data` up to `length`.
* @param data The data to parse.
*/
parse(data: Uint32Array, length: number): void;
@@ -86,10 +119,10 @@ export interface IEscapeSequenceParser extends IDisposable {
clearExecuteHandler(flag: string): void;
setExecuteHandlerFallback(callback: (code: number) => void): void;
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void;
setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void;
clearCsiHandler(flag: string): void;
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void;
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
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;
setEscHandler(collectAndFlag: string, callback: () => void): void;
+3 -2
View File
@@ -11,6 +11,7 @@ import { Terminal as TerminalCore } from '../Terminal';
import * as Strings from '../browser/LocalizableStrings';
import { IEvent } from 'common/EventEmitter';
import { AddonManager } from './AddonManager';
import { IParams } from 'common/parser/Types';
export class Terminal implements ITerminalApi {
private _core: ITerminal;
@@ -56,8 +57,8 @@ export class Terminal implements ITerminalApi {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
}
public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
return this._core.addCsiHandler(flag, callback);
public addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable {
return this._core.addCsiHandler(flag, (params: IParams, collect: string) => callback(params.toArray(), collect));
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._core.addOscHandler(ident, callback);
+17
View File
@@ -309,6 +309,23 @@ describe('InputHandler Integration Tests', function(): void {
});
});
});
describe('addCsiHandler', () => {
it('should call custom CSI handler with js array params', async () => {
await page.evaluate(`
window.term.reset();
const _customCsiHandlerParams = [];
const _customCsiHandler = window.term.addCsiHandler('m', (params, collect) => {
_customCsiHandlerParams.push(params);
return false;
}, '');
`);
await page.evaluate(`
window.term.write('\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams');
`);
assert.deepEqual(await page.evaluate(`(() => _customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]);
});
});
});
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
@@ -6,7 +6,7 @@ import { perfContext, before, beforeEach, ThroughputRuntimeCase } from 'xterm-be
import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
import { C0, C1 } from 'common/data/EscapeSequences';
import { IDcsHandler } from 'common/parser/Types';
import { IDcsHandler, IParams } from 'common/parser/Types';
function toUtf32(s: string): Uint32Array {
@@ -18,7 +18,7 @@ function toUtf32(s: string): Uint32Array {
}
class DcsHandler implements IDcsHandler {
hook(collect: string, params: number[], flag: number) : void {}
hook(collect: string, params: IParams, flag: number) : void {}
put(data: Uint32Array, start: number, end: number) : void {}
unhook() :void {}
}
+4 -2
View File
@@ -489,12 +489,14 @@ declare module 'xterm' {
* final character (e.g "m" for SGR) of the CSI sequence.
* @param callback The function to handle the escape sequence. The callback
* is called with the numerical params, as well as the special characters
* (e.g. "$" for DECSCPP). Return true if the sequence was handled; false if
* (e.g. "$" for DECSCPP). If the sequence has subparams the array will
* contain subarrays with their numercial values.
* Return true if the sequence was handled; false if
* we should try a previous handler (set by addCsiHandler or setCsiHandler).
* The most recently-added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable;
/**
* (EXPERIMENTAL) Adds a handler for OSC escape sequences.