diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts
index 13da69616b..87d911c2a4 100644
--- a/fixtures/typings-test/typings-test.ts
+++ b/fixtures/typings-test/typings-test.ts
@@ -4,7 +4,7 @@
///
-import { Terminal } from 'xterm';
+import { Terminal, IDisposable } from 'xterm';
namespace constructor {
{
@@ -119,6 +119,12 @@ namespace methods_core {
const t: Terminal = new Terminal();
t.attachCustomKeyEventHandler((e: KeyboardEvent) => true);
t.attachCustomKeyEventHandler((e: KeyboardEvent) => false);
+ const d1: IDisposable = t.addCsiHandler("x",
+ (params: number[], collect: string): boolean => params[0]===1);
+ d1.dispose();
+ const d2: IDisposable = t.addOscHandler(199,
+ (data: string): boolean => true);
+ d2.dispose();
}
namespace options {
{
diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts
index e92f412574..0f1d63cc0d 100644
--- a/src/EscapeSequenceParser.test.ts
+++ b/src/EscapeSequenceParser.test.ts
@@ -1169,6 +1169,73 @@ describe('EscapeSequenceParser', function (): void {
parser2.parse(INPUT);
chai.expect(csi).eql([]);
});
+ 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; });
+ parser2.parse(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; });
+ parser2.parse(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; });
+ parser2.parse(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; });
+ parser2.parse(INPUT);
+ chai.expect(csi).eql([], 'Should not fallback to original handler');
+ chai.expect(csiCustom).eql([], 'Should not fallback once');
+ chai.expect(csiCustom2).eql([['m', [1, 31], ''], ['m', [0], '']]);
+ });
+ 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.parse('\x1b[0m');
+ 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; });
+ customHandler.dispose();
+ parser2.parse(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; });
+ customHandler.dispose();
+ customHandler.dispose();
+ parser2.parse(INPUT);
+ chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]);
+ chai.expect(csiCustom).eql([], 'Should not use custom handler as it was disposed');
+ });
+ });
it('EXECUTE handler', function (): void {
parser2.setExecuteHandler('\n', function (): void {
exe.push('\n');
@@ -1196,6 +1263,73 @@ describe('EscapeSequenceParser', function (): void {
parser2.parse(INPUT);
chai.expect(osc).eql([]);
});
+ 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.parse(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.parse(INPUT);
+ chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler');
+ chai.expect(oscCustom).eql([[1, 'foo=bar']]);
+ });
+ 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.parse(INPUT);
+ chai.expect(osc).eql([], 'Should not fallback to original handler');
+ chai.expect(oscCustom).eql([[1, 'foo=bar']]);
+ chai.expect(oscCustom2).eql([[1, 'foo=bar']]);
+ });
+ 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.parse(INPUT);
+ chai.expect(osc).eql([], 'Should not fallback to original handler');
+ chai.expect(oscCustom).eql([], 'Should not fallback once');
+ chai.expect(oscCustom2).eql([[1, 'foo=bar']]);
+ });
+ 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.parse('\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; });
+ customHandler.dispose();
+ parser2.parse(INPUT);
+ chai.expect(osc).eql([[1, 'foo=bar']]);
+ chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed');
+ });
+ 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; });
+ customHandler.dispose();
+ customHandler.dispose();
+ parser2.parse(INPUT);
+ chai.expect(osc).eql([[1, 'foo=bar']]);
+ chai.expect(oscCustom).eql([], 'Should not use custom handler as it was disposed');
+ });
+ });
it('DCS handler', function (): void {
parser2.setDcsHandler('+p', {
hook: function (collect: string, params: number[], flag: number): void {
diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts
index f489884177..6d3de0e0ed 100644
--- a/src/EscapeSequenceParser.ts
+++ b/src/EscapeSequenceParser.ts
@@ -4,8 +4,16 @@
*/
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
+import { IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';
+interface IHandlerCollection {
+ [key: string]: T[];
+}
+
+type CsiHandler = (params: number[], collect: string) => boolean | void;
+type OscHandler = (data: string) => boolean | void;
+
/**
* Returns an array filled with numbers between the low and high parameters (right exclusive).
* @param low The low number.
@@ -222,9 +230,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// handler lookup containers
protected _printHandler: (data: string, start: number, end: number) => void;
protected _executeHandlers: any;
- protected _csiHandlers: any;
+ protected _csiHandlers: IHandlerCollection;
protected _escHandlers: any;
- protected _oscHandlers: any;
+ protected _oscHandlers: IHandlerCollection;
protected _dcsHandlers: any;
protected _activeDcsHandler: IDcsHandler | null;
protected _errorHandler: (state: IParsingState) => IParsingState;
@@ -278,8 +286,8 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._errorHandlerFb = null;
this._printHandler = null;
this._executeHandlers = null;
- this._csiHandlers = null;
this._escHandlers = null;
+ this._csiHandlers = null;
this._oscHandlers = null;
this._dcsHandlers = null;
this._activeDcsHandler = null;
@@ -303,8 +311,24 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._executeHandlerFb = callback;
}
+ addCsiHandler(flag: string, callback: CsiHandler): IDisposable {
+ const index = flag.charCodeAt(0);
+ if (this._csiHandlers[index] === undefined) {
+ this._csiHandlers[index] = [];
+ }
+ const handlerList = this._csiHandlers[index];
+ handlerList.push(callback);
+ return {
+ dispose: () => {
+ const handlerIndex = handlerList.indexOf(callback);
+ if (handlerIndex !== -1) {
+ handlerList.splice(handlerIndex, 1);
+ }
+ }
+ };
+ }
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void {
- this._csiHandlers[flag.charCodeAt(0)] = callback;
+ this._csiHandlers[flag.charCodeAt(0)] = [callback];
}
clearCsiHandler(flag: string): void {
if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)];
@@ -323,8 +347,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._escHandlerFb = callback;
}
+ 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);
+ }
+ }
+ };
+ }
setOscHandler(ident: number, callback: (data: string) => void): void {
- this._oscHandlers[ident] = callback;
+ this._oscHandlers[ident] = [callback];
}
clearOscHandler(ident: number): void {
if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
@@ -461,9 +500,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}
break;
case ParserAction.CSI_DISPATCH:
- callback = this._csiHandlers[code];
- if (callback) callback(params, collect);
- else this._csiHandlerFb(collect, params, code);
+ // Trigger CSI Handler
+ const handlers = this._csiHandlers[code];
+ let j = handlers ? handlers.length - 1 : -1;
+ for (; j >= 0; j--) {
+ if (handlers[j](params, collect)) {
+ break;
+ }
+ }
+ if (j < 0) {
+ this._csiHandlerFb(collect, params, code);
+ }
break;
case ParserAction.PARAM:
if (code === 0x3b) params.push(0);
@@ -538,9 +585,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// or with an explicit NaN OSC handler
const identifier = parseInt(osc.substring(0, idx));
const content = osc.substring(idx + 1);
- callback = this._oscHandlers[identifier];
- if (callback) callback(content);
- else this._oscHandlerFb(identifier, content);
+ // Trigger OSC Handler
+ const handlers = this._oscHandlers[identifier];
+ let j = handlers ? handlers.length - 1 : -1;
+ for (; j >= 0; j--) {
+ if (handlers[j](content)) {
+ break;
+ }
+ }
+ if (j < 0) {
+ this._oscHandlerFb(identifier, content);
+ }
}
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
diff --git a/src/InputHandler.ts b/src/InputHandler.ts
index 7604b01f69..eb1ca1050b 100644
--- a/src/InputHandler.ts
+++ b/src/InputHandler.ts
@@ -12,6 +12,7 @@ import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser';
import { ICharset } from './core/Types';
+import { IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';
/**
@@ -465,6 +466,13 @@ export class InputHandler extends Disposable implements IInputHandler {
this._terminal.updateRange(buffer.y);
}
+ addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
+ return this._parser.addCsiHandler(flag, callback);
+ }
+ addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
+ return this._parser.addOscHandler(ident, callback);
+ }
+
/**
* BEL
* Bell (Ctrl-G).
diff --git a/src/Terminal.ts b/src/Terminal.ts
index bc8fb103a2..bed45e46ec 100644
--- a/src/Terminal.ts
+++ b/src/Terminal.ts
@@ -1413,6 +1413,15 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
this._customKeyEventHandler = customKeyEventHandler;
}
+ /** Add handler for CSI escape sequence. See xterm.d.ts for details. */
+ public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
+ return this._inputHandler.addCsiHandler(flag, callback);
+ }
+ /** Add handler for OSC escape sequence. See xterm.d.ts for details. */
+ public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
+ return this._inputHandler.addOscHandler(ident, callback);
+ }
+
/**
* Registers a link matcher, allowing custom link patterns to be matched and
* handled.
diff --git a/src/Types.ts b/src/Types.ts
index 8ebb28d3e3..b54b9d6610 100644
--- a/src/Types.ts
+++ b/src/Types.ts
@@ -492,6 +492,8 @@ export interface IEscapeSequenceParser extends IDisposable {
setCsiHandler(flag: string, callback: (params: number[], 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;
+ addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
setEscHandler(collectAndFlag: string, callback: () => void): void;
clearEscHandler(collectAndFlag: string): void;
diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts
index 8ff7cf2b3b..87fcfaef99 100644
--- a/src/public/Terminal.ts
+++ b/src/public/Terminal.ts
@@ -59,6 +59,12 @@ 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 addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
+ return this._core.addOscHandler(ident, callback);
+ }
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number {
return this._core.registerLinkMatcher(regex, handler, options);
}
diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts
index 10033a3355..e6e4aaa32f 100644
--- a/src/ui/TestUtils.test.ts
+++ b/src/ui/TestUtils.test.ts
@@ -54,6 +54,12 @@ 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 {
+ throw new Error('Method not implemented.');
+ }
+ addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
+ throw new Error('Method not implemented.');
+ }
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
throw new Error('Method not implemented.');
}
diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts
index 7528bb5540..0ceab01dcc 100644
--- a/typings/xterm.d.ts
+++ b/typings/xterm.d.ts
@@ -481,6 +481,29 @@ declare module 'xterm' {
*/
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
+ /**
+ * (EXPERIMENTAL) Adds a handler for CSI escape sequences.
+ * @param flag The flag should be one-character string, which specifies the
+ * final character (e.g "m" for SGR) of the CSI sequence.
+ * @param callback The function to handle the escape sequence. The callback
+ * is called with the numerical params, as well as the special characters
+ * (e.g. "$" for DECSCPP). 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;
+
+ /**
+ * (EXPERIMENTAL) Adds a handler for OSC escape sequences.
+ * @param ident The number (first parameter) of the sequence.
+ * @param callback The function to handle the escape sequence. The callback
+ * is called with OSC data string. Return true if the sequence was handled;
+ * false if we should try a previous handler (set by addOscHandler or
+ * setOscHandler). The most recently-added handler is tried first.
+ * @return An IDisposable you can call to remove this handler.
+ */
+ addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
/**
* (EXPERIMENTAL) Registers a link matcher, allowing custom link patterns to
* be matched and handled.