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.ts b/src/EscapeSequenceParser.ts index f489884177..c85f670d4c 100644 --- a/src/EscapeSequenceParser.ts +++ b/src/EscapeSequenceParser.ts @@ -4,8 +4,13 @@ */ import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types'; +import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; +interface IHandlerLink extends IDisposable { + nextHandler: IHandlerLink | null; +} + /** * Returns an array filled with numbers between the low and high parameters (right exclusive). * @param low The low number. @@ -41,7 +46,7 @@ export class TransitionTable { * @param action parser action to be done * @param next next parser state */ - add(code: number, state: number, action: number | null, next: number | null): void { + add(code: number, state: number, action: number | null, next: number | null): void { this.table[state << 8 | code] = ((action | 0) << 4) | ((next === undefined) ? state : next); } @@ -303,6 +308,38 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._executeHandlerFb = callback; } + private _linkHandler(handlers: object[], index: number, newCallback: object): IDisposable { + const newHead: any = newCallback; + newHead.nextHandler = handlers[index] as IHandlerLink; + newHead.dispose = function (): void { + let previous = null; + let cur = handlers[index] as IHandlerLink; + for (; cur && cur.nextHandler; + previous = cur, cur = cur.nextHandler) { + if (cur === newHead) { + if (previous) { previous.nextHandler = cur.nextHandler; } + else { handlers[index] = cur.nextHandler; } + break; + } + } + }; + handlers[index] = newHead; + return newHead; + } + + addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable { + const index = flag.charCodeAt(0); + const newHead = + (params: number[], collect: string): void => { + if (! callback(params, collect)) { + const next = (newHead as unknown as IHandlerLink).nextHandler; + if (next) { (next as any)(params, collect); } + else { this._csiHandlerFb(collect, params, index); } + } + }; + return this._linkHandler(this._csiHandlers, index, newHead); + } + setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { this._csiHandlers[flag.charCodeAt(0)] = callback; } @@ -323,6 +360,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._escHandlerFb = callback; } + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + const newHead = + (data: string): void => { + if (! callback(data)) { + const next = (newHead as unknown as IHandlerLink).nextHandler; + if (next) { (next as any)(data); } + else { this._oscHandlerFb(ident, data); } + } + }; + return this._linkHandler(this._oscHandlers, ident, newHead); + } setOscHandler(ident: number, callback: (data: string) => void): void { this._oscHandlers[ident] = callback; } 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..cf489a610c 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -481,6 +481,31 @@ 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.