diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index e93fc945..553997cd 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -546,4 +546,343 @@ describe('InputHandler', () => { assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [5, 0, 0]); }); }); + describe('cursor positioning', () => { + let term: TestTerminal; + beforeEach(() => { + term = new TestTerminal({cols: 10, rows: 10}); + }); + function getCursor(term: TestTerminal): number[] { + return [ + term.buffer.x, + term.buffer.y + ]; + } + it('cursor forward (CUF)', () => { + term.writeSync('\x1b[C'); + assert.deepEqual(getCursor(term), [1, 0]); + term.writeSync('\x1b[1C'); + assert.deepEqual(getCursor(term), [2, 0]); + term.writeSync('\x1b[4C'); + assert.deepEqual(getCursor(term), [6, 0]); + term.writeSync('\x1b[100C'); + assert.deepEqual(getCursor(term), [9, 0]); + // should not change y + term.buffer.x = 8; + term.buffer.y = 4; + term.writeSync('\x1b[C'); + assert.deepEqual(getCursor(term), [9, 4]); + }); + it('cursor backward (CUB)', () => { + term.writeSync('\x1b[D'); + assert.deepEqual(getCursor(term), [0, 0]); + term.writeSync('\x1b[1D'); + assert.deepEqual(getCursor(term), [0, 0]); + // place cursor at end of first line + term.writeSync('\x1b[100C'); + term.writeSync('\x1b[D'); + assert.deepEqual(getCursor(term), [8, 0]); + term.writeSync('\x1b[1D'); + assert.deepEqual(getCursor(term), [7, 0]); + term.writeSync('\x1b[4D'); + assert.deepEqual(getCursor(term), [3, 0]); + term.writeSync('\x1b[100D'); + assert.deepEqual(getCursor(term), [0, 0]); + // should not change y + term.buffer.x = 4; + term.buffer.y = 4; + term.writeSync('\x1b[D'); + assert.deepEqual(getCursor(term), [3, 4]); + }); + it('cursor down (CUD)', () => { + term.writeSync('\x1b[B'); + assert.deepEqual(getCursor(term), [0, 1]); + term.writeSync('\x1b[1B'); + assert.deepEqual(getCursor(term), [0, 2]); + term.writeSync('\x1b[4B'); + assert.deepEqual(getCursor(term), [0, 6]); + term.writeSync('\x1b[100B'); + assert.deepEqual(getCursor(term), [0, 9]); + // should not change x + term.buffer.x = 8; + term.buffer.y = 0; + term.writeSync('\x1b[B'); + assert.deepEqual(getCursor(term), [8, 1]); + }); + it('cursor up (CUU)', () => { + term.writeSync('\x1b[A'); + assert.deepEqual(getCursor(term), [0, 0]); + term.writeSync('\x1b[1A'); + assert.deepEqual(getCursor(term), [0, 0]); + // place cursor at beginning of last row + term.writeSync('\x1b[100B'); + term.writeSync('\x1b[A'); + assert.deepEqual(getCursor(term), [0, 8]); + term.writeSync('\x1b[1A'); + assert.deepEqual(getCursor(term), [0, 7]); + term.writeSync('\x1b[4A'); + assert.deepEqual(getCursor(term), [0, 3]); + term.writeSync('\x1b[100A'); + assert.deepEqual(getCursor(term), [0, 0]); + // should not change x + term.buffer.x = 8; + term.buffer.y = 9; + term.writeSync('\x1b[A'); + assert.deepEqual(getCursor(term), [8, 8]); + }); + it('cursor next line (CNL)', () => { + term.writeSync('\x1b[E'); + assert.deepEqual(getCursor(term), [0, 1]); + term.writeSync('\x1b[1E'); + assert.deepEqual(getCursor(term), [0, 2]); + term.writeSync('\x1b[4E'); + assert.deepEqual(getCursor(term), [0, 6]); + term.writeSync('\x1b[100E'); + assert.deepEqual(getCursor(term), [0, 9]); + // should reset x to zero + term.buffer.x = 8; + term.buffer.y = 0; + term.writeSync('\x1b[E'); + assert.deepEqual(getCursor(term), [0, 1]); + }); + it('cursor previous line (CPL)', () => { + term.writeSync('\x1b[F'); + assert.deepEqual(getCursor(term), [0, 0]); + term.writeSync('\x1b[1F'); + assert.deepEqual(getCursor(term), [0, 0]); + // place cursor at beginning of last row + term.writeSync('\x1b[100E'); + term.writeSync('\x1b[F'); + assert.deepEqual(getCursor(term), [0, 8]); + term.writeSync('\x1b[1F'); + assert.deepEqual(getCursor(term), [0, 7]); + term.writeSync('\x1b[4F'); + assert.deepEqual(getCursor(term), [0, 3]); + term.writeSync('\x1b[100F'); + assert.deepEqual(getCursor(term), [0, 0]); + // should reset x to zero + term.buffer.x = 8; + term.buffer.y = 9; + term.writeSync('\x1b[F'); + assert.deepEqual(getCursor(term), [0, 8]); + }); + it('cursor character absolute (CHA)', () => { + term.writeSync('\x1b[G'); + assert.deepEqual(getCursor(term), [0, 0]); + term.writeSync('\x1b[1G'); + assert.deepEqual(getCursor(term), [0, 0]); + term.writeSync('\x1b[2G'); + assert.deepEqual(getCursor(term), [1, 0]); + term.writeSync('\x1b[5G'); + assert.deepEqual(getCursor(term), [4, 0]); + term.writeSync('\x1b[100G'); + assert.deepEqual(getCursor(term), [9, 0]); + }); + it('cursor position (CUP)', () => { + term.buffer.x = 5; + term.buffer.y = 5; + term.writeSync('\x1b[H'); + assert.deepEqual(getCursor(term), [0, 0]); + term.buffer.x = 5; + term.buffer.y = 5; + term.writeSync('\x1b[1H'); + assert.deepEqual(getCursor(term), [0, 0]); + term.buffer.x = 5; + term.buffer.y = 5; + term.writeSync('\x1b[1;1H'); + assert.deepEqual(getCursor(term), [0, 0]); + term.buffer.x = 5; + term.buffer.y = 5; + term.writeSync('\x1b[8H'); + assert.deepEqual(getCursor(term), [0, 7]); + term.buffer.x = 5; + term.buffer.y = 5; + term.writeSync('\x1b[;8H'); + assert.deepEqual(getCursor(term), [7, 0]); + term.buffer.x = 5; + term.buffer.y = 5; + term.writeSync('\x1b[100;100H'); + assert.deepEqual(getCursor(term), [9, 9]); + }); + it('horizontal position absolute (HPA)', () => { + term.writeSync('\x1b[`'); + assert.deepEqual(getCursor(term), [0, 0]); + term.writeSync('\x1b[1`'); + assert.deepEqual(getCursor(term), [0, 0]); + term.writeSync('\x1b[2`'); + assert.deepEqual(getCursor(term), [1, 0]); + term.writeSync('\x1b[5`'); + assert.deepEqual(getCursor(term), [4, 0]); + term.writeSync('\x1b[100`'); + assert.deepEqual(getCursor(term), [9, 0]); + }); + it('horizontal position relative (HPR)', () => { + term.writeSync('\x1b[a'); + assert.deepEqual(getCursor(term), [1, 0]); + term.writeSync('\x1b[1a'); + assert.deepEqual(getCursor(term), [2, 0]); + term.writeSync('\x1b[4a'); + assert.deepEqual(getCursor(term), [6, 0]); + term.writeSync('\x1b[100a'); + assert.deepEqual(getCursor(term), [9, 0]); + // should not change y + term.buffer.x = 8; + term.buffer.y = 4; + term.writeSync('\x1b[a'); + assert.deepEqual(getCursor(term), [9, 4]); + }); + it('vertical position absolute (VPA)', () => { + term.writeSync('\x1b[d'); + assert.deepEqual(getCursor(term), [0, 0]); + term.writeSync('\x1b[1d'); + assert.deepEqual(getCursor(term), [0, 0]); + term.writeSync('\x1b[2d'); + assert.deepEqual(getCursor(term), [0, 1]); + term.writeSync('\x1b[5d'); + assert.deepEqual(getCursor(term), [0, 4]); + term.writeSync('\x1b[100d'); + assert.deepEqual(getCursor(term), [0, 9]); + // should not change x + term.buffer.x = 8; + term.buffer.y = 4; + term.writeSync('\x1b[d'); + assert.deepEqual(getCursor(term), [8, 0]); + }); + it('vertical position relative (VPR)', () => { + term.writeSync('\x1b[e'); + assert.deepEqual(getCursor(term), [0, 1]); + term.writeSync('\x1b[1e'); + assert.deepEqual(getCursor(term), [0, 2]); + term.writeSync('\x1b[4e'); + assert.deepEqual(getCursor(term), [0, 6]); + term.writeSync('\x1b[100e'); + assert.deepEqual(getCursor(term), [0, 9]); + // should not change x + term.buffer.x = 8; + term.buffer.y = 4; + term.writeSync('\x1b[e'); + assert.deepEqual(getCursor(term), [8, 5]); + }); + describe('should clamp cursor into addressible range', () => { + it('CUF', () => { + term.buffer.x = 10000; + term.buffer.y = 10000; + term.writeSync('\x1b[C'); + assert.deepEqual(getCursor(term), [9, 9]); + term.buffer.x = -10000; + term.buffer.y = -10000; + term.writeSync('\x1b[C'); + assert.deepEqual(getCursor(term), [1, 0]); + }); + it('CUB', () => { + term.buffer.x = 10000; + term.buffer.y = 10000; + term.writeSync('\x1b[D'); + assert.deepEqual(getCursor(term), [8, 9]); + term.buffer.x = -10000; + term.buffer.y = -10000; + term.writeSync('\x1b[D'); + assert.deepEqual(getCursor(term), [0, 0]); + }); + it('CUD', () => { + term.buffer.x = 10000; + term.buffer.y = 10000; + term.writeSync('\x1b[B'); + assert.deepEqual(getCursor(term), [9, 9]); + term.buffer.x = -10000; + term.buffer.y = -10000; + term.writeSync('\x1b[B'); + assert.deepEqual(getCursor(term), [0, 1]); + }); + it('CUU', () => { + term.buffer.x = 10000; + term.buffer.y = 10000; + term.writeSync('\x1b[A'); + assert.deepEqual(getCursor(term), [9, 8]); + term.buffer.x = -10000; + term.buffer.y = -10000; + term.writeSync('\x1b[A'); + assert.deepEqual(getCursor(term), [0, 0]); + }); + it('CNL', () => { + term.buffer.x = 10000; + term.buffer.y = 10000; + term.writeSync('\x1b[E'); + assert.deepEqual(getCursor(term), [0, 9]); + term.buffer.x = -10000; + term.buffer.y = -10000; + term.writeSync('\x1b[E'); + assert.deepEqual(getCursor(term), [0, 1]); + }); + it('CPL', () => { + term.buffer.x = 10000; + term.buffer.y = 10000; + term.writeSync('\x1b[F'); + assert.deepEqual(getCursor(term), [0, 8]); + term.buffer.x = -10000; + term.buffer.y = -10000; + term.writeSync('\x1b[F'); + assert.deepEqual(getCursor(term), [0, 0]); + }); + it('CHA', () => { + term.buffer.x = 10000; + term.buffer.y = 10000; + term.writeSync('\x1b[5G'); + assert.deepEqual(getCursor(term), [4, 9]); + term.buffer.x = -10000; + term.buffer.y = -10000; + term.writeSync('\x1b[5G'); + assert.deepEqual(getCursor(term), [4, 0]); + }); + it('CUP', () => { + term.buffer.x = 10000; + term.buffer.y = 10000; + term.writeSync('\x1b[5;5H'); + assert.deepEqual(getCursor(term), [4, 4]); + term.buffer.x = -10000; + term.buffer.y = -10000; + term.writeSync('\x1b[5;5H'); + assert.deepEqual(getCursor(term), [4, 4]); + }); + it('HPA', () => { + term.buffer.x = 10000; + term.buffer.y = 10000; + term.writeSync('\x1b[5`'); + assert.deepEqual(getCursor(term), [4, 9]); + term.buffer.x = -10000; + term.buffer.y = -10000; + term.writeSync('\x1b[5`'); + assert.deepEqual(getCursor(term), [4, 0]); + }); + it('HPR', () => { + term.buffer.x = 10000; + term.buffer.y = 10000; + term.writeSync('\x1b[a'); + assert.deepEqual(getCursor(term), [9, 9]); + term.buffer.x = -10000; + term.buffer.y = -10000; + term.writeSync('\x1b[a'); + assert.deepEqual(getCursor(term), [1, 0]); + }); + it('VPA', () => { + term.buffer.x = 10000; + term.buffer.y = 10000; + term.writeSync('\x1b[5d'); + assert.deepEqual(getCursor(term), [9, 4]); + term.buffer.x = -10000; + term.buffer.y = -10000; + term.writeSync('\x1b[5d'); + assert.deepEqual(getCursor(term), [0, 4]); + }); + it('VPR', () => { + term.buffer.x = 10000; + term.buffer.y = 10000; + term.writeSync('\x1b[e'); + assert.deepEqual(getCursor(term), [9, 9]); + term.buffer.x = -10000; + term.buffer.y = -10000; + term.writeSync('\x1b[e'); + assert.deepEqual(getCursor(term), [0, 1]); + }); + }); + }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 18eb9fa6..ed7812f2 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -598,6 +598,22 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.updateRange(this._terminal.buffer.y); } + // restrict cursor changes to addressible cols/rows + private _restrictCursor(): void { + // cols + if (this._terminal.buffer.x < 0) { + this._terminal.buffer.x = 0; + } else if (this._terminal.buffer.x >= this._terminal.cols) { + this._terminal.buffer.x = this._terminal.cols - 1; + } + // rows + if (this._terminal.buffer.y < 0) { + this._terminal.buffer.y = 0; + } else if (this._terminal.buffer.y >= this._terminal.rows) { + this._terminal.buffer.y = this._terminal.rows - 1; + } + } + /** * CSI Ps A * Cursor Up Ps Times (default = 1) (CUU). @@ -607,10 +623,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (param < 1) { param = 1; } + this._restrictCursor(); this._terminal.buffer.y -= param; - if (this._terminal.buffer.y < 0) { - this._terminal.buffer.y = 0; - } + this._restrictCursor(); } /** @@ -622,14 +637,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (param < 1) { param = 1; } + this._restrictCursor(); this._terminal.buffer.y += param; - if (this._terminal.buffer.y >= this._terminal.rows) { - this._terminal.buffer.y = this._terminal.rows - 1; - } - // If the end of the line is hit, prevent this action from wrapping around to the next line. - if (this._terminal.buffer.x >= this._terminal.cols) { - this._terminal.buffer.x--; - } + this._restrictCursor(); } /** @@ -641,10 +651,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (param < 1) { param = 1; } + this._restrictCursor(); this._terminal.buffer.x += param; - if (this._terminal.buffer.x >= this._terminal.cols) { - this._terminal.buffer.x = this._terminal.cols - 1; - } + this._restrictCursor(); } /** @@ -656,49 +665,42 @@ export class InputHandler extends Disposable implements IInputHandler { if (param < 1) { param = 1; } - // If the end of the line is hit, prevent this action from wrapping around to the next line. - if (this._terminal.buffer.x >= this._terminal.cols) { - this._terminal.buffer.x--; - } + this._restrictCursor(); this._terminal.buffer.x -= param; - if (this._terminal.buffer.x < 0) { - this._terminal.buffer.x = 0; - } + this._restrictCursor(); } /** * CSI Ps E * Cursor Next Line Ps Times (default = 1) (CNL). - * same as CSI Ps B ? + * Other than cursorDown (CUD) also set the cursor to first column. */ public cursorNextLine(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; } + this._restrictCursor(); this._terminal.buffer.y += param; - if (this._terminal.buffer.y >= this._terminal.rows) { - this._terminal.buffer.y = this._terminal.rows - 1; - } this._terminal.buffer.x = 0; + this._restrictCursor(); } /** * CSI Ps F - * Cursor Preceding Line Ps Times (default = 1) (CNL). - * reuse CSI Ps A ? + * Cursor Previous Line Ps Times (default = 1) (CPL). + * Other than cursorUp (CUU) also set the cursor to first column. */ public cursorPrecedingLine(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; } + this._restrictCursor(); this._terminal.buffer.y -= param; - if (this._terminal.buffer.y < 0) { - this._terminal.buffer.y = 0; - } this._terminal.buffer.x = 0; + this._restrictCursor(); } @@ -711,7 +713,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (param < 1) { param = 1; } + this._restrictCursor(); this._terminal.buffer.x = param - 1; + this._restrictCursor(); } /** @@ -720,7 +724,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public cursorPosition(params: number[]): void { let col: number; - let row: number = params[0] - 1; + const row: number = params[0] - 1; if (params.length >= 2) { col = params[1] - 1; @@ -728,20 +732,10 @@ export class InputHandler extends Disposable implements IInputHandler { col = 0; } - if (row < 0) { - row = 0; - } else if (row >= this._terminal.rows) { - row = this._terminal.rows - 1; - } - - if (col < 0) { - col = 0; - } else if (col >= this._terminal.cols) { - col = this._terminal.cols - 1; - } - + this._restrictCursor(); this._terminal.buffer.x = col; this._terminal.buffer.y = row; + this._restrictCursor(); } /** @@ -1017,32 +1011,31 @@ export class InputHandler extends Disposable implements IInputHandler { /** * CSI Pm ` Character Position Absolute * [column] (default = [row,1]) (HPA). + * Currently same functionality as CHA. */ public charPosAbsolute(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; } + this._restrictCursor(); this._terminal.buffer.x = param - 1; - if (this._terminal.buffer.x >= this._terminal.cols) { - this._terminal.buffer.x = this._terminal.cols - 1; - } + this._restrictCursor(); } /** * CSI Pm a Character Position Relative * [columns] (default = [row,col+1]) (HPR) - * reuse CSI Ps C ? + * Currently same functionality as CUF. */ public hPositionRelative(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; } + this._restrictCursor(); this._terminal.buffer.x += param; - if (this._terminal.buffer.x >= this._terminal.cols) { - this._terminal.buffer.x = this._terminal.cols - 1; - } + this._restrictCursor(); } /** @@ -1155,10 +1148,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (param < 1) { param = 1; } + this._restrictCursor(); this._terminal.buffer.y = param - 1; - if (this._terminal.buffer.y >= this._terminal.rows) { - this._terminal.buffer.y = this._terminal.rows - 1; - } + this._restrictCursor(); } /** @@ -1171,37 +1163,19 @@ export class InputHandler extends Disposable implements IInputHandler { if (param < 1) { param = 1; } + this._restrictCursor(); this._terminal.buffer.y += param; - if (this._terminal.buffer.y >= this._terminal.rows) { - this._terminal.buffer.y = this._terminal.rows - 1; - } - // If the end of the line is hit, prevent this action from wrapping around to the next line. - if (this._terminal.buffer.x >= this._terminal.cols) { - this._terminal.buffer.x--; - } + this._restrictCursor(); } /** * CSI Ps ; Ps f * Horizontal and Vertical Position [row;column] (default = * [1,1]) (HVP). + * Same as CUP. */ public hVPosition(params: number[]): void { - if (params.length < 2) { - params.push(1); - } - if (params[0] < 1) params[0] = 1; - if (params[1] < 1) params[1] = 1; - - this._terminal.buffer.y = params[0] - 1; - if (this._terminal.buffer.y >= this._terminal.rows) { - this._terminal.buffer.y = this._terminal.rows - 1; - } - - this._terminal.buffer.x = params[1] - 1; - if (this._terminal.buffer.x >= this._terminal.cols) { - this._terminal.buffer.x = this._terminal.cols - 1; - } + this.cursorPosition(params); } /**