diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 02e20961..aed32ab1 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -231,8 +231,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.setExecuteHandler(C0.SO, () => this.shiftOut()); this._parser.setExecuteHandler(C0.SI, () => this.shiftIn()); // FIXME: What do to with missing? Old code just added those to print. - - // some C1 control codes - FIXME: should those be enabled by default? + this._parser.setExecuteHandler(C1.IND, () => this.index()); this._parser.setExecuteHandler(C1.NEL, () => this.nextLine()); this._parser.setExecuteHandler(C1.HTS, () => this.tabSet()); diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts index a22ca696..0112e935 100644 --- a/src/common/parser/DcsParser.ts +++ b/src/common/parser/DcsParser.ts @@ -12,7 +12,8 @@ import { PAYLOAD_LIMIT } from 'common/parser/Constants'; export class DcsParser implements IDcsParser { private _handlers: IHandlerCollection = Object.create(null); - private _active: IDcsHandler[] = []; + private _empty: IDcsHandler[] = []; + private _active: IDcsHandler[] = this._empty; private _ident: number = 0; private _handlerFb: DcsFallbackHandler = () => {}; @@ -53,13 +54,15 @@ export class DcsParser implements IDcsParser { if (this._active.length) { this.unhook(false); } - this._active = []; + this._active = this._empty; this._ident = 0; } public hook(ident: number, params: IParams): void { + // always reset leftover handlers + this.reset(); this._ident = ident; - this._active = this._handlers[ident] || []; + this._active = this._handlers[ident] || this._empty; if (!this._active.length) { this._handlerFb(this._ident, 'HOOK', params); } else { @@ -95,7 +98,7 @@ export class DcsParser implements IDcsParser { this._active[j].unhook(false); } } - this._active = []; + this._active = this._empty; this._ident = 0; } } diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts index 5c4fe5b0..40ce38b1 100644 --- a/src/common/parser/EscapeSequenceParser.test.ts +++ b/src/common/parser/EscapeSequenceParser.test.ts @@ -279,12 +279,10 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); for (state in states) { parser.currentState = state; - parser.osc = '#'; parser.params = [23]; parser.collect = '#'; parse(parser, '\x1b'); chai.expect(parser.currentState).equal(ParserState.ESCAPE); - chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); chai.expect(parser.collect).equal(''); parser.reset(); @@ -396,24 +394,20 @@ describe('EscapeSequenceParser', function (): void { parser.reset(); // C0 parser.currentState = ParserState.ESCAPE; - parser.osc = '#'; parser.params = [123]; parser.collect = '#'; parse(parser, '['); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); - chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); chai.expect(parser.collect).equal(''); parser.reset(); // C1 for (state in states) { parser.currentState = state; - parser.osc = '#'; parser.params = [123]; parser.collect = '#'; parse(parser, '\x9b'); chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); - chai.expect(parser.osc).equal(''); chai.expect(parser.params).eql([0]); chai.expect(parser.collect).equal(''); parser.reset(); diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts index d40f39de..30aabc6d 100644 --- a/src/common/parser/EscapeSequenceParser.ts +++ b/src/common/parser/EscapeSequenceParser.ts @@ -334,24 +334,24 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this._dcsParser.dispose(); } - setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { + public setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void { this._printHandler = callback; } - clearPrintHandler(): void { + public clearPrintHandler(): void { this._printHandler = this._printHandlerFb; } - setExecuteHandler(flag: string, callback: () => void): void { + public setExecuteHandler(flag: string, callback: () => void): void { this._executeHandlers[flag.charCodeAt(0)] = callback; } - clearExecuteHandler(flag: string): void { + public clearExecuteHandler(flag: string): void { if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)]; } - setExecuteHandlerFallback(callback: (code: number) => void): void { + public setExecuteHandlerFallback(callback: (code: number) => void): void { this._executeHandlerFb = callback; } - addCsiHandler(id: IFunctionIdentifier, callback: CsiHandler): IDisposable { + public addCsiHandler(id: IFunctionIdentifier, callback: CsiHandler): IDisposable { const ident = this._identifier(id); if (this._csiHandlers[ident] === undefined) { this._csiHandlers[ident] = []; @@ -367,17 +367,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } }; } - setCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => void): void { + public setCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => void): void { this._csiHandlers[this._identifier(id)] = [callback]; } - clearCsiHandler(id: IFunctionIdentifier): void { + public clearCsiHandler(id: IFunctionIdentifier): void { if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)]; } - setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void { + public setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void { this._csiHandlerFb = callback; } - addEscHandler(id: IFunctionIdentifier, callback: EscHandler): IDisposable { + public addEscHandler(id: IFunctionIdentifier, callback: EscHandler): IDisposable { const ident = this._identifier(id, [0x30, 0x7e]); if (this._escHandlers[ident] === undefined) { this._escHandlers[ident] = []; @@ -393,50 +393,50 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } }; } - setEscHandler(id: IFunctionIdentifier, callback: () => void): void { + public setEscHandler(id: IFunctionIdentifier, callback: () => void): void { this._escHandlers[this._identifier(id, [0x30, 0x7e])] = [callback]; } - clearEscHandler(id: IFunctionIdentifier): void { + public clearEscHandler(id: IFunctionIdentifier): void { if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])]; } - setEscHandlerFallback(callback: (ident: number) => void): void { + public setEscHandlerFallback(callback: (ident: number) => void): void { this._escHandlerFb = callback; } - addOscHandler(ident: number, handler: IOscHandler): IDisposable { + public addOscHandler(ident: number, handler: IOscHandler): IDisposable { return this._oscParser.addOscHandler(ident, handler); } - setOscHandler(ident: number, handler: IOscHandler): void { + public setOscHandler(ident: number, handler: IOscHandler): void { this._oscParser.setOscHandler(ident, handler); } - clearOscHandler(ident: number): void { + public clearOscHandler(ident: number): void { this._oscParser.clearOscHandler(ident); } - setOscHandlerFallback(handler: OscFallbackHandler): void { + public setOscHandlerFallback(handler: OscFallbackHandler): void { this._oscParser.setOscHandlerFallback(handler); } - addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { + public addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable { return this._dcsParser.addDcsHandler(this._identifier(id), handler); } - setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void { + public setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void { this._dcsParser.setDcsHandler(this._identifier(id), handler); } - clearDcsHandler(id: IFunctionIdentifier): void { + public clearDcsHandler(id: IFunctionIdentifier): void { this._dcsParser.clearDcsHandler(this._identifier(id)); } - setDcsHandlerFallback(handler: DcsFallbackHandler): void { + public setDcsHandlerFallback(handler: DcsFallbackHandler): void { this._dcsParser.setDcsHandlerFallback(handler); } - setErrorHandler(callback: (state: IParsingState) => IParsingState): void { + public setErrorHandler(callback: (state: IParsingState) => IParsingState): void { this._errorHandler = callback; } - clearErrorHandler(): void { + public clearErrorHandler(): void { this._errorHandler = this._errorHandlerFb; } - reset(): void { + public reset(): void { this.currentState = this.initialState; this._oscParser.reset(); this._dcsParser.reset(); @@ -463,7 +463,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP * - OSC_STRING:OSC_PUT * - DCS_PASSTHROUGH:DCS_PUT */ - parse(data: Uint32Array, length: number): void { + public parse(data: Uint32Array, length: number): void { let code = 0; let transition = 0; let currentState = this.currentState; @@ -472,7 +472,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP let collect = this._collect; const params = this._params; const table: Uint8Array = this.TRANSITIONS.table; - let callback: Function | null = null; // process input string for (let i = 0; i < length; ++i) { @@ -508,8 +507,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP } break; case ParserAction.EXECUTE: - callback = this._executeHandlers[code]; - if (callback) callback(); + if (this._executeHandlers[code]) this._executeHandlers[code](); else this._executeHandlerFb(code); this.precedingCodepoint = 0; break; @@ -581,7 +579,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP this.precedingCodepoint = 0; break; case ParserAction.CLEAR: - osc.reset(); params.reset(); params.addParam(0); // ZDM collect = 0; @@ -603,7 +600,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP case ParserAction.DCS_UNHOOK: dcs.unhook(code !== 0x18 && code !== 0x1a); if (code === 0x1b) transition |= ParserState.ESCAPE; - osc.reset(); params.reset(); params.addParam(0); // ZDM collect = 0; @@ -625,7 +621,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP case ParserAction.OSC_END: osc.end(code !== 0x18 && code !== 0x1a); if (code === 0x1b) transition |= ParserState.ESCAPE; - osc.reset(); params.reset(); params.addParam(0); // ZDM collect = 0; @@ -635,7 +630,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP currentState = transition & TableAccess.TRANSITION_STATE_MASK; } - // save non pushable buffers + // save collected intermediates this._collect = collect; // save state diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts index cf363714..ed549deb 100644 --- a/src/common/parser/OscParser.ts +++ b/src/common/parser/OscParser.ts @@ -16,7 +16,7 @@ export class OscParser extends Disposable { private _handlers: IHandlerCollection = Object.create(null); private _handlerFb: OscFallbackHandler = () => { }; - addOscHandler(ident: number, handler: IOscHandler): IDisposable { + public addOscHandler(ident: number, handler: IOscHandler): IDisposable { if (this._handlers[ident] === undefined) { this._handlers[ident] = []; } @@ -31,13 +31,13 @@ export class OscParser extends Disposable { } }; } - setOscHandler(ident: number, handler: IOscHandler): void { + public setOscHandler(ident: number, handler: IOscHandler): void { this._handlers[ident] = [handler]; } - clearOscHandler(ident: number): void { + public clearOscHandler(ident: number): void { if (this._handlers[ident]) delete this._handlers[ident]; } - setOscHandlerFallback(handler: OscFallbackHandler): void { + public setOscHandlerFallback(handler: OscFallbackHandler): void { this._handlerFb = handler; } @@ -100,6 +100,8 @@ export class OscParser extends Disposable { } public start(): void { + // always reset leftover handlers + this.reset(); this._id = -1; this._state = OscState.ID; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 417e03a9..3891004e 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -502,14 +502,13 @@ declare module 'xterm' { /** * Adds a handler for CSI escape sequences. - * @param id Specifies the function identifier under which the callback gets registered, - * e.g. {final: 'm'} for SGR. - * @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). 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). + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {final: 'm'} for SGR. + * @param callback The function to handle the sequence. The callback is + * called with the numerical params. 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. */ @@ -517,17 +516,18 @@ declare module 'xterm' { /** * Adds a handler for DCS escape sequences. - * @param id Specifies the function identifier under which the callback gets registered, - * e.g. {intermediates: '$' final: 'q'} for DECRQSS. - * @param callback The function to handle the escape sequence. Note that the + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {intermediates: '$' final: 'q'} for DECRQSS. + * @param callback The function to handle the sequence. Note that the * function will only be called once if the sequence finished sucessfully. - * There is currently no way to intercept smaller data chunks, those will be stored up - * until the sequence is finished. Since DCS sequences are not limited by the amount - * of data this might impose a problem for big payloads. Currently xterm.js limits - * DCS payload to 10 MB which should give enough room for most use cases. - * The function gets numerical parameter and the data as arguments. - * Return true if the sequence was handled; false if - * we should try a previous handler (set by addDcsHandler or setDcsHandler). + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since DCS sequences + * are not limited by the amount of data this might impose a problem for + * big payloads. Currently xterm.js limits DCS payload to 10 MB + * which should give enough room for most use cases. + * The function gets the payload and numerical parameters as arguments. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addDcsHandler or setDcsHandler). * The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ @@ -535,11 +535,12 @@ declare module 'xterm' { /** * Adds a handler for ESC escape sequences. - * @param id Specifies the function identifier under which the callback gets registered, - * e.g. {intermediates: '%' final: 'G'} for default charset selection. - * @param callback The function to handle the escape sequence. - * Return true if the sequence was handled; false if - * we should try a previous handler (set by addEscHandler or setEscHandler). + * @param id Specifies the function identifier under which the callback + * gets registered, e.g. {intermediates: '%' final: 'G'} for + * default charset selection. + * @param callback The function to handle the sequence. + * Return true if the sequence was handled; false if we should try + * a previous handler (set by addEscHandler or setEscHandler). * The most recently-added handler is tried first. * @return An IDisposable you can call to remove this handler. */ @@ -548,15 +549,17 @@ declare module 'xterm' { /** * 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. Note that the + * @param callback The function to handle the sequence. Note that the * function will only be called once if the sequence finished sucessfully. - * There is currently no way to intercept smaller data chunks, those will be stored up - * until the sequence is finished. Since OSC sequences are not limited by the amount - * of data this might impose a problem for big payloads. Currently xterm.js limits - * OSC payload to 10 MB which should give enough room for most use cases. - * 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. + * There is currently no way to intercept smaller data chunks, data chunks + * will be stored up until the sequence is finished. Since OSC sequences + * are not limited by the amount of data this might impose a problem for + * big payloads. Currently xterm.js limits OSC payload to 10 MB + * which should give enough room for most use cases. + * 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; @@ -988,19 +991,44 @@ declare module 'xterm' { } /** - * Data type to register a CSI, DCS or ESC callback in the parser. + * Data type to register a CSI, DCS or ESC callback in the parser in the form: + * ESC I..I F + * CSI Prefix P..P I..I F + * DCS Prefix P..P I..I F data_bytes ST + * + * with these rules/restrictions: + * - prefix can only be used with CSI and DCS + * - only one leading prefix byte is recognized by the parser + * before any other parameter bytes (P..P) + * - intermediate bytes are recognized up to 2 + * + * For custom sequences make sure to read ECMA-48 and the resources at + * vt100.net to not clash with existing sequences or reserved address space. + * General recommendations: + * - use private address space (see ECMA-48) + * - use max one intermediate byte (technically not limited by the spec, + * in practice there are no sequences with more than one intermediate byte, + * thus parsers might get confused with more intermediates) + * - test against other common emulators to check whether they escape/ignore + * the sequence correctly + * + * Notes: OSC command registration is handled differently (see addOscHandler) + * APC, PM or SOS is currently not supported */ export interface IFunctionIdentifier { /** * Optional prefix byte, must be in range \x3c .. \x3f. + * Usable in CSI and DCS. */ prefix?: string; /** * Optional intermediate bytes, must be in range \x20 .. \x2f. + * Usable in CSI, DCS and ESC. */ intermediates?: string; /** - * Final byte, must be in range \x40 .. \x7e (\x30 .. \x7e for ESC). + * Final byte, must be in range \x40 .. \x7e for CSI and DCS, + * \x30 .. \x7e for ESC. */ final: string; }