diff --git a/demo/client.ts b/demo/client.ts index c07d008171..eb4e92a93a 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -250,7 +250,8 @@ function initOptions(term: TerminalType): void { 'convertEol', 'termName', // Complex option - 'theme' + 'theme', + 'windowOptions' ]; const stringOptions = { bellSound: null, diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index 1431d22deb..de43022b15 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -44,7 +44,7 @@ describe('InputHandler', () => { bufferService.buffer.x = 1; bufferService.buffer.y = 2; bufferService.buffer.ybase = 0; - const inputHandler = new TestInputHandler(terminal, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new TestInputHandler(terminal, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any); inputHandler.curAttrData.fg = 3; // Save cursor position inputHandler.saveCursor(); @@ -64,7 +64,7 @@ describe('InputHandler', () => { describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { const optionsService = new MockOptionsService(); - const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService(), {} as any); inputHandler.setCursorStyle(Params.fromArray([0])); assert.equal(optionsService.options['cursorStyle'], 'block'); @@ -105,7 +105,7 @@ describe('InputHandler', () => { it('should toggle Terminal.bracketedPasteMode', () => { const terminal = new MockInputHandlingTerminal(); terminal.bracketedPasteMode = false; - const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any); // Set bracketed paste mode inputHandler.setModePrivate(Params.fromArray([2004])); assert.equal(terminal.bracketedPasteMode, true); @@ -124,7 +124,7 @@ describe('InputHandler', () => { it('insertChars', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any); // insert some data in first and second line inputHandler.parse(Array(bufferService.cols - 9).join('a')); @@ -162,7 +162,7 @@ describe('InputHandler', () => { it('deleteChars', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any); // insert some data in first and second line inputHandler.parse(Array(bufferService.cols - 9).join('a')); @@ -203,7 +203,7 @@ describe('InputHandler', () => { it('eraseInLine', function(): void { const term = new Terminal(); const bufferService = new MockBufferService(80, 30); - const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any); // fill 6 lines to test 3 different states inputHandler.parse(Array(bufferService.cols + 1).join('a')); @@ -232,7 +232,7 @@ describe('InputHandler', () => { it('eraseInDisplay', function(): void { const term = new Terminal({cols: 80, rows: 7}); const bufferService = new MockBufferService(80, 7); - const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any); // fill display with a's for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); @@ -367,7 +367,7 @@ describe('InputHandler', () => { describe('print', () => { it('should not cause an infinite loop (regression test)', () => { const term = new Terminal(); - const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any); const container = new Uint32Array(10); container[0] = 0x200B; inputHandler.print(container, 0, 1); @@ -382,7 +382,7 @@ describe('InputHandler', () => { beforeEach(() => { term = new Terminal(); bufferService = new MockBufferService(80, 30); - handler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + handler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any); }); it('should handle DECSET/DECRST 47 (alt screen buffer)', () => { handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); @@ -1270,6 +1270,122 @@ describe('InputHandler', () => { [131072, 131072], [131072, 131072], [131072, 300000 - 131072 - 131072] ]); }); + describe('windowOptions', () => { + it('all should be disabled by default and not report', () => { + const term = new TestTerminal({cols: 10, rows: 10}); + 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, windowOptions: {getWinSizePixels: true}}); + 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, windowOptions: {getCellSizePixels: true}}); + 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, windowOptions: {getWinSizeChars: true}}); + 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('22/23 - PushTitle/PopTitle', () => { + const term = new TestTerminal({cols: 10, rows: 10, windowOptions: {pushTitle: true, popTitle: true}}); + 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, windowOptions: {pushTitle: true, popTitle: true}}); + 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, windowOptions: {pushTitle: true, popTitle: true}}); + 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, windowOptions: {setWinLines: true}}); + term2.writeSync('\x1b[?3l'); + assert.equal((term2 as any)._bufferService.cols, 80); + term2.writeSync('\x1b[?3h'); + assert.equal((term2 as any)._bufferService.cols, 132); + }); + }); describe('should correctly reset cells taken by wide chars', () => { let term: TestTerminal; beforeEach(() => { diff --git a/src/InputHandler.ts b/src/InputHandler.ts index b5b6e32f77..fb37f4ab1f 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -17,10 +17,11 @@ import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionId import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { AttributeData } from 'common/buffer/AttributeData'; -import { IAttributeData, IDisposable } from 'common/Types'; -import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService } from 'common/services/Services'; +import { IAttributeData, IDisposable, IWindowOptions } from 'common/Types'; +import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IInstantiationService } from 'common/services/Services'; import { OscHandler } from 'common/parser/OscParser'; import { DcsHandler } from 'common/parser/DcsParser'; +import { IRenderService } from 'browser/services/Services'; /** * Map collect to glevel. Used in `selectCharset`. @@ -56,6 +57,44 @@ 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; + +// map params to window option +function paramToWindowOption(n: number, opts: IWindowOptions): boolean { + if (n > 24) { + return opts.setWinLines || false; + } + switch (n) { + case 1: return !!opts.restoreWin; + case 2: return !!opts.minimizeWin; + case 3: return !!opts.setWinPosition; + case 4: return !!opts.setWinSizePixels; + case 5: return !!opts.raiseWin; + case 6: return !!opts.lowerWin; + case 7: return !!opts.refreshWin; + case 8: return !!opts.setWinSizeChars; + case 9: return !!opts.maximizeWin; + case 10: return !!opts.fullscreenWin; + case 11: return !!opts.getWinState; + case 13: return !!opts.getWinPosition; + case 14: return !!opts.getWinSizePixels; + case 15: return !!opts.getScreenSizePixels; + case 16: return !!opts.getCellSizePixels; + case 18: return !!opts.getWinSizeChars; + case 19: return !!opts.getScreenSizeChars; + case 20: return !!opts.getIconTitle; + case 21: return !!opts.getWinTitle; + case 22: return !!opts.pushTitle; + case 23: return !!opts.popTitle; + case 24: return !!opts.setWinLines; + } + return false; +} + + /** * DCS subparser implementations @@ -177,6 +216,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 _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone(); private _eraseAttrDataInternal: IAttributeData = DEFAULT_ATTR_DATA.clone(); @@ -204,6 +247,7 @@ export class InputHandler extends Disposable implements IInputHandler { private readonly _optionsService: IOptionsService, private readonly _coreMouseService: ICoreMouseService, private readonly _unicodeService: IUnicodeService, + private readonly _instantiationService: IInstantiationService, private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser()) { super(); @@ -282,6 +326,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.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)); @@ -308,8 +353,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" @@ -575,6 +621,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 (!paramToWindowOption(params.params[0], this._optionsService.options.windowOptions)) { + return true; + } + return callback(params); + }); + } return this._parser.addCsiHandler(id, callback); } @@ -1674,11 +1729,16 @@ export class InputHandler extends Disposable implements IInputHandler { this._charsetService.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._onRequestReset.fire(); + case 3: + /** + * DECCOLM - 132 column mode. + * This is only active if 'SetWinLines' (24) is enabled + * through `options.windowsOptions`. + */ + if (this._optionsService.options.windowOptions.setWinLines) { + this._terminal.resize(132, this._bufferService.rows); + this._onRequestReset.fire(); + } break; case 6: this._coreService.decPrivateModes.origin = true; @@ -1897,14 +1957,15 @@ export class InputHandler extends Disposable implements IInputHandler { this._coreService.decPrivateModes.applicationCursorKeys = false; break; case 3: - // 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 enabled + * through `options.windowsOptions`. + */ + if (this._optionsService.options.windowOptions.setWinLines) { + this._terminal.resize(80, this._bufferService.rows); + this._onRequestReset.fire(); } - delete this._terminal.savedCols; - this._onRequestReset.fire(); break; case 6: this._coreService.decPrivateModes.origin = false; @@ -2365,6 +2426,92 @@ export class InputHandler extends Disposable implements IInputHandler { } } + /** + * CSI Ps ; Ps ; Ps t - Various window manipulations and reports (xterm) + * + * 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 windowOptions(params: IParams): void { + if (!paramToWindowOption(params.params[0], this._optionsService.options.windowOptions)) { + return; + } + const second = (params.length > 1) ? params.params[1] : 0; + const rs = this._instantiationService.getService(IRenderService); + switch (params.params[0]) { + case 14: // GetWinSizePixels, returns CSI 4 ; height ; width t + if (rs && second !== 2) { + console.log(rs.dimensions); + const w = rs.dimensions.scaledCanvasWidth.toFixed(0); + const h = rs.dimensions.scaledCanvasHeight.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.scaledCellWidth.toFixed(0); + const h = rs.dimensions.scaledCellHeight.toFixed(0); + this._coreService.triggerDataEvent(`${C0.ESC}[6;${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 22: // PushTitle + if (second === 0 || second === 2) { + this._windowTitleStack.push(this._windowTitle); + if (this._windowTitleStack.length > STACK_LIMIT) { + this._windowTitleStack.shift(); + } + } + if (second === 0 || second === 1) { + this._iconNameStack.push(this._iconName); + if (this._iconNameStack.length > STACK_LIMIT) { + this._iconNameStack.shift(); + } + } + break; + case 23: // PopTitle + if (second === 0 || second === 2) { + if (this._windowTitleStack.length) { + this.setTitle(this._windowTitleStack.pop()); + } + } + if (second === 0 || second === 1) { + if (this._iconNameStack.length) { + this.setIconName(this._iconNameStack.pop()); + } + } + break; + } + } + /** * CSI s @@ -2405,9 +2552,8 @@ 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. * * @vt: #P[Icon name is not exposed.] OSC 0 "Set Windows Title and Icon Name" "OSC 0 ; Pt BEL" "Set window title and icon name." * Icon name is not supported. For Window Title see below. @@ -2416,9 +2562,18 @@ export class InputHandler extends Disposable implements IInputHandler { * xterm.js does not manipulate the title directly, instead exposes changes via the event `Terminal.onTitleChange`. */ 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 6ee3cbe516..1bcac1f056 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -116,9 +116,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public mouseEvents: CoreMouseEventType = CoreMouseEventType.NONE; public sendFocus: boolean; - // misc - public savedCols: number; - // write buffer private _writeBuffer: WriteBuffer; @@ -248,7 +245,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._inputHandler.reset(); } else { // Register input handler and refire/handle events - this._inputHandler = new InputHandler(this, this._bufferService, this._charsetService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService, this.unicodeService); + this._inputHandler = new InputHandler(this, this._bufferService, this._charsetService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService, this.unicodeService, this._instantiationService); this._inputHandler.onRequestBell(() => this.bell()); this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)); this._inputHandler.onRequestReset(() => this.reset()); @@ -1392,7 +1389,7 @@ 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); + 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 c6e193a736..c96cd05be0 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -201,7 +201,6 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { onA11yTabEmitter: EventEmitter; insertMode: boolean; bracketedPasteMode: boolean; - savedCols: number; sendFocus: boolean; buffers: IBufferSet; buffer: IBuffer = new MockBuffer(); diff --git a/src/Types.d.ts b/src/Types.d.ts index 0f3a7751f3..e304ddc183 100644 --- a/src/Types.d.ts +++ b/src/Types.d.ts @@ -23,7 +23,6 @@ export type LineData = CharData[]; export interface IInputHandlingTerminal { insertMode: boolean; bracketedPasteMode: boolean; - savedCols: number; sendFocus: boolean; buffers: IBufferSet; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index fdb5eb739d..8aaa06f2a1 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -257,3 +257,31 @@ export interface ICoreMouseProtocol { * (DEFAULT only supports up to 223 1-based as coord value). */ export type CoreMouseEncoding = (event: ICoreMouseEvent) => string; + +/** + * windowOptions + */ +export interface IWindowOptions { + restoreWin?: boolean; + minimizeWin?: boolean; + setWinPosition?: boolean; + setWinSizePixels?: boolean; + raiseWin?: boolean; + lowerWin?: boolean; + refreshWin?: boolean; + setWinSizeChars?: boolean; + maximizeWin?: boolean; + fullscreenWin?: boolean; + getWinState?: boolean; + getWinPosition?: boolean; + getWinSizePixels?: boolean; + getScreenSizePixels?: boolean; + getCellSizePixels?: boolean; + getWinSizeChars?: boolean; + getScreenSizeChars?: boolean; + getIconTitle?: boolean; + getWinTitle?: boolean; + pushTitle?: boolean; + popTitle?: boolean; + setWinLines?: boolean; +} diff --git a/src/common/services/InstantiationService.ts b/src/common/services/InstantiationService.ts index abce8ac54e..ade3bbe3b4 100644 --- a/src/common/services/InstantiationService.ts +++ b/src/common/services/InstantiationService.ts @@ -36,7 +36,7 @@ export class ServiceCollection { return this._entries.has(id); } - get(id: IServiceIdentifier): T { + get(id: IServiceIdentifier): T | undefined { return this._entries.get(id); } } @@ -52,6 +52,10 @@ export class InstantiationService implements IInstantiationService { this._services.set(id, instance); } + public getService(id: IServiceIdentifier): T | undefined { + return this._services.get(id); + } + public createInstance(ctor: any, ...args: any[]): any { const serviceDependencies = getServiceDependencies(ctor).sort((a, b) => a.index - b.index); diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 4e0b5e8b71..44a55d33da 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -45,12 +45,13 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({ theme: {}, rightClickSelectsWord: isMac, rendererType: 'canvas', + windowOptions: {}, windowsMode: false, + wordSeparator: ' ()[]{}\',"`', convertEol: false, termName: 'xterm', - cancelEvents: false, - wordSeparator: ' ()[]{}\',"`' + cancelEvents: false }); /** diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index fd11b92768..23b74e1444 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; export const IBufferService = createDecorator('BufferService'); @@ -171,6 +171,7 @@ export interface IConstructorSignature8 { export const IInstantiationService = createDecorator('InstantiationService'); export interface IInstantiationService { setService(id: IServiceIdentifier, instance: T): void; + getService(id: IServiceIdentifier): T | undefined; createInstance(ctor: IConstructorSignature0): T; createInstance(ctor: IConstructorSignature1, first: A1): T; @@ -240,6 +241,7 @@ export interface IPartialTerminalOptions { theme?: ITheme; windowsMode?: boolean; wordSeparator?: string; + windowOptions?: IWindowOptions; } export interface ITerminalOptions { @@ -273,6 +275,7 @@ export interface ITerminalOptions { tabStopWidth: number; theme: ITheme; windowsMode: boolean; + windowOptions: IWindowOptions; wordSeparator: string; [key: string]: any; diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index 968a9e0bbb..ad30a51c11 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -345,6 +345,44 @@ describe('InputHandler Integration Tests', function(): void { `); await pollFor(page, () => getLinesAsArray(3), ['#', ' #', 'abcd####']); }); + + describe('Window Options - CSI Ps ; Ps ; Ps t', () => { + it('should be disabled by default', async function() { + await page.evaluate(`(() => { + window._stack = []; + const _h = window.term.onData(data => window._stack.push(data)); + window.term.write('\x1b[14t'); + window.term.write('\x1b[16t'); + window.term.write('\x1b[18t'); + window.term.write('\x1b[20t'); + window.term.write('\x1b[21t'); + return new Promise((r) => window.term.write('', () => { _h.dispose(); r(); })); + })()`); + await pollFor(page, async () => await page.evaluate(`(() => _stack)()`), []); + }); + it('14 - GetWinSizePixels', async function() { + await page.evaluate(`window.term.setOption('windowOptions', {getWinSizePixels: true});`); + await page.evaluate(`(() => { + window._stack = []; + const _h = window.term.onData(data => window._stack.push(data)); + window.term.write('\x1b[14t'); + return new Promise((r) => window.term.write('', () => { _h.dispose(); r(); })); + })()`); + const d = await getDimensions(); + await pollFor(page, async () => await page.evaluate(`(() => _stack)()`), [`\x1b[4;${d.height};${d.width}t`]); + }); + it('16 - GetCellSizePixels', async function() { + await page.evaluate(`window.term.setOption('windowOptions', {getCellSizePixels: true});`); + await page.evaluate(`(() => { + window._stack = []; + const _h = window.term.onData(data => window._stack.push(data)); + window.term.write('\x1b[16t'); + return new Promise((r) => window.term.write('', () => { _h.dispose(); r(); })); + })()`); + const d = await getDimensions(); + await pollFor(page, async () => await page.evaluate(`(() => _stack)()`), [`\x1b[6;${d.cellHeight};${d.cellWidth}t`]); + }); + }); }); describe('ESC', () => { @@ -403,3 +441,13 @@ async function getCursor(): Promise<{col: number, row: number}> { })(); `); } + +async function getDimensions(): Promise { + const dim = await page.evaluate(`term._core._renderService.dimensions`); + return { + cellWidth: dim.actualCellWidth.toFixed(0), + cellHeight: dim.actualCellHeight.toFixed(0), + width: dim.canvasWidth.toFixed(0), + height: dim.canvasHeight.toFixed(0) + }; +} diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index b8caa06587..fc2549bc7f 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -232,6 +232,12 @@ declare module 'xterm' { * double click to select work logic. */ wordSeparator?: string; + + /** + * Enable various window manipulation and report features. + * All features are disabled by default for security reasons. + */ + windowOptions?: IWindowOptions; } /** @@ -379,6 +385,162 @@ declare module 'xterm' { tooMuchOutput: string; } + /** + * Enable various window manipulation and report features (CSI Ps ; Ps ; Ps t). + * + * Most settings have no default implementation, as they heavily rely on + * the embedding environment. + * + * To implement a feature, create a custom CSI hook like this: + * ```ts + * term.parser.addCsiHandler({final: 't'}, params => { + * const ps = params[0]; + * switch (ps) { + * case XY: + * ... // your implementation for option XY + * return true; // signal Ps=XY was handled + * } + * return false; // any Ps that was not handled + * }); + * ``` + * + * Note on security: + * Most features are meant to deal with some information of the host machine + * where the terminal runs on. This is seen as a security risk possibly leaking + * sensitive data of the host to the program in the terminal. Therefore all options + * (even those without a default implementation) are guarded by the boolean flag + * and disabled by default. + */ + export interface IWindowOptions { + /** + * Ps=1 De-iconify window. + * No default implementation. + */ + restoreWin?: boolean; + /** + * Ps=2 Iconify window. + * No default implementation. + */ + minimizeWin?: boolean; + /** + * Ps=3 ; x ; y + * Move window to [x, y]. + * No default implementation. + */ + setWinPosition?: boolean; + /** + * Ps = 4 ; height ; width + * Resize the window to given `height` and `width` in pixels. + * Omitted parameters should reuse the current height or width. + * Zero parameters should use the display's height or width. + * No default implementation. + */ + setWinSizePixels?: boolean; + /** + * Ps=5 Raise the window to the front of the stacking order. + * No default implementation. + */ + raiseWin?: boolean; + /** + * Ps=6 Lower the xterm window to the bottom of the stacking order. + * No default implementation. + */ + lowerWin?: boolean; + /** Ps=7 Refresh the window. */ + refreshWin?: boolean; + /** + * Ps = 8 ; height ; width + * Resize the text area to given height and width in characters. + * Omitted parameters should reuse the current height or width. + * Zero parameters use the display's height or width. + * No default implementation. + */ + setWinSizeChars?: boolean; + /** + * Ps=9 ; 0 Restore maximized window. + * Ps=9 ; 1 Maximize window (i.e., resize to screen size). + * Ps=9 ; 2 Maximize window vertically. + * Ps=9 ; 3 Maximize window horizontally. + * No default implementation. + */ + maximizeWin?: boolean; + /** + * Ps=10 ; 0 Undo full-screen mode. + * Ps=10 ; 1 Change to full-screen. + * Ps=10 ; 2 Toggle full-screen. + * No default implementation. + */ + fullscreenWin?: boolean; + /** Ps=11 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". + * No default implementation. + */ + getWinState?: boolean; + /** + * Ps=13 Report xterm window position. Result is "CSI 3 ; x ; y t". + * Ps=13 ; 2 Report xterm text-area position. Result is "CSI 3 ; x ; y t". + * No default implementation. + */ + getWinPosition?: boolean; + /** + * Ps=14 Report xterm text area size in pixels. Result is "CSI 4 ; height ; width t". + * Ps=14 ; 2 Report xterm window size in pixels. Result is "CSI 4 ; height ; width t". + * Has a default implementation. + */ + getWinSizePixels?: boolean; + /** + * Ps=15 Report size of the screen in pixels. Result is "CSI 5 ; height ; width t". + * No default implementation. + */ + getScreenSizePixels?: boolean; + /** + * Ps=16 Report xterm character cell size in pixels. Result is "CSI 6 ; height ; width t". + * Has a default implementation. + */ + getCellSizePixels?: boolean; + /** + * Ps=18 Report the size of the text area in characters. Result is "CSI 8 ; height ; width t". + * Has a default implementation. + */ + getWinSizeChars?: boolean; + /** + * Ps=19 Report the size of the screen in characters. Result is "CSI 9 ; height ; width t". + * No default implementation. + */ + getScreenSizeChars?: boolean; + /** + * Ps=20 Report xterm window's icon label. Result is "OSC L label ST". + * No default implementation. + */ + getIconTitle?: boolean; + /** + * Ps=21 Report xterm window's title. Result is "OSC l label ST". + * No default implementation. + */ + getWinTitle?: boolean; + /** + * Ps=22 ; 0 Save xterm icon and window title on stack. + * Ps=22 ; 1 Save xterm icon title on stack. + * Ps=22 ; 2 Save xterm window title on stack. + * All variants have a default implementation. + */ + pushTitle?: boolean; + /** + * Ps=23 ; 0 Restore xterm icon and window title from stack. + * Ps=23 ; 1 Restore xterm icon title from stack. + * Ps=23 ; 2 Restore xterm window title from stack. + * All variants have a default implementation. + */ + popTitle?: boolean; + /** + * Ps>=24 Resize to Ps lines (DECSLPP). + * DECSLPP is not implemented. This settings is also used to + * enable / disable DECCOLM (earlier variant of DECSLPP). + */ + setWinLines?: boolean; + } + /** * The class that represents an xterm.js terminal. */