Skip to content

Commit 7a579a3

Browse files
authored
Merge pull request #1853 from PerBothner/control-seq-handler
hooks for custom CSI/OSC control sequences
2 parents 68b2b79 + f534758 commit 7a579a3

9 files changed

+261
-12
lines changed

fixtures/typings-test/typings-test.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
/// <reference path="../../typings/xterm.d.ts" />
66

7-
import { Terminal } from 'xterm';
7+
import { Terminal, IDisposable } from 'xterm';
88

99
namespace constructor {
1010
{
@@ -119,6 +119,12 @@ namespace methods_core {
119119
const t: Terminal = new Terminal();
120120
t.attachCustomKeyEventHandler((e: KeyboardEvent) => true);
121121
t.attachCustomKeyEventHandler((e: KeyboardEvent) => false);
122+
const d1: IDisposable = t.addCsiHandler("x",
123+
(params: number[], collect: string): boolean => params[0]===1);
124+
d1.dispose();
125+
const d2: IDisposable = t.addOscHandler(199,
126+
(data: string): boolean => true);
127+
d2.dispose();
122128
}
123129
namespace options {
124130
{

src/EscapeSequenceParser.test.ts

+134
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,73 @@ describe('EscapeSequenceParser', function (): void {
11691169
parser2.parse(INPUT);
11701170
chai.expect(csi).eql([]);
11711171
});
1172+
describe('CSI custom handlers', () => {
1173+
it('Prevent fallback', () => {
1174+
const csiCustom: [string, number[], string][] = [];
1175+
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
1176+
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
1177+
parser2.parse(INPUT);
1178+
chai.expect(csi).eql([], 'Should not fallback to original handler');
1179+
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
1180+
});
1181+
it('Allow fallback', () => {
1182+
const csiCustom: [string, number[], string][] = [];
1183+
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
1184+
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return false; });
1185+
parser2.parse(INPUT);
1186+
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler');
1187+
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
1188+
});
1189+
it('Multiple custom handlers fallback once', () => {
1190+
const csiCustom: [string, number[], string][] = [];
1191+
const csiCustom2: [string, number[], string][] = [];
1192+
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
1193+
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
1194+
parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return false; });
1195+
parser2.parse(INPUT);
1196+
chai.expect(csi).eql([], 'Should not fallback to original handler');
1197+
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
1198+
chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]);
1199+
});
1200+
it('Multiple custom handlers no fallback', () => {
1201+
const csiCustom: [string, number[], string][] = [];
1202+
const csiCustom2: [string, number[], string][] = [];
1203+
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
1204+
parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
1205+
parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params, collect]); return true; });
1206+
parser2.parse(INPUT);
1207+
chai.expect(csi).eql([], 'Should not fallback to original handler');
1208+
chai.expect(csiCustom).eql([], 'Should not fallback once');
1209+
chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]);
1210+
});
1211+
it('Execution order should go from latest handler down to the original', () => {
1212+
const order: number[] = [];
1213+
parser2.setCsiHandler('m', () => order.push(1));
1214+
parser2.addCsiHandler('m', () => { order.push(2); return false; });
1215+
parser2.addCsiHandler('m', () => { order.push(3); return false; });
1216+
parser2.parse('\x1b[0m');
1217+
chai.expect(order).eql([3, 2, 1]);
1218+
});
1219+
it('Dispose should work', () => {
1220+
const csiCustom: [string, number[], string][] = [];
1221+
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
1222+
const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
1223+
customHandler.dispose();
1224+
parser2.parse(INPUT);
1225+
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]);
1226+
chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed');
1227+
});
1228+
it('Should not corrupt the parser when dispose is called twice', () => {
1229+
const csiCustom: [string, number[], string][] = [];
1230+
parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params, collect]));
1231+
const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params, collect]); return true; });
1232+
customHandler.dispose();
1233+
customHandler.dispose();
1234+
parser2.parse(INPUT);
1235+
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]);
1236+
chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed');
1237+
});
1238+
});
11721239
it('EXECUTE handler', function (): void {
11731240
parser2.setExecuteHandler('\n', function (): void {
11741241
exe.push('\n');
@@ -1196,6 +1263,73 @@ describe('EscapeSequenceParser', function (): void {
11961263
parser2.parse(INPUT);
11971264
chai.expect(osc).eql([]);
11981265
});
1266+
describe('OSC custom handlers', () => {
1267+
it('Prevent fallback', () => {
1268+
const oscCustom: [number, string][] = [];
1269+
parser2.setOscHandler(1, data => osc.push([1, data]));
1270+
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
1271+
parser2.parse(INPUT);
1272+
chai.expect(osc).eql([], 'Should not fallback to original handler');
1273+
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
1274+
});
1275+
it('Allow fallback', () => {
1276+
const oscCustom: [number, string][] = [];
1277+
parser2.setOscHandler(1, data => osc.push([1, data]));
1278+
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; });
1279+
parser2.parse(INPUT);
1280+
chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler');
1281+
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
1282+
});
1283+
it('Multiple custom handlers fallback once', () => {
1284+
const oscCustom: [number, string][] = [];
1285+
const oscCustom2: [number, string][] = [];
1286+
parser2.setOscHandler(1, data => osc.push([1, data]));
1287+
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
1288+
parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return false; });
1289+
parser2.parse(INPUT);
1290+
chai.expect(osc).eql([], 'Should not fallback to original handler');
1291+
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
1292+
chai.expect(oscCustom2).eql([[1, 'foo=bar']]);
1293+
});
1294+
it('Multiple custom handlers no fallback', () => {
1295+
const oscCustom: [number, string][] = [];
1296+
const oscCustom2: [number, string][] = [];
1297+
parser2.setOscHandler(1, data => osc.push([1, data]));
1298+
parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
1299+
parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return true; });
1300+
parser2.parse(INPUT);
1301+
chai.expect(osc).eql([], 'Should not fallback to original handler');
1302+
chai.expect(oscCustom).eql([], 'Should not fallback once');
1303+
chai.expect(oscCustom2).eql([[1, 'foo=bar']]);
1304+
});
1305+
it('Execution order should go from latest handler down to the original', () => {
1306+
const order: number[] = [];
1307+
parser2.setOscHandler(1, () => order.push(1));
1308+
parser2.addOscHandler(1, () => { order.push(2); return false; });
1309+
parser2.addOscHandler(1, () => { order.push(3); return false; });
1310+
parser2.parse('\x1b]1;foo=bar\x1b\\');
1311+
chai.expect(order).eql([3, 2, 1]);
1312+
});
1313+
it('Dispose should work', () => {
1314+
const oscCustom: [number, string][] = [];
1315+
parser2.setOscHandler(1, data => osc.push([1, data]));
1316+
const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
1317+
customHandler.dispose();
1318+
parser2.parse(INPUT);
1319+
chai.expect(osc).eql([[1, 'foo=bar']]);
1320+
chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed');
1321+
});
1322+
it('Should not corrupt the parser when dispose is called twice', () => {
1323+
const oscCustom: [number, string][] = [];
1324+
parser2.setOscHandler(1, data => osc.push([1, data]));
1325+
const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
1326+
customHandler.dispose();
1327+
customHandler.dispose();
1328+
parser2.parse(INPUT);
1329+
chai.expect(osc).eql([[1, 'foo=bar']]);
1330+
chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed');
1331+
});
1332+
});
11991333
it('DCS handler', function (): void {
12001334
parser2.setDcsHandler('+p', {
12011335
hook: function (collect: string, params: number[], flag: number): void {

src/EscapeSequenceParser.ts

+66-11
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@
44
*/
55

66
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
7+
import { IDisposable } from 'xterm';
78
import { Disposable } from './common/Lifecycle';
89

10+
interface IHandlerCollection<T> {
11+
[key: string]: T[];
12+
}
13+
14+
type CsiHandler = (params: number[], collect: string) => boolean | void;
15+
type OscHandler = (data: string) => boolean | void;
16+
917
/**
1018
* Returns an array filled with numbers between the low and high parameters (right exclusive).
1119
* @param low The low number.
@@ -222,9 +230,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
222230
// handler lookup containers
223231
protected _printHandler: (data: string, start: number, end: number) => void;
224232
protected _executeHandlers: any;
225-
protected _csiHandlers: any;
233+
protected _csiHandlers: IHandlerCollection<CsiHandler>;
226234
protected _escHandlers: any;
227-
protected _oscHandlers: any;
235+
protected _oscHandlers: IHandlerCollection<OscHandler>;
228236
protected _dcsHandlers: any;
229237
protected _activeDcsHandler: IDcsHandler | null;
230238
protected _errorHandler: (state: IParsingState) => IParsingState;
@@ -278,8 +286,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
278286
this._errorHandlerFb = null;
279287
this._printHandler = null;
280288
this._executeHandlers = null;
281-
this._csiHandlers = null;
282289
this._escHandlers = null;
290+
this._csiHandlers = null;
283291
this._oscHandlers = null;
284292
this._dcsHandlers = null;
285293
this._activeDcsHandler = null;
@@ -303,8 +311,24 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
303311
this._executeHandlerFb = callback;
304312
}
305313

314+
addCsiHandler(flag: string, callback: CsiHandler): IDisposable {
315+
const index = flag.charCodeAt(0);
316+
if (this._csiHandlers[index] === undefined) {
317+
this._csiHandlers[index] = [];
318+
}
319+
const handlerList = this._csiHandlers[index];
320+
handlerList.push(callback);
321+
return {
322+
dispose: () => {
323+
const handlerIndex = handlerList.indexOf(callback);
324+
if (handlerIndex !== -1) {
325+
handlerList.splice(handlerIndex, 1);
326+
}
327+
}
328+
};
329+
}
306330
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void {
307-
this._csiHandlers[flag.charCodeAt(0)] = callback;
331+
this._csiHandlers[flag.charCodeAt(0)] = [callback];
308332
}
309333
clearCsiHandler(flag: string): void {
310334
if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)];
@@ -323,8 +347,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
323347
this._escHandlerFb = callback;
324348
}
325349

350+
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
351+
if (this._oscHandlers[ident] === undefined) {
352+
this._oscHandlers[ident] = [];
353+
}
354+
const handlerList = this._oscHandlers[ident];
355+
handlerList.push(callback);
356+
return {
357+
dispose: () => {
358+
const handlerIndex = handlerList.indexOf(callback);
359+
if (handlerIndex !== -1) {
360+
handlerList.splice(handlerIndex, 1);
361+
}
362+
}
363+
};
364+
}
326365
setOscHandler(ident: number, callback: (data: string) => void): void {
327-
this._oscHandlers[ident] = callback;
366+
this._oscHandlers[ident] = [callback];
328367
}
329368
clearOscHandler(ident: number): void {
330369
if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
@@ -461,9 +500,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
461500
}
462501
break;
463502
case ParserAction.CSI_DISPATCH:
464-
callback = this._csiHandlers[code];
465-
if (callback) callback(params, collect);
466-
else this._csiHandlerFb(collect, params, code);
503+
// Trigger CSI Handler
504+
const handlers = this._csiHandlers[code];
505+
let j = handlers ? handlers.length - 1 : -1;
506+
for (; j >= 0; j--) {
507+
if (handlers[j](params, collect)) {
508+
break;
509+
}
510+
}
511+
if (j < 0) {
512+
this._csiHandlerFb(collect, params, code);
513+
}
467514
break;
468515
case ParserAction.PARAM:
469516
if (code === 0x3b) params.push(0);
@@ -538,9 +585,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
538585
// or with an explicit NaN OSC handler
539586
const identifier = parseInt(osc.substring(0, idx));
540587
const content = osc.substring(idx + 1);
541-
callback = this._oscHandlers[identifier];
542-
if (callback) callback(content);
543-
else this._oscHandlerFb(identifier, content);
588+
// Trigger OSC Handler
589+
const handlers = this._oscHandlers[identifier];
590+
let j = handlers ? handlers.length - 1 : -1;
591+
for (; j >= 0; j--) {
592+
if (handlers[j](content)) {
593+
break;
594+
}
595+
}
596+
if (j < 0) {
597+
this._oscHandlerFb(identifier, content);
598+
}
544599
}
545600
}
546601
if (code === 0x1b) transition |= ParserState.ESCAPE;

src/InputHandler.ts

+8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { FLAGS } from './renderer/Types';
1212
import { wcwidth } from './CharWidth';
1313
import { EscapeSequenceParser } from './EscapeSequenceParser';
1414
import { ICharset } from './core/Types';
15+
import { IDisposable } from 'xterm';
1516
import { Disposable } from './common/Lifecycle';
1617

1718
/**
@@ -465,6 +466,13 @@ export class InputHandler extends Disposable implements IInputHandler {
465466
this._terminal.updateRange(buffer.y);
466467
}
467468

469+
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
470+
return this._parser.addCsiHandler(flag, callback);
471+
}
472+
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
473+
return this._parser.addOscHandler(ident, callback);
474+
}
475+
468476
/**
469477
* BEL
470478
* Bell (Ctrl-G).

src/Terminal.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
14131413
this._customKeyEventHandler = customKeyEventHandler;
14141414
}
14151415

1416+
/** Add handler for CSI escape sequence. See xterm.d.ts for details. */
1417+
public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
1418+
return this._inputHandler.addCsiHandler(flag, callback);
1419+
}
1420+
/** Add handler for OSC escape sequence. See xterm.d.ts for details. */
1421+
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
1422+
return this._inputHandler.addOscHandler(ident, callback);
1423+
}
1424+
14161425
/**
14171426
* Registers a link matcher, allowing custom link patterns to be matched and
14181427
* handled.

src/Types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,8 @@ export interface IEscapeSequenceParser extends IDisposable {
492492
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void;
493493
clearCsiHandler(flag: string): void;
494494
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void;
495+
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
496+
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
495497

496498
setEscHandler(collectAndFlag: string, callback: () => void): void;
497499
clearEscHandler(collectAndFlag: string): void;

src/public/Terminal.ts

+6
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ export class Terminal implements ITerminalApi {
5959
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
6060
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
6161
}
62+
public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
63+
return this._core.addCsiHandler(flag, callback);
64+
}
65+
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
66+
return this._core.addOscHandler(ident, callback);
67+
}
6268
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number {
6369
return this._core.registerLinkMatcher(regex, handler, options);
6470
}

src/ui/TestUtils.test.ts

+6
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ export class MockTerminal implements ITerminal {
5454
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
5555
throw new Error('Method not implemented.');
5656
}
57+
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
58+
throw new Error('Method not implemented.');
59+
}
60+
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
61+
throw new Error('Method not implemented.');
62+
}
5763
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
5864
throw new Error('Method not implemented.');
5965
}

0 commit comments

Comments
 (0)