diff --git a/demo/client.ts b/demo/client.ts index 7942cffe..2041c048 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -227,7 +227,8 @@ function initOptions(term: TerminalType): void { fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], logLevel: ['debug', 'info', 'warn', 'error', 'off'], rendererType: ['dom', 'canvas'], - wordSeparator: null + wordSeparator: null, + allowedWindowOps: '' }; const options = Object.keys((term)._core.options); const booleanOptions = []; @@ -258,7 +259,7 @@ function initOptions(term: TerminalType): void { }); html += '
'; Object.keys(stringOptions).forEach(o => { - if (stringOptions[o]) { + if (stringOptions[o] && typeof stringOptions[o] !== 'string') { html += `
`; } else { html += `
`; diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index d8defa1c..21d6d742 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -1266,4 +1266,151 @@ describe('InputHandler', () => { [131072, 131072], [131072, 131072], [131072, 300000 - 131072 - 131072] ]); }); + describe('windowOps', () => { + it('all should be disabled by default and not report', () => { + const term = new TestTerminal({cols: 10, rows: 10}); + assert.deepEqual(term.options.allowedWindowOps, []); + const stack: string[] = []; + term.onData(data => stack.push(data)); + term.writeSync('\x1b[14t'); + term.writeSync('\x1b[16t'); + term.writeSync('\x1b[18t'); + term.writeSync('\x1b[20t'); + term.writeSync('\x1b[21t'); + assert.deepEqual(stack, []); + }); + it('14 - GetWinSizePixels', () => { + const term = new TestTerminal({cols: 10, rows: 10, allowedWindowOps: [14]}); + assert.deepEqual(term.options.allowedWindowOps, [14]); + const stack: string[] = []; + term.onData(data => stack.push(data)); + term.writeSync('\x1b[14t'); + // does not report in test terminal due to missing renderer + assert.deepEqual(stack, []); + }); + it('16 - GetCellSizePixels', () => { + const term = new TestTerminal({cols: 10, rows: 10, allowedWindowOps: [16]}); + assert.deepEqual(term.options.allowedWindowOps, [16]); + const stack: string[] = []; + term.onData(data => stack.push(data)); + term.writeSync('\x1b[16t'); + // does not report in test terminal due to missing renderer + assert.deepEqual(stack, []); + }); + it('18 - GetWinSizeChars', () => { + const term = new TestTerminal({cols: 10, rows: 10, allowedWindowOps: [18]}); + assert.deepEqual(term.options.allowedWindowOps, [18]); + const stack: string[] = []; + term.onData(data => stack.push(data)); + term.writeSync('\x1b[18t'); + assert.deepEqual(stack, ['\x1b[8;10;10t']); + term.resize(50, 20); + term.writeSync('\x1b[18t'); + assert.deepEqual(stack, ['\x1b[8;10;10t', '\x1b[8;20;50t']); + }); + it('20 - GetIconTitle', () => { + const term = new TestTerminal({cols: 10, rows: 10, allowedWindowOps: [20]}); + assert.deepEqual(term.options.allowedWindowOps, [20]); + const stack: string[] = []; + term.onData(data => stack.push(data)); + term.writeSync('\x1b]1;hello world!\x07'); + term.writeSync('\x1b[20t'); + assert.deepEqual(stack, ['\x1b]Lhello world!\x1b\\']); + term.writeSync('\x1b]1;some other\x07'); + term.writeSync('\x1b[20t'); + assert.deepEqual(stack, ['\x1b]Lhello world!\x1b\\', '\x1b]Lsome other\x1b\\']); + }); + it('21 - GetWinTitle', () => { + const term = new TestTerminal({cols: 10, rows: 10, allowedWindowOps: [21]}); + assert.deepEqual(term.options.allowedWindowOps, [21]); + const stack: string[] = []; + term.onData(data => stack.push(data)); + term.writeSync('\x1b]2;hello world!\x07'); + term.writeSync('\x1b[21t'); + assert.deepEqual(stack, ['\x1b]lhello world!\x1b\\']); + term.writeSync('\x1b]2;some other\x07'); + term.writeSync('\x1b[21t'); + assert.deepEqual(stack, ['\x1b]lhello world!\x1b\\', '\x1b]lsome other\x1b\\']); + }); + it('22/23 - PushTitle/PopTitle', () => { + const term = new TestTerminal({cols: 10, rows: 10, allowedWindowOps: [22, 23]}); + assert.deepEqual(term.options.allowedWindowOps, [22, 23]); + const stack: string[] = []; + term.onTitleChange(data => stack.push(data)); + term.writeSync('\x1b]0;1\x07'); + term.writeSync('\x1b[22t'); + term.writeSync('\x1b]0;2\x07'); + term.writeSync('\x1b[22t'); + term.writeSync('\x1b]0;3\x07'); + term.writeSync('\x1b[22t'); + assert.deepEqual((term as any)._inputHandler._windowTitleStack, ['1', '2', '3']); + assert.deepEqual((term as any)._inputHandler._iconNameStack, ['1', '2', '3']); + assert.deepEqual(stack, ['1', '2', '3']); + term.writeSync('\x1b[23t'); + term.writeSync('\x1b[23t'); + term.writeSync('\x1b[23t'); + term.writeSync('\x1b[23t'); // one more to test "overflow" + assert.deepEqual((term as any)._inputHandler._windowTitleStack, []); + assert.deepEqual((term as any)._inputHandler._iconNameStack, []); + assert.deepEqual(stack, ['1', '2', '3', '3', '2', '1']); + }); + it('22/23 - PushTitle/PopTitle with ;1', () => { + const term = new TestTerminal({cols: 10, rows: 10, allowedWindowOps: [22, 23]}); + assert.deepEqual(term.options.allowedWindowOps, [22, 23]); + const stack: string[] = []; + term.onTitleChange(data => stack.push(data)); + term.writeSync('\x1b]0;1\x07'); + term.writeSync('\x1b[22;1t'); + term.writeSync('\x1b]0;2\x07'); + term.writeSync('\x1b[22;1t'); + term.writeSync('\x1b]0;3\x07'); + term.writeSync('\x1b[22;1t'); + assert.deepEqual((term as any)._inputHandler._windowTitleStack, []); + assert.deepEqual((term as any)._inputHandler._iconNameStack, ['1', '2', '3']); + assert.deepEqual(stack, ['1', '2', '3']); + term.writeSync('\x1b[23;1t'); + term.writeSync('\x1b[23;1t'); + term.writeSync('\x1b[23;1t'); + term.writeSync('\x1b[23;1t'); // one more to test "overflow" + assert.deepEqual((term as any)._inputHandler._windowTitleStack, []); + assert.deepEqual((term as any)._inputHandler._iconNameStack, []); + assert.deepEqual(stack, ['1', '2', '3']); + }); + it('22/23 - PushTitle/PopTitle with ;2', () => { + const term = new TestTerminal({cols: 10, rows: 10, allowedWindowOps: [22, 23]}); + assert.deepEqual(term.options.allowedWindowOps, [22, 23]); + const stack: string[] = []; + term.onTitleChange(data => stack.push(data)); + term.writeSync('\x1b]0;1\x07'); + term.writeSync('\x1b[22;2t'); + term.writeSync('\x1b]0;2\x07'); + term.writeSync('\x1b[22;2t'); + term.writeSync('\x1b]0;3\x07'); + term.writeSync('\x1b[22;2t'); + assert.deepEqual((term as any)._inputHandler._windowTitleStack, ['1', '2', '3']); + assert.deepEqual((term as any)._inputHandler._iconNameStack, []); + assert.deepEqual(stack, ['1', '2', '3']); + term.writeSync('\x1b[23;2t'); + term.writeSync('\x1b[23;2t'); + term.writeSync('\x1b[23;2t'); + term.writeSync('\x1b[23;2t'); // one more to test "overflow" + assert.deepEqual((term as any)._inputHandler._windowTitleStack, []); + assert.deepEqual((term as any)._inputHandler._iconNameStack, []); + assert.deepEqual(stack, ['1', '2', '3', '3', '2', '1']); + }); + it('DECCOLM - should only work with "SetWinLines" (24) enabled', () => { + // disabled + const term = new TestTerminal({cols: 10, rows: 10}); + term.writeSync('\x1b[?3l'); + assert.equal((term as any)._bufferService.cols, 10); + term.writeSync('\x1b[?3h'); + assert.equal((term as any)._bufferService.cols, 10); + // enabled + const term2 = new TestTerminal({cols: 10, rows: 10, allowedWindowOps: [24]}); + term2.writeSync('\x1b[?3l'); + assert.equal((term2 as any)._bufferService.cols, 80); + term2.writeSync('\x1b[?3h'); + assert.equal((term2 as any)._bufferService.cols, 132); + }); + }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index de28c40c..574d29b8 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -33,6 +33,11 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, */ const MAX_PARSEBUFFER_LENGTH = 131072; +/** + * Limit length of title and icon name stacks. + */ +const STACK_LIMIT = 10; + /** * DCS subparser implementations @@ -127,6 +132,10 @@ export class InputHandler extends Disposable implements IInputHandler { private _stringDecoder: StringToUtf32 = new StringToUtf32(); private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32(); private _workCell: CellData = new CellData(); + private _windowTitle = ''; + private _iconName = ''; + private _windowTitleStack: string[] = []; + private _iconNameStack: string[] = []; private _onCursorMove = new EventEmitter(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } @@ -222,7 +231,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._parser.setCsiHandler({intermediates: ' ', final: 'q'}, params => this.setCursorStyle(params)); this._parser.setCsiHandler({final: 'r'}, params => this.setScrollRegion(params)); this._parser.setCsiHandler({final: 's'}, params => this.saveCursor(params)); - this._parser.setCsiHandler({final: 't'}, params => this.manipulateWindowOptions(params)); + this._parser.setCsiHandler({final: 't'}, params => this.windowOptions(params)); this._parser.setCsiHandler({final: 'u'}, params => this.restoreCursor(params)); this._parser.setCsiHandler({intermediates: '\'', final: '}'}, params => this.insertColumns(params)); this._parser.setCsiHandler({intermediates: '\'', final: '~'}, params => this.deleteColumns(params)); @@ -249,8 +258,9 @@ export class InputHandler extends Disposable implements IInputHandler { * OSC handler */ // 0 - icon name + title - this._parser.setOscHandler(0, new OscHandler((data: string) => this.setTitle(data))); + this._parser.setOscHandler(0, new OscHandler((data: string) => { this.setTitle(data); this.setIconName(data); })); // 1 - icon name + this._parser.setOscHandler(1, new OscHandler((data: string) => this.setIconName(data))); // 2 - title this._parser.setOscHandler(2, new OscHandler((data: string) => this.setTitle(data))); // 3 - set property X in the form "prop=value" @@ -502,6 +512,15 @@ export class InputHandler extends Disposable implements IInputHandler { * Forward addCsiHandler from parser. */ public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + if (id.final === 't' && !id.prefix && !id.intermediates) { + // security: always check whether window option is allowed + return this._parser.addCsiHandler(id, params => { + if (!~this._optionsService.options.allowedWindowOps.indexOf(params.params[0])) { + return true; + } + return callback(params); + }); + } return this._parser.addCsiHandler(id, callback); } @@ -1391,11 +1410,16 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.setgCharset(3, DEFAULT_CHARSET); // set VT100 mode here break; - case 3: // 132 col mode - // TODO: move DECCOLM into compat addon - this._terminal.savedCols = this._bufferService.cols; - this._terminal.resize(132, this._bufferService.rows); - this._terminal.reset(); + case 3: + /** + * DECCOLM - 132 column mode. + * This is only active if 'SetWinLines' (24) is listed + * in `options.allowedWindowOps`. + */ + if (~this._optionsService.options.allowedWindowOps.indexOf(24)) { + this._terminal.resize(132, this._bufferService.rows); + this._terminal.reset(); + } break; case 6: this._terminal.originMode = true; @@ -1574,11 +1598,15 @@ export class InputHandler extends Disposable implements IInputHandler { // TODO: move DECCOLM into compat addon // Note: This impl currently does not enforce col 80, instead reverts // to previous terminal width before entering DECCOLM 132 - if (this._bufferService.cols === 132 && this._terminal.savedCols) { - this._terminal.resize(this._terminal.savedCols, this._bufferService.rows); + /** + * DECCOLM - 80 column mode. + * This is only active if 'SetWinLines' (24) is listed + * in `options.allowedWindowOps`. + */ + if (~this._optionsService.options.allowedWindowOps.indexOf(24)) { + this._terminal.resize(80, this._bufferService.rows); + this._terminal.reset(); } - delete this._terminal.savedCols; - this._terminal.reset(); break; case 6: this._terminal.originMode = false; @@ -2020,96 +2048,91 @@ export class InputHandler extends Disposable implements IInputHandler { /** * CSI Ps ; Ps ; Ps t - Various window manipulations and reports (xterm) - * Ps = 1 -> De-iconify window. not supported - * Ps = 2 -> Iconify window. not supported - * Ps = 3 ; x ; y -> Move window to [x, y]. not supported - * Ps = 4 ; height ; width TBD - * Resize the xterm window to given height and width in pixels. - * Omitted parameters reuse the current height or width. - * Zero parameters use the display's height or width. - * Ps = 5 not supported - * Raise the xterm window to the front of the stacking order. - * Ps = 6 not supported - * Lower the xterm window to the bottom of the stacking order. - * Ps = 7 TBD - * Refresh the xterm window. - * Ps = 8 ; height ; width TBD - * Resize the text area to given height and width in characters. - * Omitted parameters reuse the current height or width. - * Zero parameters use the display's height or width. - * Ps = 9 ; 0 -> Restore maximized window. TBD - * Ps = 9 ; 1 TBD - * Maximize window (i.e., resize to screen size). - * Ps = 9 ; 2 -> Maximize window vertically. TBD - * Ps = 9 ; 3 -> Maximize window horizontally. TBD - * Ps = 10 ; 0 -> Undo full-screen mode. TBD - * Ps = 10 ; 1 -> Change to full-screen. TBD - * Ps = 10 ; 2 -> Toggle full-screen. TBD - * Ps = 11 not supported (always report non-iconified?) - * Report xterm window state. - * If the xterm window is non-iconified, it returns CSI 1 t . - * If the xterm window is iconified, it returns CSI 2 t . - * Ps = 13 -> Report xterm window position. TBD - * Note: X Toolkit positions can be negative, but the reported - * values are unsigned, in the range 0-65535. Negative values - * correspond to 32768-65535. - * Result is CSI 3 ; x ; y t - * Ps = 13 ; 2 TBD - * Report xterm text-area position. Result is CSI 3 ; x ; y t - * Ps = 1 4 TBD - * Report xterm text area size in pixels. Result is CSI 4 ; height ; width t - * Ps = 14 ; 2 TBD - * Report xterm window size in pixels. - * Normally xterm's window is larger than its text area, since it - * includes the frame (or decoration) applied by the window manager, - * as well as the area used by a scroll-bar. Result is CSI 4 ; height ; width t - * Ps = 15 TBD - * Report size of the screen in pixels. Result is CSI 5 ; height ; width t - * Ps = 16 TBD - * Report xterm character cell size in pixels. Result is CSI 6 ; height ; width t - * Ps = 18 TBD - * Report the size of the text area in characters. Result is CSI 8 ; height ; width t - * Ps = 19 TBD - * Report the size of the screen in characters. Result is CSI 9 ; height ; width t - * Ps = 20 TBD - * Report xterm window's icon label. Result is OSC L label ST - * Ps = 21 TBD - * Report xterm window's title. Result is OSC l label ST - * Ps = 22 ; 0 -> Save xterm icon and window title on stack. TBD - * Ps = 22 ; 1 -> Save xterm icon title on stack. TBD - * Ps = 22 ; 2 -> Save xterm window title on stack. TBD - * Ps = 23 ; 0 -> Restore xterm icon and window title from stack. TBD - * Ps = 23 ; 1 -> Restore xterm icon title from stack. TBD - * Ps = 23 ; 2 -> Restore xterm window title from stack. TBD - * Ps >= 24 TBD - * Resize to Ps lines (DECSLPP), VT340 and VT420. xterm adapts this by resizing its window. + * + * Note: Only those listed below are supported. All others are left to integrators and + * need special treatment based on the embedding environment. + * + * Ps = 1 4 supported + * Report xterm text area size in pixels. + * Result is CSI 4 ; height ; width t + * Ps = 14 ; 2 not implemented + * Ps = 16 supported + * Report xterm character cell size in pixels. + * Result is CSI 6 ; height ; width t + * Ps = 18 supported + * Report the size of the text area in characters. + * Result is CSI 8 ; height ; width t + * Ps = 20 supported + * Report xterm window's icon label. + * Result is OSC L label ST + * Ps = 21 supported + * Report xterm window's title. + * Result is OSC l label ST + * Ps = 22 ; 0 -> Save xterm icon and window title on stack. supported + * Ps = 22 ; 1 -> Save xterm icon title on stack. supported + * Ps = 22 ; 2 -> Save xterm window title on stack. supported + * Ps = 23 ; 0 -> Restore xterm icon and window title from stack. supported + * Ps = 23 ; 1 -> Restore xterm icon title from stack. supported + * Ps = 23 ; 2 -> Restore xterm window title from stack. supported + * Ps >= 24 not implemented */ - public manipulateWindowOptions(params?: IParams): void { - console.log(params); + public windowOptions(params?: IParams): void { + if (!~this._optionsService.options.allowedWindowOps.indexOf(params.params[0])) { + return; + } + const second = (params.length > 1) ? params.params[1] : 0; + const rs = (this._terminal as any)._renderService; // FIXME: import renderService to get rid of any type here switch (params.params[0]) { - case 8: - if (params.length === 3) { - // TODO: 0/1 support - const cols = Math.max(2, params.params[2]); - const rows = Math.max(2, params.params[1]); - // make it work with demo thus shamelessly taken from demo's client.ts - const width = (cols * (this._terminal as any)._renderService.dimensions.actualCellWidth + this._terminal.viewport.scrollBarWidth).toString() + 'px'; - const height = (rows * (this._terminal as any)._renderService.dimensions.actualCellHeight).toString() + 'px'; - (this._terminal as any)._parent.style.width = width; - (this._terminal as any)._parent.style.height = height; - const term = Function('return this.term;')(); - // move fit into browser codebase? - for (const addon of (term as any)._addonManager._addons) { - if (`${addon.instance.constructor}`.indexOf('FitAddon') !== -1) { - addon.instance.fit(); - } + case 14: // GetWinSizePixels, returns CSI 4 ; height ; width t + if (rs && second !== 2) { + const w = rs.dimensions.canvasWidth.toFixed(0); + const h = rs.dimensions.canvasHeight.toFixed(0); + this._coreService.triggerDataEvent(`${C0.ESC}[4;${h};${w}t`); + } + break; + case 16: // GetCellSizePixels, returns CSI 6 ; height ; width t + if (rs) { + const w = rs.dimensions.actualCellWidth.toFixed(0); + const h = rs.dimensions.actualCellHeight.toFixed(0); + this._coreService.triggerDataEvent(`${C0.ESC}[4;${h};${w}t`); + } + break; + case 18: // GetWinSizeChars, returns CSI 8 ; height ; width t + if (this._bufferService) { + this._coreService.triggerDataEvent(`${C0.ESC}[8;${this._bufferService.rows};${this._bufferService.cols}t`); + } + break; + case 20: // GetIconTitle, returns OSC L label ST + this._coreService.triggerDataEvent(`${C0.ESC}]L${this._iconName}${C0.ESC}\\`); + break; + case 21: // GetWinTitle, returns OSC l label ST + this._coreService.triggerDataEvent(`${C0.ESC}]l${this._windowTitle}${C0.ESC}\\`); + break; + case 22: // PushTitle + if (!second || second === 2) { + this._windowTitleStack.push(this._windowTitle); + if (this._windowTitleStack.length > STACK_LIMIT) { + this._windowTitleStack.shift(); + } + } + if (!second || second === 1) { + this._iconNameStack.push(this._iconName); + if (this._iconNameStack.length > STACK_LIMIT) { + this._iconNameStack.shift(); } } break; - case 18: - const height = this._bufferService.rows; - const width = this._bufferService.cols; - this._coreService.triggerDataEvent(`${C0.ESC}[8;${height};${width}t`); + case 23: // PopTitle + if (!second || second === 2) { + if (this._windowTitleStack.length) { + this.setTitle(this._windowTitleStack.pop()); + } + } + if (!second || second === 1) { + if (this._iconNameStack.length) { + this.setIconName(this._iconNameStack.pop()); + } + } break; } } @@ -2148,14 +2171,22 @@ export class InputHandler extends Disposable implements IInputHandler { /** - * OSC 0; ST (set icon name + window title) * OSC 2; ST (set window title) - * Proxy to set window title. Icon name is not supported. + * Proxy to set window title. */ public setTitle(data: string): void { + this._windowTitle = data; this._terminal.handleTitle(data); } + /** + * OSC 1; ST + * Note: Icon name is not exposed. + */ + public setIconName(data: string): void { + this._iconName = data; + } + /** * ESC E * C1.NEL diff --git a/src/Terminal.ts b/src/Terminal.ts index 3e7e55ca..ffa34da8 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -131,8 +131,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public sendFocus: boolean; // misc - public savedCols: number; - public curAttrData: IAttributeData; private _eraseAttrData: IAttributeData; @@ -1463,7 +1461,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // Sync the scroll area to make sure scroll events don't fire and scroll the viewport to an // invalid location - this.viewport.syncScrollArea(true); + if (this.viewport) { + this.viewport.syncScrollArea(true); + } this.refresh(0, this.rows - 1); this._onResize.fire({ cols: x, rows: y }); diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index a70f751c..640a6c76 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -211,7 +211,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { wraparoundMode: boolean; bracketedPasteMode: boolean; curAttrData = new AttributeData(); - savedCols: number; x10Mouse: boolean; vt200Mouse: boolean; normalMouse: boolean; diff --git a/src/Types.d.ts b/src/Types.d.ts index be1019e5..faccb402 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -35,7 +35,6 @@ export interface IInputHandlingTerminal { wraparoundMode: boolean; bracketedPasteMode: boolean; curAttrData: IAttributeData; - savedCols: number; mouseEvents: CoreMouseEventType; sendFocus: boolean; cursorHidden: boolean; diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index d9ea60d5..3c3d4bea 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -14,6 +14,34 @@ import { clone } from 'common/Clone'; // made, apart from the conversion to base64. export const DEFAULT_BELL_SOUND = 'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjMyLjEwNAAAAAAAAAAAAAAA//tQxAADB8AhSmxhIIEVCSiJrDCQBTcu3UrAIwUdkRgQbFAZC1CQEwTJ9mjRvBA4UOLD8nKVOWfh+UlK3z/177OXrfOdKl7pyn3Xf//WreyTRUoAWgBgkOAGbZHBgG1OF6zM82DWbZaUmMBptgQhGjsyYqc9ae9XFz280948NMBWInljyzsNRFLPWdnZGWrddDsjK1unuSrVN9jJsK8KuQtQCtMBjCEtImISdNKJOopIpBFpNSMbIHCSRpRR5iakjTiyzLhchUUBwCgyKiweBv/7UsQbg8isVNoMPMjAAAA0gAAABEVFGmgqK////9bP/6XCykxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'; +// setting names controlled by allowedWindowOps +// not supported: GetChecksum, GetSelection, SetChecksum, SetSelection, SetXprop +export const WINDOW_OPTIONS: {[key: string]: number} = { + 'RestoreWin': 1, + 'MinimizeWin': 2, + 'SetWinPosition': 3, + 'SetWinSizePixels': 4, + 'RaiseWin': 5, + 'LowerWin': 6, + 'RefreshWin': 7, + 'SetWinSizeChars': 8, + 'MaximizeWin': 9, + 'FullscreenWin': 10, + 'GetWinState': 11, + 'GetWinPosition': 13, + 'GetWinSizePixels': 14, + 'GetScreenSizePixels': 15, // note: name not in xterm + 'GetCellSizePixels': 16, // note: name not in xterm + 'GetWinSizeChars': 18, + 'GetScreenSizeChars': 19, + 'GetIconTitle': 20, + 'GetWinTitle': 21, + 'PushTitle': 22, + 'PopTitle': 23, + 'SetWinLines': 24 // any param >= 24, also handles DECCOLM +}; + + // TODO: Freeze? export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ cols: 80, @@ -50,7 +78,8 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ screenKeys: false, cancelEvents: false, useFlowControl: false, - wordSeparator: ' ()[]{}\',:;"' + wordSeparator: ' ()[]{}\',:;"', + allowedWindowOps: [] }); /** @@ -128,6 +157,20 @@ export class OptionsService implements IOptionsService { throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`); } break; + case 'allowedWindowOps': + const values = (value as string).split(','); + const cleaned: number[] = []; + for (let i = 0; i < values.length; ++i) { + const option = parseInt(values[i].trim()) || WINDOW_OPTIONS[values[i].trim()]; + if (!option || option < 0 || option > 24) { + throw new Error(`unknown window option "${values[i]}"`); + } + if (!~cleaned.indexOf(option)) { + cleaned.push(option); + } + } + value = cleaned; + break; } return value; } @@ -136,6 +179,9 @@ export class OptionsService implements IOptionsService { if (!(key in DEFAULT_OPTIONS)) { throw new Error(`No option with key "${key}"`); } + if (key === 'allowedWindowOps') { + return this.options[key].join(); + } return this.options[key]; } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 0872d3db..a0a25bb8 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -201,6 +201,7 @@ export interface IPartialTerminalOptions { theme?: ITheme; windowsMode?: boolean; wordSeparator?: string; + allowedWindowOps?: number[]; } export interface ITerminalOptions { @@ -240,6 +241,7 @@ export interface ITerminalOptions { screenKeys: boolean; termName: string; useFlowControl: boolean; + allowedWindowOps: number[]; } export interface ITheme {