From a5894ee2344337442aafafa34ed9328a8fbf45f1 Mon Sep 17 00:00:00 2001 From: Jeff Smith Date: Sat, 29 Sep 2018 09:39:49 -0500 Subject: [PATCH] Fix cursor movement when swapping buffers --- src/BufferSet.test.ts | 26 ++++++++++++++++++++++ src/BufferSet.ts | 4 ++++ src/InputHandler.test.ts | 48 +++++++++++++++++++++++++++++++++++++++- src/InputHandler.ts | 37 ++++++++++++++++++++++--------- 4 files changed, 103 insertions(+), 12 deletions(-) diff --git a/src/BufferSet.test.ts b/src/BufferSet.test.ts index 009ebf2e..38f2ddab 100644 --- a/src/BufferSet.test.ts +++ b/src/BufferSet.test.ts @@ -48,4 +48,30 @@ describe('BufferSet', () => { assert.equal(bufferSet.active, bufferSet.alt); }); }); + + describe('cursor handling when swapping buffers', () => { + beforeEach(() => { + bufferSet.normal.x = 0; + bufferSet.normal.y = 0; + bufferSet.alt.x = 0; + bufferSet.alt.y = 0; + }); + + it('should keep the cursor stationary when activating alt buffer', () => { + bufferSet.activateNormalBuffer(); + bufferSet.active.x = 30; + bufferSet.active.y = 10; + bufferSet.activateAltBuffer(); + assert.equal(bufferSet.active.x, 30); + assert.equal(bufferSet.active.y, 10); + }); + it('should keep the cursor stationary when activating normal buffer', () => { + bufferSet.activateAltBuffer(); + bufferSet.active.x = 30; + bufferSet.active.y = 10; + bufferSet.activateNormalBuffer(); + assert.equal(bufferSet.active.x, 30); + assert.equal(bufferSet.active.y, 10); + }); + }); }); diff --git a/src/BufferSet.ts b/src/BufferSet.ts index c91ab751..b213db0a 100644 --- a/src/BufferSet.ts +++ b/src/BufferSet.ts @@ -61,6 +61,8 @@ export class BufferSet extends EventEmitter implements IBufferSet { if (this._activeBuffer === this._normal) { return; } + this._normal.x = this._alt.x; + this._normal.y = this._alt.y; // The alt buffer should always be cleared when we switch to the normal // buffer. This frees up memory since the alt buffer should always be new // when activated. @@ -82,6 +84,8 @@ export class BufferSet extends EventEmitter implements IBufferSet { // Since the alt buffer is always cleared when the normal buffer is // activated, we want to fill it when switching to it. this._alt.fillViewportRows(); + this._alt.x = this._normal.x; + this._alt.y = this._normal.y; this._activeBuffer = this._alt; this.emit('activate', { activeBuffer: this._alt, diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index aaaf57c3..cd9d7d0b 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -6,7 +6,7 @@ import { assert, expect } from 'chai'; import { InputHandler } from './InputHandler'; import { MockInputHandlingTerminal } from './utils/TestUtils.test'; -import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, CHAR_DATA_CHAR_INDEX } from './Buffer'; +import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, DEFAULT_ATTR } from './Buffer'; import { Terminal } from './Terminal'; import { IBufferLine } from './Types'; @@ -506,4 +506,50 @@ describe('InputHandler', () => { inputHandler.print(String.fromCharCode(0x200B), 0, 1); }); }); + + describe('alt screen', () => { + let term: Terminal; + let handler: InputHandler; + + function lineContent(line: IBufferLine): string { + let content = ''; + for (let i = 0; i < line.length; ++i) content += line.get(i)[CHAR_DATA_CHAR_INDEX]; + return content; + } + + beforeEach(() => { + term = new Terminal(); + handler = new InputHandler(term); + }); + it('should handle DECSET/DECRST 47 (alt screen buffer)', () => { + handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); + expect(lineContent(term.buffer.lines.get(0))).to.equal(Array(term.cols + 1).join(' ')); + expect(lineContent(term.buffer.lines.get(1))).to.equal(' TEST' + Array(term.cols - 7).join(' ')); + // Text color of 'TEST' should be red + expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); + }); + it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => { + handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST'); + expect(lineContent(term.buffer.lines.get(0))).to.equal(Array(term.cols + 1).join(' ')); + expect(lineContent(term.buffer.lines.get(1))).to.equal(' TEST' + Array(term.cols - 7).join(' ')); + // Text color of 'TEST' should be red + expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); + }); + it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => { + handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST'); + expect(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' ')); + expect(lineContent(term.buffer.lines.get(1))).to.equal('JUNK' + Array(term.cols - 3).join(' ')); + // Text color of 'TEST' should be default + expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR); + // Text color of 'JUNK' should be red + expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1); + }); + it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => { + handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST'); + expect(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' ')); + expect(lineContent(term.buffer.lines.get(1))).to.equal(Array(term.cols + 1).join(' ')); + // Text color of 'TEST' should be default + expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR); + }); + }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index a34590ef..3fa9b705 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -1280,7 +1280,9 @@ export class InputHandler extends Disposable implements IInputHandler { case 66: this._terminal.log('Serial port requested application keypad.'); this._terminal.applicationKeypad = true; - this._terminal.viewport.syncScrollArea(); + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } break; case 9: // X10 Mouse // no release, no motion, no wheel, no modifiers. @@ -1329,14 +1331,19 @@ export class InputHandler extends Disposable implements IInputHandler { case 25: // show cursor this._terminal.cursorHidden = false; break; + case 1048: // alt screen cursor + this.saveCursor(params); + break; case 1049: // alt screen buffer cursor - // TODO: Not sure if we need to save/restore after switching the buffer - // this.saveCursor(params); + this.saveCursor(params); // FALL-THROUGH case 47: // alt screen buffer case 1047: // alt screen buffer this._terminal.buffers.activateAltBuffer(); - this._terminal.viewport.syncScrollArea(); + this._terminal.refresh(0, this._terminal.rows - 1); + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } this._terminal.showCursor(); break; case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) @@ -1469,7 +1476,9 @@ export class InputHandler extends Disposable implements IInputHandler { case 66: this._terminal.log('Switching back to normal keypad.'); this._terminal.applicationKeypad = false; - this._terminal.viewport.syncScrollArea(); + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } break; case 9: // X10 Mouse case 1000: // vt200 mouse @@ -1497,18 +1506,22 @@ export class InputHandler extends Disposable implements IInputHandler { case 25: // hide cursor this._terminal.cursorHidden = true; break; + case 1048: // alt screen cursor + this.restoreCursor(params); + break; case 1049: // alt screen buffer cursor // FALL-THROUGH case 47: // normal screen buffer case 1047: // normal screen buffer - clearing it first // Ensure the selection manager has the correct buffer this._terminal.buffers.activateNormalBuffer(); - // TODO: Not sure if we need to save/restore after switching the buffer - // if (params[0] === 1049) { - // this.restoreCursor(params); - // } + if (params[0] === 1049) { + this.restoreCursor(params); + } this._terminal.refresh(0, this._terminal.rows - 1); - this._terminal.viewport.syncScrollArea(); + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } this._terminal.showCursor(); break; case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) @@ -1787,7 +1800,9 @@ export class InputHandler extends Disposable implements IInputHandler { this._terminal.originMode = false; this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false this._terminal.applicationKeypad = false; // ? - this._terminal.viewport.syncScrollArea(); + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } this._terminal.applicationCursor = false; this._terminal.buffer.scrollTop = 0; this._terminal.buffer.scrollBottom = this._terminal.rows - 1;