Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b3750be
early hack
jerch Aug 24, 2019
fb9019e
Merge branch 'master' into window_manipulation
jerch Sep 5, 2019
477d1b2
Merge branch 'master' into window_manipulation
Tyriar Sep 6, 2019
50b7fcb
Merge branch 'master' into window_manipulation
jerch Oct 4, 2019
d0689ab
Merge branch 'master' into window_manipulation
jerch Oct 24, 2019
38f05da
Merge branch 'master' into window_manipulation
jerch Oct 29, 2019
da3ae79
implement 14, 16, 18, 20, 21, 22, 23; restrict DECCOLM to 24
jerch Oct 29, 2019
2cad586
add integration tests for pixel reports
jerch Oct 29, 2019
98cc17d
remove leftover .only
jerch Oct 29, 2019
2797faa
Merge branch 'master' into window_manipulation
jerch Nov 12, 2019
d09e2fb
stub for window options
jerch Nov 13, 2019
8fc2d22
add IWindowOptions to xterm.d.ts
jerch Nov 14, 2019
eeb9270
change WindowOptions type
jerch Nov 14, 2019
26052a2
fix api test
jerch Nov 14, 2019
45d1139
windowOptions on ctor options
jerch Nov 14, 2019
8991ed8
simplify options
jerch Nov 14, 2019
7ad2a68
cleanup
jerch Nov 14, 2019
6cc6cb5
Merge branch 'master' into window_manipulation
jerch Nov 14, 2019
7f26b04
Merge branch 'master' into window_manipulation
jerch Nov 15, 2019
5a1e848
Merge branch 'master' into window_manipulation
jerch Nov 23, 2019
0be6d9f
Merge branch 'master' into window_manipulation
Tyriar Nov 26, 2019
be3f3da
Merge branch 'master' into window_manipulation
jerch Dec 7, 2019
5b95f7e
Merge branch 'window_manipulation' of github.com:jerch/xterm.js into …
jerch Dec 7, 2019
b5fdd3b
several fixes
jerch Dec 7, 2019
d7737f6
cleanup
jerch Dec 7, 2019
5120768
Merge branch 'master' into window_manipulation
jerch Dec 8, 2019
8460825
Merge branch 'master' into window_manipulation
jerch Dec 21, 2019
67bbbd8
Merge branch 'master' into window_manipulation
jerch Jan 5, 2020
f560d18
Merge branch 'master' into window_manipulation
jerch Jan 8, 2020
de9df12
Merge remote-tracking branch 'ups/master' into pr/jerch/2393
Tyriar Feb 3, 2020
479fddf
Merge branch 'master' into window_manipulation
Tyriar Feb 3, 2020
58047e8
Merge branch 'master' into window_manipulation
Tyriar Feb 4, 2020
51aab3e
Merge branch 'master' into window_manipulation
Tyriar Feb 4, 2020
fc5bf99
Merge branch 'master' into window_manipulation
Tyriar Feb 4, 2020
489d811
Move windowOptions into public API part
Tyriar Feb 4, 2020
3d2d35f
Pull render service off instantiation service safely
Tyriar Feb 4, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion demo/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@ function initOptions(term: TerminalType): void {
'termName',
'useFlowControl',
// Complex option
'theme'
'theme',
'windowOptions'
];
const stringOptions = {
bellSound: null,
Expand Down
116 changes: 116 additions & 0 deletions src/InputHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
185 changes: 169 additions & 16 deletions src/InputHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ 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 { IAttributeData, IDisposable, IWindowOptions } from 'common/Types';
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService } from 'common/services/Services';
import { OscHandler } from 'common/parser/OscParser';
import { DcsHandler } from 'common/parser/DcsParser';
Expand All @@ -32,6 +32,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
Expand Down Expand Up @@ -126,6 +164,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();
Expand Down Expand Up @@ -231,6 +273,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));
Expand All @@ -257,8 +300,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"
Expand Down Expand Up @@ -527,6 +571,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);
}

Expand Down Expand Up @@ -1425,11 +1478,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;
Expand Down Expand Up @@ -1601,14 +1659,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;
Expand Down Expand Up @@ -2037,6 +2096,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._terminal as any)._renderService; // FIXME: import renderService to get rid of any type here
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
Expand Down Expand Up @@ -2071,14 +2216,22 @@ export class InputHandler extends Disposable implements IInputHandler {


/**
* OSC 0; <data> ST (set icon name + window title)
* OSC 2; <data> 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; <data> ST
* Note: Icon name is not exposed.
*/
public setIconName(data: string): void {
this._iconName = data;
}

/**
* ESC E
* C1.NEL
Expand Down
Loading