diff --git a/.eslintrc.json b/.eslintrc.json index 41767bed..32f8e992 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,7 +9,6 @@ "project": [ "src/browser/tsconfig.json", "src/common/tsconfig.json", - "src/tsconfig.json", "test/api/tsconfig.json", "test/benchmark/tsconfig.json", "addons/xterm-addon-attach/src/tsconfig.json", diff --git a/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts b/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts index fbc16e03..c1764d8e 100644 --- a/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts +++ b/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts @@ -7,7 +7,7 @@ import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark'; import { spawn } from 'node-pty'; import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder'; -import { Terminal } from 'public/Terminal'; +import { Terminal } from 'browser/public/Terminal'; import { SerializeAddon } from 'SerializeAddon'; class TestTerminal extends Terminal { diff --git a/addons/xterm-addon-serialize/benchmark/tsconfig.json b/addons/xterm-addon-serialize/benchmark/tsconfig.json index 4e62ed59..42aaaa1b 100644 --- a/addons/xterm-addon-serialize/benchmark/tsconfig.json +++ b/addons/xterm-addon-serialize/benchmark/tsconfig.json @@ -11,8 +11,6 @@ "paths": { "common/*": ["../../../src/common/*"], "browser/*": ["../../../src/browser/*"], - "public/*": ["../../../src/public/*"], - "Terminal": ["../../../src/Terminal"], "SerializeAddon": ["../src/SerializeAddon"] } }, diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts index daf7f6fc..7165edb3 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminalOptions } from '../../../src/Types'; +import { ITerminalOptions } from '../../../src/common/Types'; import { ITheme } from 'xterm'; import { assert } from 'chai'; import { openTerminal, pollFor, writeSync, getBrowserType } from '../../../out-test/api/TestUtils'; diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 0333fd20..ac997c96 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -3,7 +3,6 @@ * @license MIT */ -import { ITerminal } from '../../../src/Types'; import { GlyphRenderer } from './GlyphRenderer'; import { LinkRenderLayer } from './renderLayer/LinkRenderLayer'; import { CursorRenderLayer } from './renderLayer/CursorRenderLayer'; @@ -17,7 +16,7 @@ import { NULL_CELL_CODE } from 'common/buffer/Constants'; import { Terminal, IEvent } from 'xterm'; import { IRenderLayer } from './renderLayer/Types'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; -import { IColorSet } from 'browser/Types'; +import { ITerminal, IColorSet } from 'browser/Types'; import { EventEmitter } from 'common/EventEmitter'; import { CellData } from 'common/buffer/CellData'; @@ -52,8 +51,8 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core = (this._terminal as any)._core; this._renderLayers = [ - new LinkRenderLayer(this._core.screenElement, 2, this._colors, this._core), - new CursorRenderLayer(this._core.screenElement, 3, this._colors, this._onRequestRedraw) + new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core), + new CursorRenderLayer(this._core.screenElement!, 3, this._colors, this._onRequestRedraw) ]; this.dimensions = { scaledCharWidth: 0, @@ -83,7 +82,7 @@ export class WebglRenderer extends Disposable implements IRenderer { if (!this._gl) { throw new Error('WebGL2 not supported ' + this._gl); } - this._core.screenElement.appendChild(this._canvas); + this._core.screenElement!.appendChild(this._canvas); this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions); this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions); @@ -91,12 +90,12 @@ export class WebglRenderer extends Disposable implements IRenderer { // Update dimensions and acquire char atlas this.onCharSizeChanged(); - this._isAttached = document.body.contains(this._core.screenElement); + this._isAttached = document.body.contains(this._core.screenElement!); } public dispose(): void { this._renderLayers.forEach(l => l.dispose()); - this._core.screenElement.removeChild(this._canvas); + this._core.screenElement!.removeChild(this._canvas); super.dispose(); } @@ -150,8 +149,8 @@ export class WebglRenderer extends Disposable implements IRenderer { this._canvas.style.height = `${this.dimensions.canvasHeight}px`; // Resize the screen - this._core.screenElement.style.width = `${this.dimensions.canvasWidth}px`; - this._core.screenElement.style.height = `${this.dimensions.canvasHeight}px`; + this._core.screenElement!.style.width = `${this.dimensions.canvasWidth}px`; + this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`; this._glyphRenderer.setDimensions(this.dimensions); this._glyphRenderer.onResize(); @@ -229,7 +228,7 @@ export class WebglRenderer extends Disposable implements IRenderer { public renderRows(start: number, end: number): void { if (!this._isAttached) { - if (document.body.contains(this._core.screenElement) && (this._core as any)._charSizeService.width && (this._core as any)._charSizeService.height) { + if (document.body.contains(this._core.screenElement!) && (this._core as any)._charSizeService.width && (this._core as any)._charSizeService.height) { this._updateDimensions(); this._refreshCharAtlas(); this._isAttached = true; diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts index 10cf9c1f..8f6b3e6d 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -3,18 +3,17 @@ * @license MIT */ -import { ILinkifierAccessor } from '../../../../src/Types'; import { Terminal } from 'xterm'; import { BaseRenderLayer } from './BaseRenderLayer'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { is256Color } from '../atlas/CharAtlasUtils'; -import { IColorSet, ILinkifierEvent } from 'browser/Types'; +import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; export class LinkRenderLayer extends BaseRenderLayer { private _state: ILinkifierEvent | undefined; - constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) { + constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ITerminal) { super(container, 'link', zIndex, true, colors); terminal.linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); terminal.linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); diff --git a/bin/test.js b/bin/test.js index 1380a512..b9725acd 100644 --- a/bin/test.js +++ b/bin/test.js @@ -13,7 +13,6 @@ const env = { ...process.env }; env.NODE_PATH = path.resolve(__dirname, '../out'); let testFiles = [ - './out/*test.js', './out/**/*test.js', './addons/**/out/*test.js', ]; diff --git a/demo/client.ts b/demo/client.ts index b009a534..eaa6f8c1 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -8,7 +8,7 @@ /// // Use tsc version (yarn watch) -import { Terminal } from '../out/public/Terminal'; +import { Terminal } from '../out/browser/public/Terminal'; import { AttachAddon } from '../addons/xterm-addon-attach/out/AttachAddon'; import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon'; import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon'; diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts deleted file mode 100644 index 84d4dae5..00000000 --- a/src/InputHandler.test.ts +++ /dev/null @@ -1,1497 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert, expect } from 'chai'; -import { InputHandler } from './InputHandler'; -import { MockInputHandlingTerminal, TestTerminal } from './TestUtils.test'; -import { Terminal } from './Terminal'; -import { IBufferLine, IAttributeData } from 'common/Types'; -import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; -import { CellData } from 'common/buffer/CellData'; -import { Attributes } from 'common/buffer/Constants'; -import { AttributeData } from 'common/buffer/AttributeData'; -import { Params } from 'common/parser/Params'; -import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService } from 'common/TestUtils.test'; -import { IBufferService } from 'common/services/Services'; -import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; -import { clone } from 'common/Clone'; - -function getCursor(term: TestTerminal): number[] { - return [ - term.buffer.x, - term.buffer.y - ]; -} - -function getLines(term: TestTerminal, limit: number = term.rows): string[] { - const res: string[] = []; - for (let i = 0; i < limit; ++i) { - res.push(term.buffer.lines.get(i).translateToString(true)); - } - return res; -} - -class TestInputHandler extends InputHandler { - public get curAttrData(): IAttributeData { return (this as any)._curAttrData; } -} - -describe('InputHandler', () => { - describe('save and restore cursor', () => { - const terminal = new MockInputHandlingTerminal(); - const bufferService = new MockBufferService(80, 30); - 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(), {} as any); - inputHandler.curAttrData.fg = 3; - // Save cursor position - inputHandler.saveCursor(); - assert.equal(bufferService.buffer.x, 1); - assert.equal(bufferService.buffer.y, 2); - assert.equal(inputHandler.curAttrData.fg, 3); - // Change cursor position - bufferService.buffer.x = 10; - bufferService.buffer.y = 20; - inputHandler.curAttrData.fg = 30; - // Restore cursor position - inputHandler.restoreCursor(); - assert.equal(bufferService.buffer.x, 1); - assert.equal(bufferService.buffer.y, 2); - assert.equal(inputHandler.curAttrData.fg, 3); - }); - 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(), {} as any); - - inputHandler.setCursorStyle(Params.fromArray([0])); - assert.equal(optionsService.options['cursorStyle'], 'block'); - assert.equal(optionsService.options['cursorBlink'], true); - - optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([1])); - assert.equal(optionsService.options['cursorStyle'], 'block'); - assert.equal(optionsService.options['cursorBlink'], true); - - optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([2])); - assert.equal(optionsService.options['cursorStyle'], 'block'); - assert.equal(optionsService.options['cursorBlink'], false); - - optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([3])); - assert.equal(optionsService.options['cursorStyle'], 'underline'); - assert.equal(optionsService.options['cursorBlink'], true); - - optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([4])); - assert.equal(optionsService.options['cursorStyle'], 'underline'); - assert.equal(optionsService.options['cursorBlink'], false); - - optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([5])); - assert.equal(optionsService.options['cursorStyle'], 'bar'); - assert.equal(optionsService.options['cursorBlink'], true); - - optionsService.options = clone(DEFAULT_OPTIONS); - inputHandler.setCursorStyle(Params.fromArray([6])); - assert.equal(optionsService.options['cursorStyle'], 'bar'); - assert.equal(optionsService.options['cursorBlink'], false); - }); - }); - describe('setMode', () => { - 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(), {} as any); - // Set bracketed paste mode - inputHandler.setModePrivate(Params.fromArray([2004])); - assert.equal(terminal.bracketedPasteMode, true); - // Reset bracketed paste mode - inputHandler.resetModePrivate(Params.fromArray([2004])); - assert.equal(terminal.bracketedPasteMode, false); - }); - }); - describe('regression tests', function(): void { - function termContent(bufferService: IBufferService, trim: boolean): string[] { - const result = []; - for (let i = 0; i < bufferService.rows; ++i) result.push(bufferService.buffer.lines.get(i).translateToString(trim)); - return result; - } - - 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(), {} as any); - - // insert some data in first and second line - inputHandler.parse(Array(bufferService.cols - 9).join('a')); - inputHandler.parse('1234567890'); - inputHandler.parse(Array(bufferService.cols - 9).join('a')); - inputHandler.parse('1234567890'); - const line1: IBufferLine = bufferService.buffer.lines.get(0); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '1234567890'); - - // insert one char from params = [0] - bufferService.buffer.y = 0; - bufferService.buffer.x = 70; - inputHandler.insertChars(Params.fromArray([0])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' 123456789'); - - // insert one char from params = [1] - bufferService.buffer.y = 0; - bufferService.buffer.x = 70; - inputHandler.insertChars(Params.fromArray([1])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' 12345678'); - - // insert two chars from params = [2] - bufferService.buffer.y = 0; - bufferService.buffer.x = 70; - inputHandler.insertChars(Params.fromArray([2])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' 123456'); - - // insert 10 chars from params = [10] - bufferService.buffer.y = 0; - bufferService.buffer.x = 70; - inputHandler.insertChars(Params.fromArray([10])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' '); - expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a')); - }); - 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(), {} as any); - - // insert some data in first and second line - inputHandler.parse(Array(bufferService.cols - 9).join('a')); - inputHandler.parse('1234567890'); - inputHandler.parse(Array(bufferService.cols - 9).join('a')); - inputHandler.parse('1234567890'); - const line1: IBufferLine = bufferService.buffer.lines.get(0); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '1234567890'); - - // delete one char from params = [0] - bufferService.buffer.y = 0; - bufferService.buffer.x = 70; - inputHandler.deleteChars(Params.fromArray([0])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '234567890 '); - expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a') + '234567890'); - - // insert one char from params = [1] - bufferService.buffer.y = 0; - bufferService.buffer.x = 70; - inputHandler.deleteChars(Params.fromArray([1])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '34567890 '); - expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a') + '34567890'); - - // insert two chars from params = [2] - bufferService.buffer.y = 0; - bufferService.buffer.x = 70; - inputHandler.deleteChars(Params.fromArray([2])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '567890 '); - expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a') + '567890'); - - // insert 10 chars from params = [10] - bufferService.buffer.y = 0; - bufferService.buffer.x = 70; - inputHandler.deleteChars(Params.fromArray([10])); - expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' '); - expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a')); - }); - 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(), {} as any); - - // fill 6 lines to test 3 different states - inputHandler.parse(Array(bufferService.cols + 1).join('a')); - inputHandler.parse(Array(bufferService.cols + 1).join('a')); - inputHandler.parse(Array(bufferService.cols + 1).join('a')); - - // params[0] - right erase - bufferService.buffer.y = 0; - bufferService.buffer.x = 70; - inputHandler.eraseInLine(Params.fromArray([0])); - expect(bufferService.buffer.lines.get(0).translateToString(false)).equals(Array(71).join('a') + ' '); - - // params[1] - left erase - bufferService.buffer.y = 1; - bufferService.buffer.x = 70; - inputHandler.eraseInLine(Params.fromArray([1])); - expect(bufferService.buffer.lines.get(1).translateToString(false)).equals(Array(71).join(' ') + ' aaaaaaaaa'); - - // params[1] - left erase - bufferService.buffer.y = 2; - bufferService.buffer.x = 70; - inputHandler.eraseInLine(Params.fromArray([2])); - expect(bufferService.buffer.lines.get(2).translateToString(false)).equals(Array(bufferService.cols + 1).join(' ')); - - }); - 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(), {} as any); - - // fill display with a's - for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); - - // params [0] - right and below erase - bufferService.buffer.y = 5; - bufferService.buffer.x = 40; - inputHandler.eraseInDisplay(Params.fromArray([0])); - expect(termContent(bufferService, false)).eql([ - Array(bufferService.cols + 1).join('a'), - Array(bufferService.cols + 1).join('a'), - Array(bufferService.cols + 1).join('a'), - Array(bufferService.cols + 1).join('a'), - Array(bufferService.cols + 1).join('a'), - Array(40 + 1).join('a') + Array(bufferService.cols - 40 + 1).join(' '), - Array(bufferService.cols + 1).join(' ') - ]); - expect(termContent(bufferService, true)).eql([ - Array(bufferService.cols + 1).join('a'), - Array(bufferService.cols + 1).join('a'), - Array(bufferService.cols + 1).join('a'), - Array(bufferService.cols + 1).join('a'), - Array(bufferService.cols + 1).join('a'), - Array(40 + 1).join('a'), - '' - ]); - - // reset - bufferService.buffer.y = 0; - bufferService.buffer.x = 0; - for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); - - // params [1] - left and above - bufferService.buffer.y = 5; - bufferService.buffer.x = 40; - inputHandler.eraseInDisplay(Params.fromArray([1])); - expect(termContent(bufferService, false)).eql([ - Array(bufferService.cols + 1).join(' '), - Array(bufferService.cols + 1).join(' '), - Array(bufferService.cols + 1).join(' '), - Array(bufferService.cols + 1).join(' '), - Array(bufferService.cols + 1).join(' '), - Array(41 + 1).join(' ') + Array(bufferService.cols - 41 + 1).join('a'), - Array(bufferService.cols + 1).join('a') - ]); - expect(termContent(bufferService, true)).eql([ - '', - '', - '', - '', - '', - Array(41 + 1).join(' ') + Array(bufferService.cols - 41 + 1).join('a'), - Array(bufferService.cols + 1).join('a') - ]); - - // reset - bufferService.buffer.y = 0; - bufferService.buffer.x = 0; - for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); - - // params [2] - whole screen - bufferService.buffer.y = 5; - bufferService.buffer.x = 40; - inputHandler.eraseInDisplay(Params.fromArray([2])); - expect(termContent(bufferService, false)).eql([ - Array(bufferService.cols + 1).join(' '), - Array(bufferService.cols + 1).join(' '), - Array(bufferService.cols + 1).join(' '), - Array(bufferService.cols + 1).join(' '), - Array(bufferService.cols + 1).join(' '), - Array(bufferService.cols + 1).join(' '), - Array(bufferService.cols + 1).join(' ') - ]); - expect(termContent(bufferService, true)).eql([ - '', - '', - '', - '', - '', - '', - '' - ]); - - // reset and add a wrapped line - bufferService.buffer.y = 0; - bufferService.buffer.x = 0; - inputHandler.parse(Array(bufferService.cols + 1).join('a')); // line 0 - inputHandler.parse(Array(bufferService.cols + 10).join('a')); // line 1 and 2 - for (let i = 3; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); - - // params[1] left and above with wrap - // confirm precondition that line 2 is wrapped - expect(bufferService.buffer.lines.get(2).isWrapped).true; - bufferService.buffer.y = 2; - bufferService.buffer.x = 40; - inputHandler.eraseInDisplay(Params.fromArray([1])); - expect(bufferService.buffer.lines.get(2).isWrapped).false; - - // reset and add a wrapped line - bufferService.buffer.y = 0; - bufferService.buffer.x = 0; - inputHandler.parse(Array(bufferService.cols + 1).join('a')); // line 0 - inputHandler.parse(Array(bufferService.cols + 10).join('a')); // line 1 and 2 - for (let i = 3; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); - - // params[1] left and above with wrap - // confirm precondition that line 2 is wrapped - expect(bufferService.buffer.lines.get(2).isWrapped).true; - bufferService.buffer.y = 1; - bufferService.buffer.x = 90; // Cursor is beyond last column - inputHandler.eraseInDisplay(Params.fromArray([1])); - expect(bufferService.buffer.lines.get(2).isWrapped).false; - }); - }); - it('convertEol setting', function(): void { - // not converting - const termNotConverting = new Terminal({cols: 15, rows: 10}); - (termNotConverting as any)._inputHandler.parse('Hello\nWorld'); - expect(termNotConverting.buffer.lines.get(0).translateToString(false)).equals('Hello '); - expect(termNotConverting.buffer.lines.get(1).translateToString(false)).equals(' World '); - expect(termNotConverting.buffer.lines.get(0).translateToString(true)).equals('Hello'); - expect(termNotConverting.buffer.lines.get(1).translateToString(true)).equals(' World'); - - // converting - const termConverting = new Terminal({cols: 15, rows: 10, convertEol: true}); - (termConverting as any)._inputHandler.parse('Hello\nWorld'); - expect(termConverting.buffer.lines.get(0).translateToString(false)).equals('Hello '); - expect(termConverting.buffer.lines.get(1).translateToString(false)).equals('World '); - expect(termConverting.buffer.lines.get(0).translateToString(true)).equals('Hello'); - expect(termConverting.buffer.lines.get(1).translateToString(true)).equals('World'); - }); - 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(), {} as any); - const container = new Uint32Array(10); - container[0] = 0x200B; - inputHandler.print(container, 0, 1); - }); - }); - - describe('alt screen', () => { - let term: Terminal; - let bufferService: IBufferService; - let handler: 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(), {} as any); - }); - it('should handle DECSET/DECRST 47 (alt screen buffer)', () => { - handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); - expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal(''); - expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); - // Text color of 'TEST' should be red - expect((bufferService.buffer.lines.get(1).loadCell(4, new CellData()).getFgColor())).to.equal(1); - }); - it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => { - handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST'); - expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal(''); - expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); - // Text color of 'TEST' should be red - expect((bufferService.buffer.lines.get(1).loadCell(4, new CellData()).getFgColor())).to.equal(1); - }); - it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => { - handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST'); - expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); - expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal('JUNK'); - // Text color of 'TEST' should be default - expect(bufferService.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); - // Text color of 'JUNK' should be red - expect((bufferService.buffer.lines.get(1).loadCell(0, new CellData()).getFgColor())).to.equal(1); - }); - it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => { - handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST'); - expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); - expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal(''); - // Text color of 'TEST' should be default - expect(bufferService.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); - }); - it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', () => { - handler.parse('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST'); - expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); - // Text color of 'TEST' should be default - expect(bufferService.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); - handler.parse('\x1b[?1049h\x1b[uTEST'); - expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal('TEST'); - // Text color of 'TEST' should be red - expect((bufferService.buffer.lines.get(1).loadCell(0, new CellData()).getFgColor())).to.equal(1); - }); - it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', () => { - handler.parse('\x1b[42m\x1b[?1049h'); - // Buffer should be filled with green background - expect(bufferService.buffer.lines.get(20).loadCell(10, new CellData()).getBgColor()).to.equal(2); - }); - }); - - describe('text attributes', () => { - let term: TestTerminal; - beforeEach(() => { - term = new TestTerminal(); - }); - it('bold', () => { - term.writeSync('\x1b[1m'); - assert.equal(!!term.curAttrData.isBold(), true); - term.writeSync('\x1b[22m'); - assert.equal(!!term.curAttrData.isBold(), false); - }); - it('dim', () => { - term.writeSync('\x1b[2m'); - assert.equal(!!term.curAttrData.isDim(), true); - term.writeSync('\x1b[22m'); - assert.equal(!!term.curAttrData.isDim(), false); - }); - it('italic', () => { - term.writeSync('\x1b[3m'); - assert.equal(!!term.curAttrData.isItalic(), true); - term.writeSync('\x1b[23m'); - assert.equal(!!term.curAttrData.isItalic(), false); - }); - it('underline', () => { - term.writeSync('\x1b[4m'); - assert.equal(!!term.curAttrData.isUnderline(), true); - term.writeSync('\x1b[24m'); - assert.equal(!!term.curAttrData.isUnderline(), false); - }); - it('blink', () => { - term.writeSync('\x1b[5m'); - assert.equal(!!term.curAttrData.isBlink(), true); - term.writeSync('\x1b[25m'); - assert.equal(!!term.curAttrData.isBlink(), false); - }); - it('inverse', () => { - term.writeSync('\x1b[7m'); - assert.equal(!!term.curAttrData.isInverse(), true); - term.writeSync('\x1b[27m'); - assert.equal(!!term.curAttrData.isInverse(), false); - }); - it('invisible', () => { - term.writeSync('\x1b[8m'); - assert.equal(!!term.curAttrData.isInvisible(), true); - term.writeSync('\x1b[28m'); - assert.equal(!!term.curAttrData.isInvisible(), false); - }); - it('colormode palette 16', () => { - assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT - assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT - // lower 8 colors - for (let i = 0; i < 8; ++i) { - term.writeSync(`\x1b[${i + 30};${i + 40}m`); - assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P16); - assert.equal(term.curAttrData.getFgColor(), i); - assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P16); - assert.equal(term.curAttrData.getBgColor(), i); - } - // reset to DEFAULT - term.writeSync(`\x1b[39;49m`); - assert.equal(term.curAttrData.getFgColorMode(), 0); - assert.equal(term.curAttrData.getBgColorMode(), 0); - }); - it('colormode palette 256', () => { - assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT - assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT - // lower 8 colors - for (let i = 0; i < 256; ++i) { - term.writeSync(`\x1b[38;5;${i};48;5;${i}m`); - assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P256); - assert.equal(term.curAttrData.getFgColor(), i); - assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P256); - assert.equal(term.curAttrData.getBgColor(), i); - } - // reset to DEFAULT - term.writeSync(`\x1b[39;49m`); - assert.equal(term.curAttrData.getFgColorMode(), 0); - assert.equal(term.curAttrData.getFgColor(), -1); - assert.equal(term.curAttrData.getBgColorMode(), 0); - assert.equal(term.curAttrData.getBgColor(), -1); - }); - it('colormode RGB', () => { - assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT - assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT - term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`); - assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_RGB); - assert.equal(term.curAttrData.getFgColor(), 1 << 16 | 2 << 8 | 3); - assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [1, 2, 3]); - assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_RGB); - assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getBgColor()), [4, 5, 6]); - // reset to DEFAULT - term.writeSync(`\x1b[39;49m`); - assert.equal(term.curAttrData.getFgColorMode(), 0); - assert.equal(term.curAttrData.getFgColor(), -1); - assert.equal(term.curAttrData.getBgColorMode(), 0); - assert.equal(term.curAttrData.getBgColor(), -1); - }); - it('colormode transition RGB to 256', () => { - // enter RGB for FG and BG - term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`); - // enter 256 for FG and BG - term.writeSync(`\x1b[38;5;255;48;5;255m`); - assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P256); - assert.equal(term.curAttrData.getFgColor(), 255); - assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P256); - assert.equal(term.curAttrData.getBgColor(), 255); - }); - it('colormode transition RGB to 16', () => { - // enter RGB for FG and BG - term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`); - // enter 16 for FG and BG - term.writeSync(`\x1b[37;47m`); - assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P16); - assert.equal(term.curAttrData.getFgColor(), 7); - assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P16); - assert.equal(term.curAttrData.getBgColor(), 7); - }); - it('colormode transition 16 to 256', () => { - // enter 16 for FG and BG - term.writeSync(`\x1b[37;47m`); - // enter 256 for FG and BG - term.writeSync(`\x1b[38;5;255;48;5;255m`); - assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P256); - assert.equal(term.curAttrData.getFgColor(), 255); - assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P256); - assert.equal(term.curAttrData.getBgColor(), 255); - }); - it('colormode transition 256 to 16', () => { - // enter 256 for FG and BG - term.writeSync(`\x1b[38;5;255;48;5;255m`); - // enter 16 for FG and BG - term.writeSync(`\x1b[37;47m`); - assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P16); - assert.equal(term.curAttrData.getFgColor(), 7); - assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P16); - assert.equal(term.curAttrData.getBgColor(), 7); - }); - it('should zero missing RGB values', () => { - term.writeSync(`\x1b[38;2;1;2;3m`); - term.writeSync(`\x1b[38;2;5m`); - assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [5, 0, 0]); - }); - }); - describe('colon notation', () => { - let termColon: TestTerminal; - let termSemicolon: TestTerminal; - beforeEach(() => { - termColon = new TestTerminal(); - termSemicolon = new TestTerminal(); - }); - describe('should equal to semicolon', () => { - it('CSI 38:2::50:100:150 m', () => { - termColon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.writeSync('\x1b[38;2;50;100;150m'); - termColon.writeSync('\x1b[38:2::50:100:150m'); - assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - it('CSI 38:2::50:100: m', () => { - termColon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.writeSync('\x1b[38;2;50;100;m'); - termColon.writeSync('\x1b[38:2::50:100:m'); - assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - it('CSI 38:2::50:: m', () => { - termColon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.writeSync('\x1b[38;2;50;;m'); - termColon.writeSync('\x1b[38:2::50::m'); - assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 0 << 8 | 0); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - it('CSI 38:2:::: m', () => { - termColon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.writeSync('\x1b[38;2;;;m'); - termColon.writeSync('\x1b[38:2::::m'); - assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 0 << 16 | 0 << 8 | 0); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - it('CSI 38;2::50:100:150 m', () => { - termColon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.writeSync('\x1b[38;2;50;100;150m'); - termColon.writeSync('\x1b[38;2::50:100:150m'); - assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - it('CSI 38;2;50:100:150 m', () => { - termColon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.writeSync('\x1b[38;2;50;100;150m'); - termColon.writeSync('\x1b[38;2;50:100:150m'); - assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - it('CSI 38;2;50;100:150 m', () => { - termColon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.writeSync('\x1b[38;2;50;100;150m'); - termColon.writeSync('\x1b[38;2;50;100:150m'); - assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - it('CSI 38:5:50 m', () => { - termColon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.writeSync('\x1b[38;5;50m'); - termColon.writeSync('\x1b[38:5:50m'); - assert.equal(termSemicolon.curAttrData.fg & 0xFF, 50); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - it('CSI 38:5: m', () => { - termColon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.writeSync('\x1b[38;5;m'); - termColon.writeSync('\x1b[38:5:m'); - assert.equal(termSemicolon.curAttrData.fg & 0xFF, 0); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - it('CSI 38;5:50 m', () => { - termColon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.writeSync('\x1b[38;5;50m'); - termColon.writeSync('\x1b[38;5:50m'); - assert.equal(termSemicolon.curAttrData.fg & 0xFF, 50); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - }); - describe('should fill early sequence end with default of 0', () => { - it('CSI 38:2 m', () => { - termColon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.writeSync('\x1b[38;2m'); - termColon.writeSync('\x1b[38:2m'); - assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 0 << 16 | 0 << 8 | 0); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - it('CSI 38:5 m', () => { - termColon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.curAttrData.fg = 0xFFFFFFFF; - termSemicolon.writeSync('\x1b[38;5m'); - termColon.writeSync('\x1b[38:5m'); - assert.equal(termSemicolon.curAttrData.fg & 0xFF, 0); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - }); - describe('should not interfere with leading/following SGR attrs', () => { - it('CSI 1 ; 38:2::50:100:150 ; 4 m', () => { - termSemicolon.writeSync('\x1b[1;38;2;50;100;150;4m'); - termColon.writeSync('\x1b[1;38:2::50:100:150;4m'); - assert.equal(!!termSemicolon.curAttrData.isBold(), true); - assert.equal(!!termSemicolon.curAttrData.isUnderline(), true); - assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - it('CSI 1 ; 38:2::50:100: ; 4 m', () => { - termSemicolon.writeSync('\x1b[1;38;2;50;100;;4m'); - termColon.writeSync('\x1b[1;38:2::50:100:;4m'); - assert.equal(!!termSemicolon.curAttrData.isBold(), true); - assert.equal(!!termSemicolon.curAttrData.isUnderline(), true); - assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - it('CSI 1 ; 38:2::50:100 ; 4 m', () => { - termSemicolon.writeSync('\x1b[1;38;2;50;100;;4m'); - termColon.writeSync('\x1b[1;38:2::50:100;4m'); - assert.equal(!!termSemicolon.curAttrData.isBold(), true); - assert.equal(!!termSemicolon.curAttrData.isUnderline(), true); - assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - it('CSI 1 ; 38:2:: ; 4 m', () => { - termSemicolon.writeSync('\x1b[1;38;2;;;;4m'); - termColon.writeSync('\x1b[1;38:2::;4m'); - assert.equal(!!termSemicolon.curAttrData.isBold(), true); - assert.equal(!!termSemicolon.curAttrData.isUnderline(), true); - assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 0); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - it('CSI 1 ; 38;2:: ; 4 m', () => { - termSemicolon.writeSync('\x1b[1;38;2;;;;4m'); - termColon.writeSync('\x1b[1;38;2::;4m'); - assert.equal(!!termSemicolon.curAttrData.isBold(), true); - assert.equal(!!termSemicolon.curAttrData.isUnderline(), true); - assert.equal(termSemicolon.curAttrData.fg & 0xFFFFFF, 0); - assert.equal(termColon.curAttrData.fg, termSemicolon.curAttrData.fg); - }); - }); - }); - describe('cursor positioning', () => { - let term: TestTerminal; - beforeEach(() => { - term = new TestTerminal({cols: 10, rows: 10}); - }); - 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]); - }); - it('DCH', () => { - term.buffer.x = 10000; - term.buffer.y = 10000; - term.writeSync('\x1b[P'); - assert.deepEqual(getCursor(term), [9, 9]); - term.buffer.x = -10000; - term.buffer.y = -10000; - term.writeSync('\x1b[P'); - assert.deepEqual(getCursor(term), [0, 0]); - }); - it('DCH - should delete last cell', () => { - term.writeSync('0123456789\x1b[P'); - assert.equal(term.buffer.lines.get(0).translateToString(false), '012345678 '); - }); - it('ECH', () => { - term.buffer.x = 10000; - term.buffer.y = 10000; - term.writeSync('\x1b[X'); - assert.deepEqual(getCursor(term), [9, 9]); - term.buffer.x = -10000; - term.buffer.y = -10000; - term.writeSync('\x1b[X'); - assert.deepEqual(getCursor(term), [0, 0]); - }); - it('ECH - should delete last cell', () => { - term.writeSync('0123456789\x1b[X'); - assert.equal(term.buffer.lines.get(0).translateToString(false), '012345678 '); - }); - it('ICH', () => { - term.buffer.x = 10000; - term.buffer.y = 10000; - term.writeSync('\x1b[@'); - assert.deepEqual(getCursor(term), [9, 9]); - term.buffer.x = -10000; - term.buffer.y = -10000; - term.writeSync('\x1b[@'); - assert.deepEqual(getCursor(term), [0, 0]); - }); - it('ICH - should delete last cell', () => { - term.writeSync('0123456789\x1b[@'); - assert.equal(term.buffer.lines.get(0).translateToString(false), '012345678 '); - }); - }); - }); - describe('DECSTBM - scroll margins', () => { - let term: TestTerminal; - beforeEach(() => { - term = new TestTerminal({cols: 10, rows: 10}); - }); - it('should default to whole viewport', () => { - term.writeSync('\x1b[r'); - assert.equal(term.buffer.scrollTop, 0); - assert.equal(term.buffer.scrollBottom, 9); - term.writeSync('\x1b[3;7r'); - assert.equal(term.buffer.scrollTop, 2); - assert.equal(term.buffer.scrollBottom, 6); - term.writeSync('\x1b[0;0r'); - assert.equal(term.buffer.scrollTop, 0); - assert.equal(term.buffer.scrollBottom, 9); - }); - it('should clamp bottom', () => { - term.writeSync('\x1b[3;1000r'); - assert.equal(term.buffer.scrollTop, 2); - assert.equal(term.buffer.scrollBottom, 9); - }); - it('should only apply for top < bottom', () => { - term.writeSync('\x1b[7;2r'); - assert.equal(term.buffer.scrollTop, 0); - assert.equal(term.buffer.scrollBottom, 9); - }); - it('should home cursor', () => { - term.buffer.x = 10000; - term.buffer.y = 10000; - term.writeSync('\x1b[2;7r'); - assert.deepEqual(getCursor(term), [0, 0]); - }); - }); - describe('scroll margins', () => { - let term: TestTerminal; - beforeEach(() => { - term = new TestTerminal({cols: 10, rows: 10}); - }); - it('scrollUp', () => { - term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Sm'); - assert.deepEqual(getLines(term), ['m', '3', '', '', '4', '5', '6', '7', '8', '9']); - }); - it('scrollDown', () => { - term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Tm'); - assert.deepEqual(getLines(term), ['m', '', '', '1', '4', '5', '6', '7', '8', '9']); - }); - it('insertLines - out of margins', () => { - term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); - assert.equal(term.buffer.scrollTop, 2); - assert.equal(term.buffer.scrollBottom, 5); - term.writeSync('\x1b[2Lm'); - assert.deepEqual(getLines(term), ['m', '1', '2', '3', '4', '5', '6', '7', '8', '9']); - term.writeSync('\x1b[2H\x1b[2Ln'); - assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', '6', '7', '8', '9']); - // skip below scrollbottom - term.writeSync('\x1b[7H\x1b[2Lo'); - assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', 'o', '7', '8', '9']); - term.writeSync('\x1b[8H\x1b[2Lp'); - assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', '9']); - term.writeSync('\x1b[100H\x1b[2Lq'); - assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', 'q']); - }); - it('insertLines - within margins', () => { - term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); - assert.equal(term.buffer.scrollTop, 2); - assert.equal(term.buffer.scrollBottom, 5); - term.writeSync('\x1b[3H\x1b[2Lm'); - assert.deepEqual(getLines(term), ['0', '1', 'm', '', '2', '3', '6', '7', '8', '9']); - term.writeSync('\x1b[6H\x1b[2Ln'); - assert.deepEqual(getLines(term), ['0', '1', 'm', '', '2', 'n', '6', '7', '8', '9']); - }); - it('deleteLines - out of margins', () => { - term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); - assert.equal(term.buffer.scrollTop, 2); - assert.equal(term.buffer.scrollBottom, 5); - term.writeSync('\x1b[2Mm'); - assert.deepEqual(getLines(term), ['m', '1', '2', '3', '4', '5', '6', '7', '8', '9']); - term.writeSync('\x1b[2H\x1b[2Mn'); - assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', '6', '7', '8', '9']); - // skip below scrollbottom - term.writeSync('\x1b[7H\x1b[2Mo'); - assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', 'o', '7', '8', '9']); - term.writeSync('\x1b[8H\x1b[2Mp'); - assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', '9']); - term.writeSync('\x1b[100H\x1b[2Mq'); - assert.deepEqual(getLines(term), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', 'q']); - }); - it('deleteLines - within margins', () => { - term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); - assert.equal(term.buffer.scrollTop, 2); - assert.equal(term.buffer.scrollBottom, 5); - term.writeSync('\x1b[6H\x1b[2Mm'); - assert.deepEqual(getLines(term), ['0', '1', '2', '3', '4', 'm', '6', '7', '8', '9']); - term.writeSync('\x1b[3H\x1b[2Mn'); - assert.deepEqual(getLines(term), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']); - }); - }); - describe('SL/SR/DECIC/DECDC', () => { - let term: TestTerminal; - beforeEach(() => { - term = new TestTerminal({cols: 5, rows: 5, scrollback: 1}); - }); - it('SL (scrollLeft)', () => { - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[ @'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '2345', '2345', '2345', '2345', '2345']); - term.writeSync('\x1b[0 @'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '345', '345', '345', '345', '345']); - term.writeSync('\x1b[2 @'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '5', '5', '5', '5', '5']); - }); - it('SR (scrollRight)', () => { - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[ A'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']); - term.writeSync('\x1b[0 A'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']); - term.writeSync('\x1b[2 A'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']); - }); - it('insertColumns (DECIC)', () => { - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[\'}'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); - term.reset(); - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[1\'}'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); - term.reset(); - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[2\'}'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']); - }); - it('deleteColumns (DECDC)', () => { - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[\'~'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']); - term.reset(); - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[1\'~'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']); - term.reset(); - term.writeSync('12345'.repeat(6)); - term.writeSync('\x1b[3;3H'); - term.writeSync('\x1b[2\'~'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '125', '125', '125', '125', '125']); - }); - }); - it('should parse big chunks in smaller subchunks', () => { - // max single chunk size is hardcoded as 131072 - const calls: any[] = []; - const term = new TestTerminal({cols: 10, rows: 10}); - (term as any)._inputHandler._parser.parse = (data: Uint32Array, length: number) => { - calls.push([data.length, length]); - }; - term.writeSync('12345'); - term.writeSync('a'.repeat(10000)); - term.writeSync('a'.repeat(200000)); - term.writeSync('a'.repeat(300000)); - assert.deepEqual(calls, [ - [4096, 5], - [10000, 10000], - [131072, 131072], [131072, 200000 - 131072], - [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(() => { - term = new TestTerminal({cols: 10, rows: 5, scrollback: 1}); - term.writeSync('¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥'); - }); - it('print', () => { - term.writeSync('\x1b[H#'); - assert.deepEqual(getLines(term), ['# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - term.writeSync('\x1b[1;6H######'); - assert.deepEqual(getLines(term), ['# ¥ #####', '# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - term.writeSync('#'); - assert.deepEqual(getLines(term), ['# ¥ #####', '##¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - term.writeSync('#'); - assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - term.writeSync('\x1b[3;9H#'); - assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥#', '¥¥¥¥¥', '']); - term.writeSync('#'); - assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '¥¥¥¥¥', '']); - term.writeSync('#'); - assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥¥', '']); - term.writeSync('\x1b[4;10H#'); - assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥ #', '']); - }); - it('EL', () => { - term.writeSync('\x1b[1;6H\x1b[K#'); - assert.deepEqual(getLines(term), ['¥¥ #', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - term.writeSync('\x1b[2;5H\x1b[1K'); - assert.deepEqual(getLines(term), ['¥¥ #', ' ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - term.writeSync('\x1b[3;6H\x1b[1K'); - assert.deepEqual(getLines(term), ['¥¥ #', ' ¥¥', ' ¥¥', '¥¥¥¥¥', '']); - }); - it('ICH', () => { - term.writeSync('\x1b[1;6H\x1b[@'); - assert.deepEqual(getLines(term), ['¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - term.writeSync('\x1b[2;4H\x1b[2@'); - assert.deepEqual(getLines(term), ['¥¥ ¥', '¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - term.writeSync('\x1b[3;4H\x1b[3@'); - assert.deepEqual(getLines(term), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥¥¥¥¥', '']); - term.writeSync('\x1b[4;4H\x1b[4@'); - assert.deepEqual(getLines(term), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥ ¥', '']); - }); - it('DCH', () => { - term.writeSync('\x1b[1;6H\x1b[P'); - assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - term.writeSync('\x1b[2;6H\x1b[2P'); - assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - term.writeSync('\x1b[3;6H\x1b[3P'); - assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']); - }); - it('ECH', () => { - term.writeSync('\x1b[1;6H\x1b[X'); - assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - term.writeSync('\x1b[2;6H\x1b[2X'); - assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); - term.writeSync('\x1b[3;6H\x1b[3X'); - assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']); - }); - }); - describe('DECSTR', () => { - let term: TestTerminal; - beforeEach(() => { - term = new TestTerminal({cols: 10, rows: 5, scrollback: 1}); - term.writeSync('01234567890123'); - }); - it('should reset IRM', () => { - term.writeSync('\x1b[4h'); - assert.equal(term.insertMode, true); - term.writeSync('\x1b[!p'); - assert.equal(term.insertMode, false); - }); - it('should reset cursor visibility', () => { - term.writeSync('\x1b[?25l'); - assert.equal((term as any)._coreService.isCursorHidden, true); - term.writeSync('\x1b[!p'); - assert.equal((term as any)._coreService.isCursorHidden, false); - }); - it('should reset scroll margins', () => { - term.writeSync('\x1b[2;4r'); - assert.equal((term as any)._bufferService.buffer.scrollTop, 1); - assert.equal((term as any)._bufferService.buffer.scrollBottom, 3); - term.writeSync('\x1b[!p'); - assert.equal((term as any)._bufferService.buffer.scrollTop, 0); - assert.equal((term as any)._bufferService.buffer.scrollBottom, term.rows - 1); - }); - it('should reset text attributes', () => { - term.writeSync('\x1b[1;2;32;43m'); - assert.equal(!!term.curAttrData.isBold(), true); - term.writeSync('\x1b[!p'); - assert.equal(!!term.curAttrData.isBold(), false); - assert.equal(term.curAttrData.fg, 0); - assert.equal(term.curAttrData.bg, 0); - }); - it('should reset DECSC data', () => { - term.writeSync('\x1b7'); - assert.equal((term as any)._bufferService.buffer.savedX, 4); - assert.equal((term as any)._bufferService.buffer.savedY, 1); - term.writeSync('\x1b[!p'); - assert.equal((term as any)._bufferService.buffer.savedX, 0); - assert.equal((term as any)._bufferService.buffer.savedY, 0); - }); - it('should reset DECOM', () => { - term.writeSync('\x1b[?6h'); - assert.equal((term as any)._coreService.decPrivateModes.origin, true); - term.writeSync('\x1b[!p'); - assert.equal((term as any)._coreService.decPrivateModes.origin, false); - }); - }); -}); diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts deleted file mode 100644 index cac0a43d..00000000 --- a/src/TestUtils.test.ts +++ /dev/null @@ -1,360 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRedrawEvent } from 'browser/renderer/Types'; -import { IInputHandlingTerminal, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions } from './Types'; -import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; -import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset } from 'common/Types'; -import { Buffer } from 'common/buffer/Buffer'; -import * as Browser from 'common/Platform'; -import { IDisposable, IMarker, IEvent, ISelectionPosition, ILinkProvider } from 'xterm'; -import { Terminal } from './Terminal'; -import { AttributeData } from 'common/buffer/AttributeData'; -import { IColorManager, IColorSet, ILinkMatcherOptions, ILinkifier, IViewport, ILinkifier2 } from 'browser/Types'; -import { IOptionsService, IUnicodeService } from 'common/services/Services'; -import { EventEmitter } from 'common/EventEmitter'; -import { IParams, IFunctionIdentifier } from 'common/parser/Types'; -import { ISelectionService } from 'browser/services/Services'; - -export class TestTerminal extends Terminal { - public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; } - public keyDown(ev: any): boolean { return this._keyDown(ev); } - public keyPress(ev: any): boolean { return this._keyPress(ev); } -} - -export class MockTerminal implements ITerminal { - public onBlur: IEvent; - public onFocus: IEvent; - public onA11yChar: IEvent; - public onA11yTab: IEvent; - public onCursorMove: IEvent; - public onLineFeed: IEvent; - public onSelectionChange: IEvent; - public onData: IEvent; - public onBinary: IEvent; - public onTitleChange: IEvent; - public onScroll: IEvent; - public onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>; - public onRender: IEvent<{ start: number, end: number }>; - public onResize: IEvent<{ cols: number, rows: number }>; - public markers: IMarker[]; - public optionsService: IOptionsService; - public unicodeService: IUnicodeService; - public addMarker(cursorYOffset: number): IMarker { - throw new Error('Method not implemented.'); - } - public selectLines(start: number, end: number): void { - throw new Error('Method not implemented.'); - } - public scrollToLine(line: number): void { - throw new Error('Method not implemented.'); - } - public static string: any; - public setOption(key: any, value: any): void { - throw new Error('Method not implemented.'); - } - public blur(): void { - throw new Error('Method not implemented.'); - } - public focus(): void { - throw new Error('Method not implemented.'); - } - public resize(columns: number, rows: number): void { - throw new Error('Method not implemented.'); - } - public writeln(data: string): void { - throw new Error('Method not implemented.'); - } - public paste(data: string): void { - throw new Error('Method not implemented.'); - } - public open(parent: HTMLElement): void { - throw new Error('Method not implemented.'); - } - public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { - throw new Error('Method not implemented.'); - } - public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { - throw new Error('Method not implemented.'); - } - public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { - throw new Error('Method not implemented.'); - } - public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { - throw new Error('Method not implemented.'); - } - public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - throw new Error('Method not implemented.'); - } - public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number { - throw new Error('Method not implemented.'); - } - public deregisterLinkMatcher(matcherId: number): void { - throw new Error('Method not implemented.'); - } - public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { - throw new Error('Method not implemented.'); - } - public hasSelection(): boolean { - throw new Error('Method not implemented.'); - } - public getSelection(): string { - throw new Error('Method not implemented.'); - } - public getSelectionPosition(): ISelectionPosition | undefined { - throw new Error('Method not implemented.'); - } - public clearSelection(): void { - throw new Error('Method not implemented.'); - } - public select(column: number, row: number, length: number): void { - throw new Error('Method not implemented.'); - } - public selectAll(): void { - throw new Error('Method not implemented.'); - } - public dispose(): void { - throw new Error('Method not implemented.'); - } - public scrollPages(pageCount: number): void { - throw new Error('Method not implemented.'); - } - public scrollToTop(): void { - throw new Error('Method not implemented.'); - } - public scrollToBottom(): void { - throw new Error('Method not implemented.'); - } - public clear(): void { - throw new Error('Method not implemented.'); - } - public write(data: string): void { - throw new Error('Method not implemented.'); - } - public writeUtf8(data: Uint8Array): void { - throw new Error('Method not implemented.'); - } - public bracketedPasteMode: boolean; - public renderer: IRenderer; - public linkifier: ILinkifier; - public linkifier2: ILinkifier2; - public isFocused: boolean; - public options: ITerminalOptions = {}; - public element: HTMLElement; - public screenElement: HTMLElement; - public rowContainer: HTMLElement; - public selectionContainer: HTMLElement; - public selectionService: ISelectionService; - public textarea: HTMLTextAreaElement; - public rows: number; - public cols: number; - public browser: IBrowser = Browser; - public writeBuffer: string[]; - public children: HTMLElement[]; - public cursorHidden: boolean; - public cursorState: number; - public scrollback: number; - public buffers: IBufferSet; - public buffer: IBuffer; - public viewport: IViewport; - public applicationCursor: boolean; - public handler(data: string): void { - throw new Error('Method not implemented.'); - } - public on(event: string, callback: (...args: any[]) => void): void { - throw new Error('Method not implemented.'); - } - public off(type: string, listener: XtermListener): void { - throw new Error('Method not implemented.'); - } - public addDisposableListener(type: string, handler: XtermListener): IDisposable { - throw new Error('Method not implemented.'); - } - public scrollLines(disp: number, suppressScrollEvent: boolean): void { - throw new Error('Method not implemented.'); - } - public scrollToRow(absoluteRow: number): number { - throw new Error('Method not implemented.'); - } - public cancel(ev: Event, force?: boolean): void { - throw new Error('Method not implemented.'); - } - public log(text: string): void { - throw new Error('Method not implemented.'); - } - public emit(event: string, data: any): void { - throw new Error('Method not implemented.'); - } - public reset(): void { - throw new Error('Method not implemented.'); - } - public showCursor(): void { - throw new Error('Method not implemented.'); - } - public refresh(start: number, end: number): void { - throw new Error('Method not implemented.'); - } - public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; } - public deregisterCharacterJoiner(joinerId: number): void { } -} - -export class MockInputHandlingTerminal implements IInputHandlingTerminal { - public onA11yCharEmitter: EventEmitter; - public onA11yTabEmitter: EventEmitter; - public insertMode: boolean; - public bracketedPasteMode: boolean; - public sendFocus: boolean; - public buffers: IBufferSet; - public buffer: IBuffer = new MockBuffer(); - public viewport: IViewport; - public scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void { - throw new Error('Method not implemented.'); - } - public is(term: string): boolean { - throw new Error('Method not implemented.'); - } - public resize(x: number, y: number): void { - throw new Error('Method not implemented.'); - } - public showCursor(): void { - throw new Error('Method not implemented.'); - } - public handler(data: string): void { - throw new Error('Method not implemented.'); - } - public handleTitle(title: string): void { - throw new Error('Method not implemented.'); - } -} - -export class MockBuffer implements IBuffer { - public markers: IMarker[]; - public addMarker(y: number): IMarker { - throw new Error('Method not implemented.'); - } - public isCursorInViewport: boolean; - public lines: ICircularList; - public ydisp: number; - public ybase: number; - public hasScrollback: boolean; - public y: number; - public x: number; - public tabs: any; - public scrollBottom: number; - public scrollTop: number; - public savedY: number; - public savedX: number; - public savedCharset: ICharset | null; - public savedCurAttrData = new AttributeData(); - public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string { - return Buffer.prototype.translateBufferLineToString.apply(this, arguments); - } - public getWrappedRangeForLine(y: number): { first: number, last: number } { - return Buffer.prototype.getWrappedRangeForLine.apply(this, arguments); - } - public nextStop(x?: number): number { - throw new Error('Method not implemented.'); - } - public prevStop(x?: number): number { - throw new Error('Method not implemented.'); - } - public setLines(lines: ICircularList): void { - this.lines = lines; - } - public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine { - return Buffer.prototype.getBlankLine.apply(this, arguments); - } - public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] { - return Buffer.prototype.stringIndexToBufferIndex.apply(this, arguments); - } - public iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator { - return Buffer.prototype.iterator.apply(this, arguments); - } - public getNullCell(attr?: IAttributeData): ICellData { - throw new Error('Method not implemented.'); - } - public getWhitespaceCell(attr?: IAttributeData): ICellData { - throw new Error('Method not implemented.'); - } -} - -export class MockRenderer implements IRenderer { - public onRequestRedraw: IEvent; - public onCanvasResize: IEvent<{ width: number, height: number }>; - public onRender: IEvent<{ start: number, end: number }>; - public dispose(): void { - throw new Error('Method not implemented.'); - } - public colorManager: IColorManager; - public on(type: string, listener: XtermListener): void { - throw new Error('Method not implemented.'); - } - public off(type: string, listener: XtermListener): void { - throw new Error('Method not implemented.'); - } - public emit(type: string, data?: any): void { - throw new Error('Method not implemented.'); - } - public addDisposableListener(type: string, handler: XtermListener): IDisposable { - throw new Error('Method not implemented.'); - } - public dimensions: IRenderDimensions; - public setColors(colors: IColorSet): void { - throw new Error('Method not implemented.'); - } - public onResize(cols: number, rows: number): void { } - public onCharSizeChanged(): void { } - public onBlur(): void { } - public onFocus(): void { } - public onSelectionChanged(start: [number, number], end: [number, number]): void { } - public onCursorMove(): void { } - public onOptionsChanged(): void { } - public onDevicePixelRatioChange(): void { } - public clear(): void { } - public renderRows(start: number, end: number): void { } - public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; } - public deregisterCharacterJoiner(): boolean { return true; } -} - -export class MockViewport implements IViewport { - public dispose(): void { - throw new Error('Method not implemented.'); - } - public scrollBarWidth: number = 0; - public onThemeChange(colors: IColorSet): void { - throw new Error('Method not implemented.'); - } - public onWheel(ev: WheelEvent): boolean { - throw new Error('Method not implemented.'); - } - public onTouchStart(ev: TouchEvent): void { - throw new Error('Method not implemented.'); - } - public onTouchMove(ev: TouchEvent): boolean { - throw new Error('Method not implemented.'); - } - public syncScrollArea(): void { } - public getLinesScrolled(ev: WheelEvent): number { - throw new Error('Method not implemented.'); - } -} - -export class MockCompositionHelper implements ICompositionHelper { - public compositionstart(): void { - throw new Error('Method not implemented.'); - } - public compositionupdate(ev: CompositionEvent): void { - throw new Error('Method not implemented.'); - } - public compositionend(): void { - throw new Error('Method not implemented.'); - } - public updateCompositionElements(dontRecurse?: boolean): void { - throw new Error('Method not implemented.'); - } - public keydown(ev: KeyboardEvent): boolean { - return true; - } -} diff --git a/src/Types.d.ts b/src/Types.d.ts deleted file mode 100644 index cb0ba844..00000000 --- a/src/Types.d.ts +++ /dev/null @@ -1,236 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { ITerminalOptions as IPublicTerminalOptions, IDisposable, IMarker, ISelectionPosition, ILinkProvider } from 'xterm'; -import { ICharset, IAttributeData, CharData, CoreMouseEventType } from 'common/Types'; -import { IEvent, IEventEmitter } from 'common/EventEmitter'; -import { IColorSet, ILinkifier, ILinkMatcherOptions, IViewport, ILinkifier2 } from 'browser/Types'; -import { IOptionsService, IUnicodeService } from 'common/services/Services'; -import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IParams, IFunctionIdentifier } from 'common/parser/Types'; - -export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; - -export type LineData = CharData[]; - -/** - * This interface encapsulates everything needed from the Terminal by the - * InputHandler. This cleanly separates the large amount of methods needed by - * InputHandler cleanly from the ITerminal interface. - */ -export interface IInputHandlingTerminal { - insertMode: boolean; - bracketedPasteMode: boolean; - sendFocus: boolean; - - buffers: IBufferSet; - buffer: IBuffer; - viewport: IViewport; - - onA11yCharEmitter: IEventEmitter; - onA11yTabEmitter: IEventEmitter; - - scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void; - is(term: string): boolean; - resize(x: number, y: number): void; - showCursor(): void; - handleTitle(title: string): void; -} - -export interface ICompositionHelper { - compositionstart(): void; - compositionupdate(ev: CompositionEvent): void; - compositionend(): void; - updateCompositionElements(dontRecurse?: boolean): void; - keydown(ev: KeyboardEvent): boolean; -} - -/** - * Calls the parser and handles actions generated by the parser. - */ -export interface IInputHandler { - parse(data: string | Uint8Array): void; - print(data: Uint32Array, start: number, end: number): void; - - /** C0 BEL */ bell(): void; - /** C0 LF */ lineFeed(): void; - /** C0 CR */ carriageReturn(): void; - /** C0 BS */ backspace(): void; - /** C0 HT */ tab(): void; - /** C0 SO */ shiftOut(): void; - /** C0 SI */ shiftIn(): void; - - /** CSI @ */ insertChars(params: IParams): void; - /** CSI SP @ */ scrollLeft(params: IParams): void; - /** CSI A */ cursorUp(params: IParams): void; - /** CSI SP A */ scrollRight(params: IParams): void; - /** CSI B */ cursorDown(params: IParams): void; - /** CSI C */ cursorForward(params: IParams): void; - /** CSI D */ cursorBackward(params: IParams): void; - /** CSI E */ cursorNextLine(params: IParams): void; - /** CSI F */ cursorPrecedingLine(params: IParams): void; - /** CSI G */ cursorCharAbsolute(params: IParams): void; - /** CSI H */ cursorPosition(params: IParams): void; - /** CSI I */ cursorForwardTab(params: IParams): void; - /** CSI J */ eraseInDisplay(params: IParams): void; - /** CSI K */ eraseInLine(params: IParams): void; - /** CSI L */ insertLines(params: IParams): void; - /** CSI M */ deleteLines(params: IParams): void; - /** CSI P */ deleteChars(params: IParams): void; - /** CSI S */ scrollUp(params: IParams): void; - /** CSI T */ scrollDown(params: IParams, collect?: string): void; - /** CSI X */ eraseChars(params: IParams): void; - /** CSI Z */ cursorBackwardTab(params: IParams): void; - /** CSI ` */ charPosAbsolute(params: IParams): void; - /** CSI a */ hPositionRelative(params: IParams): void; - /** CSI b */ repeatPrecedingCharacter(params: IParams): void; - /** CSI c */ sendDeviceAttributesPrimary(params: IParams): void; - /** CSI > c */ sendDeviceAttributesSecondary(params: IParams): void; - /** CSI d */ linePosAbsolute(params: IParams): void; - /** CSI e */ vPositionRelative(params: IParams): void; - /** CSI f */ hVPosition(params: IParams): void; - /** CSI g */ tabClear(params: IParams): void; - /** CSI h */ setMode(params: IParams, collect?: string): void; - /** CSI l */ resetMode(params: IParams, collect?: string): void; - /** CSI m */ charAttributes(params: IParams): void; - /** CSI n */ deviceStatus(params: IParams, collect?: string): void; - /** CSI p */ softReset(params: IParams, collect?: string): void; - /** CSI q */ setCursorStyle(params: IParams, collect?: string): void; - /** CSI r */ setScrollRegion(params: IParams, collect?: string): void; - /** CSI s */ saveCursor(params: IParams): void; - /** CSI u */ restoreCursor(params: IParams): void; - /** CSI ' } */ insertColumns(params: IParams): void; - /** CSI ' ~ */ deleteColumns(params: IParams): void; - /** OSC 0 - OSC 2 */ setTitle(data: string): void; - /** ESC E */ nextLine(): void; - /** ESC = */ keypadApplicationMode(): void; - /** ESC > */ keypadNumericMode(): void; - /** ESC % G - ESC % @ */ selectDefaultCharset(): void; - /** ESC ( C - ESC ) C - ESC * C - ESC + C - ESC - C - ESC . C - ESC / C */ selectCharset(collectAndFlag: string): void; - /** ESC D */ index(): void; - /** ESC H */ tabSet(): void; - /** ESC M */ reverseIndex(): void; - /** ESC c */ fullReset(): void; - /** ESC n - ESC o - ESC | - ESC } - ESC ~ */ setgLevel(level: number): void; - /** ESC # 8 */ screenAlignmentPattern(): void; -} - -export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor { - screenElement: HTMLElement; - browser: IBrowser; - buffer: IBuffer; - buffers: IBufferSet; - viewport: IViewport; - bracketedPasteMode: boolean; - optionsService: IOptionsService; - // TODO: We should remove options once components adopt optionsService - options: ITerminalOptions; - unicodeService: IUnicodeService; - - onBlur: IEvent; - onFocus: IEvent; - onA11yChar: IEvent; - onA11yTab: IEvent; - - scrollLines(disp: number, suppressScrollEvent?: boolean): void; - cancel(ev: Event, force?: boolean): boolean | void; - showCursor(): void; -} - -// Portions of the public API that are required by the internal Terminal -export interface IPublicTerminal extends IDisposable { - textarea: HTMLTextAreaElement | undefined; - rows: number; - cols: number; - buffer: IBuffer; - markers: IMarker[]; - onCursorMove: IEvent; - onData: IEvent; - onBinary: IEvent; - onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>; - onLineFeed: IEvent; - onScroll: IEvent; - onSelectionChange: IEvent; - onRender: IEvent<{ start: number, end: number }>; - onResize: IEvent<{ cols: number, rows: number }>; - onTitleChange: IEvent; - blur(): void; - focus(): void; - resize(columns: number, rows: number): void; - open(parent: HTMLElement): void; - attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; - addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; - addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable; - addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable; - addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; - registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; - deregisterLinkMatcher(matcherId: number): void; - registerLinkProvider(linkProvider: ILinkProvider): IDisposable; - registerCharacterJoiner(handler: (text: string) => [number, number][]): number; - deregisterCharacterJoiner(joinerId: number): void; - addMarker(cursorYOffset: number): IMarker; - hasSelection(): boolean; - getSelection(): string; - getSelectionPosition(): ISelectionPosition | undefined; - clearSelection(): void; - select(column: number, row: number, length: number): void; - selectAll(): void; - selectLines(start: number, end: number): void; - dispose(): void; - scrollLines(amount: number): void; - scrollPages(pageCount: number): void; - scrollToTop(): void; - scrollToBottom(): void; - scrollToLine(line: number): void; - clear(): void; - write(data: string | Uint8Array, callback?: () => void): void; - paste(data: string): void; - refresh(start: number, end: number): void; - reset(): void; -} - -export interface IBufferAccessor { - buffer: IBuffer; -} - -export interface IElementAccessor { - readonly element: HTMLElement | undefined; -} - -export interface ILinkifierAccessor { - linkifier: ILinkifier; - linkifier2: ILinkifier2; -} - -// TODO: The options that are not in the public API should be reviewed -export interface ITerminalOptions extends IPublicTerminalOptions { - [key: string]: any; - cancelEvents?: boolean; - convertEol?: boolean; - termName?: string; -} - -export interface IBrowser { - isNode: boolean; - userAgent: string; - platform: string; - isFirefox: boolean; - isMac: boolean; - isIpad: boolean; - isIphone: boolean; - isWindows: boolean; -} diff --git a/src/AccessibilityManager.ts b/src/browser/AccessibilityManager.ts similarity index 97% rename from src/AccessibilityManager.ts rename to src/browser/AccessibilityManager.ts index 78391501..2a34d6a3 100644 --- a/src/AccessibilityManager.ts +++ b/src/browser/AccessibilityManager.ts @@ -3,8 +3,8 @@ * @license MIT */ -import * as Strings from './browser/LocalizableStrings'; -import { ITerminal } from './Types'; +import * as Strings from 'browser/LocalizableStrings'; +import { ITerminal } from 'browser/Types'; import { IBuffer } from 'common/buffer/Types'; import { isMac } from 'common/Platform'; import { RenderDebouncer } from 'browser/RenderDebouncer'; @@ -79,6 +79,9 @@ export class AccessibilityManager extends Disposable { this._liveRegion.setAttribute('aria-live', 'assertive'); this._accessibilityTreeRoot.appendChild(this._liveRegion); + if (!this._terminal.element) { + throw new Error('Cannot enable accessibility before Terminal.open'); + } this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot); this.register(this._renderRowsDebouncer); @@ -103,7 +106,7 @@ export class AccessibilityManager extends Disposable { public dispose(): void { super.dispose(); - this._terminal.element.removeChild(this._accessibilityTreeRoot); + this._terminal.element?.removeChild(this._accessibilityTreeRoot); this._rowElements.length = 0; } diff --git a/src/browser/Clipboard.ts b/src/browser/Clipboard.ts index a7c48bfb..5fe1e8fc 100644 --- a/src/browser/Clipboard.ts +++ b/src/browser/Clipboard.ts @@ -42,17 +42,17 @@ export function copyHandler(ev: ClipboardEvent, selectionService: ISelectionServ * @param ev The original paste event to be handled * @param term The terminal on which to apply the handled paste event */ -export function handlePasteEvent(ev: ClipboardEvent, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, coreService: ICoreService): void { +export function handlePasteEvent(ev: ClipboardEvent, textarea: HTMLTextAreaElement, coreService: ICoreService): void { ev.stopPropagation(); if (ev.clipboardData) { const text = ev.clipboardData.getData('text/plain'); - paste(text, textarea, bracketedPasteMode, coreService); + paste(text, textarea, coreService); } } -export function paste(text: string, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, coreService: ICoreService): void { +export function paste(text: string, textarea: HTMLTextAreaElement, coreService: ICoreService): void { text = prepareTextForTerminal(text); - text = bracketTextForPaste(text, bracketedPasteMode); + text = bracketTextForPaste(text, coreService.decPrivateModes.bracketedPasteMode); coreService.triggerDataEvent(text, true); textarea.value = ''; } diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 54812169..458b1bff 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations } from './Types'; +import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations } from 'browser/Types'; import { IDisposable } from 'common/Types'; import { IMouseService, IRenderService } from './services/Services'; import { IBufferService } from 'common/services/Services'; diff --git a/src/browser/RenderDebouncer.ts b/src/browser/RenderDebouncer.ts index 814afcef..2a06fdd6 100644 --- a/src/browser/RenderDebouncer.ts +++ b/src/browser/RenderDebouncer.ts @@ -26,7 +26,7 @@ export class RenderDebouncer implements IDisposable { } } - public refresh(rowStart: number, rowEnd: number, rowCount: number): void { + public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void { this._rowCount = rowCount; // Get the min/max row start/end for the arg values rowStart = rowStart !== undefined ? rowStart : 0; diff --git a/src/Terminal.test.ts b/src/browser/Terminal.test.ts similarity index 74% rename from src/Terminal.test.ts rename to src/browser/Terminal.test.ts index 4d590699..729c0d0f 100644 --- a/src/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -4,7 +4,7 @@ */ import { assert, expect } from 'chai'; -import { MockViewport, MockCompositionHelper, MockRenderer, TestTerminal } from './TestUtils.test'; +import { MockViewport, MockCompositionHelper, MockRenderer, TestTerminal } from 'browser/TestUtils.test'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { CellData } from 'common/buffer/CellData'; import { IBufferService, IUnicodeService } from 'common/services/Services'; @@ -121,7 +121,7 @@ describe('Terminal', () => { assert.equal(e, 'title'); done(); }); - term.handleTitle('title'); + term.write('\x1b]2;title\x07'); }); }); @@ -140,7 +140,6 @@ describe('Terminal', () => { }; beforeEach(() => { - term.showCursor = () => { }; term.clearSelection = () => { }; }); @@ -396,62 +395,62 @@ describe('Terminal', () => { describe('scroll() function', () => { describe('when scrollback > 0', () => { it('should create a new line and scroll', () => { - term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(INIT_ROWS - 1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); - assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), 'b'); - assert.equal(term.buffer.lines.get(INIT_ROWS).loadCell(0, new CellData()).getChars(), ''); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1)!.loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(INIT_ROWS)!.loadCell(0, new CellData()).getChars(), ''); }); it('should properly scroll inside a scroll region (scrollTop set)', () => { - term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); - assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c'); }); it('should properly scroll inside a scroll region (scrollBottom set)', () => { - term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); - term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); - term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = 3; term.buffer.scrollBottom = 3; term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS + 1); - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a', '\'a\' should be pushed to the scrollback'); - assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'b'); - assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'c'); - assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), 'd'); - assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(5).loadCell(0, new CellData()).getChars(), 'e'); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a', '\'a\' should be pushed to the scrollback'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(2)!.loadCell(0, new CellData()).getChars(), 'c'); + assert.equal(term.buffer.lines.get(3)!.loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(4)!.loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(5)!.loadCell(0, new CellData()).getChars(), 'e'); }); it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { - term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); - term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); - term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.buffer.scrollBottom = 3; term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); - assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); - assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd'); - assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e'); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2)!.loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(3)!.loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4)!.loadCell(0, new CellData()).getChars(), 'e'); }); }); @@ -462,65 +461,65 @@ describe('Terminal', () => { }); it('should create a new line and shift everything up', () => { - term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(INIT_ROWS - 1)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line assert.equal(term.buffer.lines.length, INIT_ROWS); term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); // 'a' gets pushed out of buffer - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b'); - assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), ''); - assert.equal(term.buffer.lines.get(INIT_ROWS - 2).loadCell(0, new CellData()).getChars(), 'c'); - assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), ''); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), ''); + assert.equal(term.buffer.lines.get(INIT_ROWS - 2)!.loadCell(0, new CellData()).getChars(), 'c'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1)!.loadCell(0, new CellData()).getChars(), ''); }); it('should properly scroll inside a scroll region (scrollTop set)', () => { - term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); - assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c'); }); it('should properly scroll inside a scroll region (scrollBottom set)', () => { - term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); - term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); - term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = 3; term.buffer.scrollBottom = 3; term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b'); - assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c'); - assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd'); - assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e'); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c'); + assert.equal(term.buffer.lines.get(2)!.loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(3)!.loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4)!.loadCell(0, new CellData()).getChars(), 'e'); }); it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { - term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); - term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); - term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); term.buffer.y = INIT_ROWS - 1; // Move cursor to last line term.buffer.scrollTop = 1; term.buffer.scrollBottom = 3; term.scroll(DEFAULT_ATTR_DATA.clone()); assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a'); - assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); - assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd'); - assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e'); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2)!.loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(3)!.loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4)!.loadCell(0, new CellData()).getChars(), 'e'); }); }); }); @@ -531,7 +530,6 @@ describe('Terminal', () => { let evKeyPress: any; beforeEach(() => { - term.showCursor = () => { }; term.clearSelection = () => { }; // term.compositionHelper = { // isComposing: false, @@ -724,11 +722,11 @@ describe('Terminal', () => { const cell = new CellData(); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.writeSync(high + String.fromCharCode(i)); - const tchar = term.buffer.lines.get(0).loadCell(0, cell); + const tchar = term.buffer.lines.get(0)!.loadCell(0, cell); expect(tchar.getChars()).eql(high + String.fromCharCode(i)); expect(tchar.getChars().length).eql(2); expect(tchar.getWidth()).eql(1); - expect(term.buffer.lines.get(0).loadCell(1, cell).getChars()).eql(''); + expect(term.buffer.lines.get(0)!.loadCell(1, cell).getChars()).eql(''); term.reset(); } }); @@ -738,9 +736,9 @@ describe('Terminal', () => { for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; term.writeSync(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars()).eql(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars().length).eql(2); - expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(''); + expect(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars()).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars().length).eql(2); + expect(term.buffer.lines.get(1)!.loadCell(0, cell).getChars()).eql(''); term.reset(); } }); @@ -751,10 +749,10 @@ describe('Terminal', () => { term.buffer.x = term.cols - 1; term.writeSync('a' + high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a'); - expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(1).loadCell(0, cell).getChars().length).eql(2); - expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql(''); + expect(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars()).eql('a'); + expect(term.buffer.lines.get(1)!.loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(1)!.loadCell(0, cell).getChars().length).eql(2); + expect(term.buffer.lines.get(1)!.loadCell(1, cell).getChars()).eql(''); term.reset(); } }); @@ -770,9 +768,9 @@ describe('Terminal', () => { } term.writeSync('a' + high + String.fromCharCode(i)); // auto wraparound mode should cut off the rest of the line - expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql(high + String.fromCharCode(i)); - expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(2); - expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql(''); + expect(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars()).eql(high + String.fromCharCode(i)); + expect(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars().length).eql(2); + expect(term.buffer.lines.get(1)!.loadCell(1, cell).getChars()).eql(''); term.reset(); } }); @@ -782,11 +780,11 @@ describe('Terminal', () => { for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.writeSync(high); term.writeSync(String.fromCharCode(i)); - const tchar = term.buffer.lines.get(0).loadCell(0, cell); + const tchar = term.buffer.lines.get(0)!.loadCell(0, cell); expect(tchar.getChars()).eql(high + String.fromCharCode(i)); expect(tchar.getChars().length).eql(2); expect(tchar.getWidth()).eql(1); - expect(term.buffer.lines.get(0).loadCell(1, cell).getChars()).eql(''); + expect(term.buffer.lines.get(0)!.loadCell(1, cell).getChars()).eql(''); term.reset(); } }); @@ -796,7 +794,7 @@ describe('Terminal', () => { const cell = new CellData(); it('café', () => { term.writeSync('cafe\u0301'); - term.buffer.lines.get(0).loadCell(3, cell); + term.buffer.lines.get(0)!.loadCell(3, cell); expect(cell.getChars()).eql('e\u0301'); expect(cell.getChars().length).eql(2); expect(cell.getWidth()).eql(1); @@ -804,11 +802,11 @@ describe('Terminal', () => { it('café - end of line', () => { term.buffer.x = term.cols - 1 - 3; term.writeSync('cafe\u0301'); - term.buffer.lines.get(0).loadCell(term.cols - 1, cell); + term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); expect(cell.getChars()).eql('e\u0301'); expect(cell.getChars().length).eql(2); expect(cell.getWidth()).eql(1); - term.buffer.lines.get(0).loadCell(1, cell); + term.buffer.lines.get(0)!.loadCell(1, cell); expect(cell.getChars()).eql(''); expect(cell.getChars().length).eql(0); expect(cell.getWidth()).eql(1); @@ -816,12 +814,12 @@ describe('Terminal', () => { it('multiple combined é', () => { term.writeSync(Array(100).join('e\u0301')); for (let i = 0; i < term.cols; ++i) { - term.buffer.lines.get(0).loadCell(i, cell); + term.buffer.lines.get(0)!.loadCell(i, cell); expect(cell.getChars()).eql('e\u0301'); expect(cell.getChars().length).eql(2); expect(cell.getWidth()).eql(1); } - term.buffer.lines.get(1).loadCell(0, cell); + term.buffer.lines.get(1)!.loadCell(0, cell); expect(cell.getChars()).eql('e\u0301'); expect(cell.getChars().length).eql(2); expect(cell.getWidth()).eql(1); @@ -829,12 +827,12 @@ describe('Terminal', () => { it('multiple surrogate with combined', () => { term.writeSync(Array(100).join('\uD800\uDC00\u0301')); for (let i = 0; i < term.cols; ++i) { - term.buffer.lines.get(0).loadCell(i, cell); + term.buffer.lines.get(0)!.loadCell(i, cell); expect(cell.getChars()).eql('\uD800\uDC00\u0301'); expect(cell.getChars().length).eql(3); expect(cell.getWidth()).eql(1); } - term.buffer.lines.get(1).loadCell(0, cell); + term.buffer.lines.get(1)!.loadCell(0, cell); expect(cell.getChars()).eql('\uD800\uDC00\u0301'); expect(cell.getChars().length).eql(3); expect(cell.getWidth()).eql(1); @@ -857,7 +855,7 @@ describe('Terminal', () => { it('line of ¥ even', () => { term.writeSync(Array(50).join('¥')); for (let i = 0; i < term.cols; ++i) { - term.buffer.lines.get(0).loadCell(i, cell); + term.buffer.lines.get(0)!.loadCell(i, cell); if (i % 2) { expect(cell.getChars()).eql(''); expect(cell.getChars().length).eql(0); @@ -868,7 +866,7 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); } } - term.buffer.lines.get(1).loadCell(0, cell); + term.buffer.lines.get(1)!.loadCell(0, cell); expect(cell.getChars()).eql('¥'); expect(cell.getChars().length).eql(1); expect(cell.getWidth()).eql(2); @@ -877,7 +875,7 @@ describe('Terminal', () => { term.buffer.x = 1; term.writeSync(Array(50).join('¥')); for (let i = 1; i < term.cols - 1; ++i) { - term.buffer.lines.get(0).loadCell(i, cell); + term.buffer.lines.get(0)!.loadCell(i, cell); if (!(i % 2)) { expect(cell.getChars()).eql(''); expect(cell.getChars().length).eql(0); @@ -888,11 +886,11 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); } } - term.buffer.lines.get(0).loadCell(term.cols - 1, cell); + term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); expect(cell.getChars()).eql(''); expect(cell.getChars().length).eql(0); expect(cell.getWidth()).eql(1); - term.buffer.lines.get(1).loadCell(0, cell); + term.buffer.lines.get(1)!.loadCell(0, cell); expect(cell.getChars()).eql('¥'); expect(cell.getChars().length).eql(1); expect(cell.getWidth()).eql(2); @@ -901,7 +899,7 @@ describe('Terminal', () => { term.buffer.x = 1; term.writeSync(Array(50).join('¥\u0301')); for (let i = 1; i < term.cols - 1; ++i) { - term.buffer.lines.get(0).loadCell(i, cell); + term.buffer.lines.get(0)!.loadCell(i, cell); if (!(i % 2)) { expect(cell.getChars()).eql(''); expect(cell.getChars().length).eql(0); @@ -912,11 +910,11 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); } } - term.buffer.lines.get(0).loadCell(term.cols - 1, cell); + term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); expect(cell.getChars()).eql(''); expect(cell.getChars().length).eql(0); expect(cell.getWidth()).eql(1); - term.buffer.lines.get(1).loadCell(0, cell); + term.buffer.lines.get(1)!.loadCell(0, cell); expect(cell.getChars()).eql('¥\u0301'); expect(cell.getChars().length).eql(2); expect(cell.getWidth()).eql(2); @@ -924,7 +922,7 @@ describe('Terminal', () => { it('line of ¥ with combining even', () => { term.writeSync(Array(50).join('¥\u0301')); for (let i = 0; i < term.cols; ++i) { - term.buffer.lines.get(0).loadCell(i, cell); + term.buffer.lines.get(0)!.loadCell(i, cell); if (i % 2) { expect(cell.getChars()).eql(''); expect(cell.getChars().length).eql(0); @@ -935,7 +933,7 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); } } - term.buffer.lines.get(1).loadCell(0, cell); + term.buffer.lines.get(1)!.loadCell(0, cell); expect(cell.getChars()).eql('¥\u0301'); expect(cell.getChars().length).eql(2); expect(cell.getWidth()).eql(2); @@ -944,7 +942,7 @@ describe('Terminal', () => { term.buffer.x = 1; term.writeSync(Array(50).join('\ud843\ude6d\u0301')); for (let i = 1; i < term.cols - 1; ++i) { - term.buffer.lines.get(0).loadCell(i, cell); + term.buffer.lines.get(0)!.loadCell(i, cell); if (!(i % 2)) { expect(cell.getChars()).eql(''); expect(cell.getChars().length).eql(0); @@ -955,11 +953,11 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); } } - term.buffer.lines.get(0).loadCell(term.cols - 1, cell); + term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); expect(cell.getChars()).eql(''); expect(cell.getChars().length).eql(0); expect(cell.getWidth()).eql(1); - term.buffer.lines.get(1).loadCell(0, cell); + term.buffer.lines.get(1)!.loadCell(0, cell); expect(cell.getChars()).eql('\ud843\ude6d\u0301'); expect(cell.getChars().length).eql(3); expect(cell.getWidth()).eql(2); @@ -967,7 +965,7 @@ describe('Terminal', () => { it('line of surrogate fullwidth with combining even', () => { term.writeSync(Array(50).join('\ud843\ude6d\u0301')); for (let i = 0; i < term.cols; ++i) { - term.buffer.lines.get(0).loadCell(i, cell); + term.buffer.lines.get(0)!.loadCell(i, cell); if (i % 2) { expect(cell.getChars()).eql(''); expect(cell.getChars().length).eql(0); @@ -978,7 +976,7 @@ describe('Terminal', () => { expect(cell.getWidth()).eql(2); } } - term.buffer.lines.get(1).loadCell(0, cell); + term.buffer.lines.get(1)!.loadCell(0, cell); expect(cell.getChars()).eql('\ud843\ude6d\u0301'); expect(cell.getChars().length).eql(3); expect(cell.getWidth()).eql(2); @@ -991,42 +989,42 @@ describe('Terminal', () => { term.writeSync(Array(9).join('0123456789').slice(-80)); term.buffer.x = 10; term.buffer.y = 0; - term.insertMode = true; + term.write('\x1b[4h'); term.writeSync('abcde'); - expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a'); - expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('e'); - expect(term.buffer.lines.get(0).loadCell(15, cell).getChars()).eql('0'); - expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('4'); + expect(term.buffer.lines.get(0)!.length).eql(term.cols); + expect(term.buffer.lines.get(0)!.loadCell(10, cell).getChars()).eql('a'); + expect(term.buffer.lines.get(0)!.loadCell(14, cell).getChars()).eql('e'); + expect(term.buffer.lines.get(0)!.loadCell(15, cell).getChars()).eql('0'); + expect(term.buffer.lines.get(0)!.loadCell(79, cell).getChars()).eql('4'); }); it('fullwidth - insert', () => { term.writeSync(Array(9).join('0123456789').slice(-80)); term.buffer.x = 10; term.buffer.y = 0; - term.insertMode = true; + term.write('\x1b[4h'); term.writeSync('¥¥¥'); - expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('¥'); - expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql(''); - expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('¥'); - expect(term.buffer.lines.get(0).loadCell(15, cell).getChars()).eql(''); - expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('3'); + expect(term.buffer.lines.get(0)!.length).eql(term.cols); + expect(term.buffer.lines.get(0)!.loadCell(10, cell).getChars()).eql('¥'); + expect(term.buffer.lines.get(0)!.loadCell(11, cell).getChars()).eql(''); + expect(term.buffer.lines.get(0)!.loadCell(14, cell).getChars()).eql('¥'); + expect(term.buffer.lines.get(0)!.loadCell(15, cell).getChars()).eql(''); + expect(term.buffer.lines.get(0)!.loadCell(79, cell).getChars()).eql('3'); }); it('fullwidth - right border', () => { term.writeSync(Array(41).join('¥')); term.buffer.x = 10; term.buffer.y = 0; - term.insertMode = true; + term.write('\x1b[4h'); term.writeSync('a'); - expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a'); - expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('¥'); - expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // fullwidth char got replaced + expect(term.buffer.lines.get(0)!.length).eql(term.cols); + expect(term.buffer.lines.get(0)!.loadCell(10, cell).getChars()).eql('a'); + expect(term.buffer.lines.get(0)!.loadCell(11, cell).getChars()).eql('¥'); + expect(term.buffer.lines.get(0)!.loadCell(79, cell).getChars()).eql(''); // fullwidth char got replaced term.writeSync('b'); - expect(term.buffer.lines.get(0).length).eql(term.cols); - expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('b'); - expect(term.buffer.lines.get(0).loadCell(12, cell).getChars()).eql('¥'); - expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // empty cell after fullwidth + expect(term.buffer.lines.get(0)!.length).eql(term.cols); + expect(term.buffer.lines.get(0)!.loadCell(11, cell).getChars()).eql('b'); + expect(term.buffer.lines.get(0)!.loadCell(12, cell).getChars()).eql('¥'); + expect(term.buffer.lines.get(0)!.loadCell(79, cell).getChars()).eql(''); // empty cell after fullwidth }); }); @@ -1271,9 +1269,9 @@ describe('Terminal', () => { terminal.writeSync(input); const s = terminal.buffer.iterator(true).next().content; assert.equal(input, s); - const stringIndex = s.match(/😃/).index; + const stringIndex = s.match(/😃/)!.index!; const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); - assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars(), '😃'); + assert(terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars(), '😃'); }); it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => { @@ -1300,7 +1298,7 @@ describe('Terminal', () => { assert.equal(input, s); for (let i = 0; i < input.length; ++i) { const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); + assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars()); } }); @@ -1318,7 +1316,7 @@ describe('Terminal', () => { : (i % 3 === 1) ? input.substr(i, 2) : input.substr(i - 1, 2), - terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars()); + terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars()); } }); @@ -1372,15 +1370,15 @@ describe('Terminal', () => { const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false}); normalTerminal.writeSync(data.join('')); - assert.equal(normalTerminal.buffer.lines.get(0).isWrapped, false); - assert.equal(normalTerminal.buffer.lines.get(1).isWrapped, false); - assert.equal(normalTerminal.buffer.lines.get(2).isWrapped, false); + assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false); + assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false); + assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false); const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true}); windowsModeTerminal.writeSync(data.join('')); - assert.equal(windowsModeTerminal.buffer.lines.get(0).isWrapped, false); - assert.equal(windowsModeTerminal.buffer.lines.get(1).isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character'); - assert.equal(windowsModeTerminal.buffer.lines.get(2).isWrapped, false); + assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false); + assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character'); + assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false); }); it('should mark lines as wrapped when the line ends in a non-null character after a CUP', () => { @@ -1392,15 +1390,99 @@ describe('Terminal', () => { const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false}); normalTerminal.writeSync(data.join('')); - assert.equal(normalTerminal.buffer.lines.get(0).isWrapped, false); - assert.equal(normalTerminal.buffer.lines.get(1).isWrapped, false); - assert.equal(normalTerminal.buffer.lines.get(2).isWrapped, false); + assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false); + assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false); + assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false); const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true}); windowsModeTerminal.writeSync(data.join('')); - assert.equal(windowsModeTerminal.buffer.lines.get(0).isWrapped, false); - assert.equal(windowsModeTerminal.buffer.lines.get(1).isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character'); - assert.equal(windowsModeTerminal.buffer.lines.get(2).isWrapped, false); + assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false); + assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character'); + assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false); + }); + }); + it('convertEol setting', function(): void { + // not converting + const termNotConverting = new TestTerminal({cols: 15, rows: 10}); + termNotConverting.writeSync('Hello\nWorld'); + expect(termNotConverting.buffer.lines.get(0)!.translateToString(false)).equals('Hello '); + expect(termNotConverting.buffer.lines.get(1)!.translateToString(false)).equals(' World '); + expect(termNotConverting.buffer.lines.get(0)!.translateToString(true)).equals('Hello'); + expect(termNotConverting.buffer.lines.get(1)!.translateToString(true)).equals(' World'); + + // converting + const termConverting = new TestTerminal({cols: 15, rows: 10, convertEol: true}); + termConverting.writeSync('Hello\nWorld'); + expect(termConverting.buffer.lines.get(0)!.translateToString(false)).equals('Hello '); + expect(termConverting.buffer.lines.get(1)!.translateToString(false)).equals('World '); + expect(termConverting.buffer.lines.get(0)!.translateToString(true)).equals('Hello'); + expect(termConverting.buffer.lines.get(1)!.translateToString(true)).equals('World'); + }); + describe('Terminal InputHandler integration', () => { + function getLines(term: TestTerminal, limit: number = term.rows): string[] { + const res: string[] = []; + for (let i = 0; i < limit; ++i) { + res.push(term.buffer.lines.get(i)!.translateToString(true)); + } + return res; + } + + // This suite cannot live in InputHandler unless Terminal.scroll moved into IBufferService + describe('SL/SR/DECIC/DECDC', () => { + let term: TestTerminal; + beforeEach(() => { + term = new TestTerminal({cols: 5, rows: 5, scrollback: 1}); + }); + it('SL (scrollLeft)', () => { + term.writeSync('12345'.repeat(6)); + term.writeSync('\x1b[ @'); + assert.deepEqual(getLines(term, term.rows + 1), ['12345', '2345', '2345', '2345', '2345', '2345']); + term.writeSync('\x1b[0 @'); + assert.deepEqual(getLines(term, term.rows + 1), ['12345', '345', '345', '345', '345', '345']); + term.writeSync('\x1b[2 @'); + assert.deepEqual(getLines(term, term.rows + 1), ['12345', '5', '5', '5', '5', '5']); + }); + it('SR (scrollRight)', () => { + term.writeSync('12345'.repeat(6)); + term.writeSync('\x1b[ A'); + assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']); + term.writeSync('\x1b[0 A'); + assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']); + term.writeSync('\x1b[2 A'); + assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']); + }); + it('insertColumns (DECIC)', () => { + term.writeSync('12345'.repeat(6)); + term.writeSync('\x1b[3;3H'); + term.writeSync('\x1b[\'}'); + assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); + term.reset(); + term.writeSync('12345'.repeat(6)); + term.writeSync('\x1b[3;3H'); + term.writeSync('\x1b[1\'}'); + assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); + term.reset(); + term.writeSync('12345'.repeat(6)); + term.writeSync('\x1b[3;3H'); + term.writeSync('\x1b[2\'}'); + assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']); + }); + it('deleteColumns (DECDC)', () => { + term.writeSync('12345'.repeat(6)); + term.writeSync('\x1b[3;3H'); + term.writeSync('\x1b[\'~'); + assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']); + term.reset(); + term.writeSync('12345'.repeat(6)); + term.writeSync('\x1b[3;3H'); + term.writeSync('\x1b[1\'~'); + assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']); + term.reset(); + term.writeSync('12345'.repeat(6)); + term.writeSync('\x1b[3;3H'); + term.writeSync('\x1b[2\'~'); + assert.deepEqual(getLines(term, term.rows + 1), ['12345', '125', '125', '125', '125', '125']); + }); }); }); }); diff --git a/src/Terminal.ts b/src/browser/Terminal.ts similarity index 59% rename from src/Terminal.ts rename to src/browser/Terminal.ts index e3a3801d..7f3ead61 100644 --- a/src/Terminal.ts +++ b/src/browser/Terminal.ts @@ -21,13 +21,13 @@ * http://linux.die.net/man/7/urxvt */ -import { IInputHandlingTerminal, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, CustomKeyEventHandler } from './Types'; +import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2 } from 'browser/Types'; import { IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types'; import { CompositionHelper } from 'browser/input/CompositionHelper'; import { Viewport } from 'browser/Viewport'; import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard'; import { C0 } from 'common/data/EscapeSequences'; -import { InputHandler } from './InputHandler'; +import { WindowsOptionsReportType } from '../common/InputHandler'; import { Renderer } from 'browser/renderer/Renderer'; import { Linkifier } from 'browser/Linkifier'; import { SelectionService } from 'browser/services/SelectionService'; @@ -39,89 +39,50 @@ import { MouseZoneManager } from 'browser/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm'; import { DomRenderer } from 'browser/renderer/dom/DomRenderer'; -import { IKeyboardEvent, KeyboardResultType, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; +import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions } from 'common/Types'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; +import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; -import { updateWindowsModeWrappedState } from 'common/WindowsMode'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; -import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService, ICharsetService, IUnicodeService } from 'common/services/Services'; -import { OptionsService } from 'common/services/OptionsService'; import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; -import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; -import { Disposable } from 'common/Lifecycle'; -import { IBufferSet, IBuffer } from 'common/buffer/Types'; +import { IBuffer } from 'common/buffer/Types'; import { MouseService } from 'browser/services/MouseService'; -import { IParams, IFunctionIdentifier } from 'common/parser/Types'; -import { CoreService } from 'common/services/CoreService'; -import { LogService } from 'common/services/LogService'; -import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2 } from 'browser/Types'; -import { DirtyRowService } from 'common/services/DirtyRowService'; -import { InstantiationService } from 'common/services/InstantiationService'; -import { CoreMouseService } from 'common/services/CoreMouseService'; -import { WriteBuffer } from 'common/input/WriteBuffer'; import { Linkifier2 } from 'browser/Linkifier2'; import { CoreBrowserService } from 'browser/services/CoreBrowserService'; -import { UnicodeService } from 'common/services/UnicodeService'; -import { CharsetService } from 'common/services/CharsetService'; +import { CoreTerminal } from 'common/CoreTerminal'; +import { ITerminalOptions as IInitializedTerminalOptions } from 'common/services/Services'; // Let it work inside Node.js for automated testing purposes. -const document = (typeof window !== 'undefined') ? window.document : null; +const document: Document = (typeof window !== 'undefined') ? window.document : null as any; +export class Terminal extends CoreTerminal implements ITerminal { + public textarea: HTMLTextAreaElement | undefined; + public element: HTMLElement | undefined; + public screenElement: HTMLElement | undefined; -export class Terminal extends Disposable implements ITerminal, IDisposable, IInputHandlingTerminal { - public textarea: HTMLTextAreaElement; - public element: HTMLElement; - public screenElement: HTMLElement; + private _document: Document | undefined; + private _viewportScrollArea: HTMLElement | undefined; + private _viewportElement: HTMLElement | undefined; + private _helperContainer: HTMLElement | undefined; + private _compositionView: HTMLElement | undefined; - private _document: Document; - private _viewportScrollArea: HTMLElement; - private _viewportElement: HTMLElement; - private _helperContainer: HTMLElement; - private _compositionView: HTMLElement; - - private _visualBellTimer: number; + // private _visualBellTimer: number; public browser: IBrowser = Browser; // TODO: We should remove options once components adopt optionsService - public get options(): ITerminalOptions { return this.optionsService.options; } + public get options(): IInitializedTerminalOptions { return this.optionsService.options; } - private _customKeyEventHandler: CustomKeyEventHandler; - - // common services - private _bufferService: IBufferService; - private _coreService: ICoreService; - private _charsetService: ICharsetService; - private _coreMouseService: ICoreMouseService; - private _dirtyRowService: IDirtyRowService; - private _instantiationService: IInstantiationService; - private _logService: ILogService; - public optionsService: IOptionsService; - public unicodeService: IUnicodeService; + private _customKeyEventHandler: CustomKeyEventHandler | undefined; // browser services - private _charSizeService: ICharSizeService; - private _mouseService: IMouseService; - private _renderService: IRenderService; - private _selectionService: ISelectionService; - private _soundService: ISoundService; - - // modes - public insertMode: boolean; - public bracketedPasteMode: boolean; - - // mouse properties - public mouseEvents: CoreMouseEventType = CoreMouseEventType.NONE; - public sendFocus: boolean; - - // write buffer - private _writeBuffer: WriteBuffer; - - // Store if user went browsing history in scrollback - private _userScrolling: boolean; + private _charSizeService: ICharSizeService | undefined; + private _mouseService: IMouseService | undefined; + private _renderService: IRenderService | undefined; + private _selectionService: ISelectionService | undefined; + private _soundService: ISoundService | undefined; /** * Records whether the keydown event has already been handled and triggered a data event, if so @@ -130,39 +91,21 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp */ private _keyDownHandled: boolean = false; - private _inputHandler: InputHandler; public linkifier: ILinkifier; public linkifier2: ILinkifier2; - public viewport: IViewport; - private _compositionHelper: ICompositionHelper; - private _mouseZoneManager: IMouseZoneManager; - private _accessibilityManager: AccessibilityManager; - private _colorManager: ColorManager; - private _theme: ITheme; - private _windowsMode: IDisposable | undefined; - - // bufferline to clone/copy from for new blank lines - private _blankLine: IBufferLine = null; - - public get cols(): number { return this._bufferService.cols; } - public get rows(): number { return this._bufferService.rows; } + public viewport: IViewport | undefined; + private _compositionHelper: ICompositionHelper | undefined; + private _mouseZoneManager: IMouseZoneManager | undefined; + private _accessibilityManager: AccessibilityManager | undefined; + private _colorManager: ColorManager | undefined; + private _theme: ITheme | undefined; private _onCursorMove = new EventEmitter(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } - private _onData = new EventEmitter(); - public get onData(): IEvent { return this._onData.event; } - private _onBinary = new EventEmitter(); - public get onBinary(): IEvent { return this._onBinary.event; } private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>(); public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; } - private _onLineFeed = new EventEmitter(); - public get onLineFeed(): IEvent { return this._onLineFeed.event; } private _onRender = new EventEmitter<{ start: number, end: number }>(); public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; } - private _onResize = new EventEmitter<{ cols: number, rows: number }>(); - public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } - private _onScroll = new EventEmitter(); - public get onScroll(): IEvent { return this._onScroll.event; } private _onSelectionChange = new EventEmitter(); public get onSelectionChange(): IEvent { return this._onSelectionChange.event; } private _onTitleChange = new EventEmitter(); @@ -172,10 +115,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp public get onFocus(): IEvent { return this._onFocus.event; } private _onBlur = new EventEmitter(); public get onBlur(): IEvent { return this._onBlur.event; } - public onA11yCharEmitter = new EventEmitter(); - public get onA11yChar(): IEvent { return this.onA11yCharEmitter.event; } - public onA11yTabEmitter = new EventEmitter(); - public get onA11yTab(): IEvent { return this.onA11yTabEmitter.event; } + private _onA11yCharEmitter = new EventEmitter(); + public get onA11yChar(): IEvent { return this._onA11yCharEmitter.event; } + private _onA11yTabEmitter = new EventEmitter(); + public get onA11yTab(): IEvent { return this._onA11yTabEmitter.event; } /** * Creates a new `Terminal` object. @@ -192,33 +135,26 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp constructor( options: ITerminalOptions = {} ) { - super(); + super(options); - // Setup and initialize common services - this._instantiationService = new InstantiationService(); - this.optionsService = new OptionsService(options); - this._instantiationService.setService(IOptionsService, this.optionsService); - this._bufferService = this._instantiationService.createInstance(BufferService); - this._instantiationService.setService(IBufferService, this._bufferService); - this._logService = this._instantiationService.createInstance(LogService); - this._instantiationService.setService(ILogService, this._logService); - this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom()); - this._instantiationService.setService(ICoreService, this._coreService); - this._coreService.onData(e => this._onData.fire(e)); - this._coreService.onBinary(e => this._onBinary.fire(e)); - this._coreMouseService = this._instantiationService.createInstance(CoreMouseService); - this._instantiationService.setService(ICoreMouseService, this._coreMouseService); - this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); - this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); - this.unicodeService = this._instantiationService.createInstance(UnicodeService); - this._instantiationService.setService(IUnicodeService, this.unicodeService); - this._charsetService = this._instantiationService.createInstance(CharsetService); - this._instantiationService.setService(ICharsetService, this._charsetService); - - this._setupOptionsListeners(); this._setup(); - this._writeBuffer = new WriteBuffer(data => this._inputHandler.parse(data)); + this.linkifier = this._instantiationService.createInstance(Linkifier); + this.linkifier2 = this._instantiationService.createInstance(Linkifier2); + + // Setup InputHandler listeners + this.register(this._inputHandler.onRequestBell(() => this.bell())); + this.register(this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end))); + this.register(this._inputHandler.onRequestReset(() => this.reset())); + this.register(this._inputHandler.onRequestScroll((eraseAttr, isWrapped) => this.scroll(eraseAttr, isWrapped || undefined))); + this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type))); + this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove)); + this.register(forwardEvent(this._inputHandler.onTitleChange, this._onTitleChange)); + this.register(forwardEvent(this._inputHandler.onA11yChar, this._onA11yCharEmitter)); + this.register(forwardEvent(this._inputHandler.onA11yTab, this._onA11yTabEmitter)); + + // Setup listeners + this._bufferService.onResize(e => this._afterResize(e.cols, e.rows)); } public dispose(): void { @@ -226,62 +162,16 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return; } super.dispose(); - this._windowsMode?.dispose(); - this._windowsMode = undefined; this._renderService?.dispose(); - this._customKeyEventHandler = null; + this._customKeyEventHandler = undefined; this.write = () => { }; this.element?.parentNode?.removeChild(this.element); } - private _setup(): void { - this._customKeyEventHandler = null; + protected _setup(): void { + super._setup(); - // modes - this.insertMode = false; - this.bracketedPasteMode = false; - - this._userScrolling = false; - - if (this._inputHandler) { - 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._instantiationService); - this._inputHandler.onRequestBell(() => this.bell()); - this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)); - this._inputHandler.onRequestReset(() => this.reset()); - this._inputHandler.onCursorMove(() => this._onCursorMove.fire()); - this._inputHandler.onLineFeed(() => this._onLineFeed.fire()); - this.register(this._inputHandler); - } - - if (!this.linkifier) { - this.linkifier = this._instantiationService.createInstance(Linkifier); - } - if (!this.linkifier2) { - this.linkifier2 = this._instantiationService.createInstance(Linkifier2); - } - - if (this.options.windowsMode) { - this._enableWindowsMode(); - } - } - - private _enableWindowsMode(): void { - if (!this._windowsMode) { - const disposables: IDisposable[] = []; - disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService))); - disposables.push(this.addCsiHandler({ final: 'H' }, () => { - updateWindowsModeWrappedState(this._bufferService); - return false; - })); - this._windowsMode = { - dispose: () => { - disposables.forEach(d => d.dispose()); - } - }; - } + this._customKeyEventHandler = undefined; } /** @@ -291,10 +181,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return this.buffers.active; } - public get buffers(): IBufferSet { - return this._bufferService.buffers; - } - /** * Focus the terminal. Delegates focus handling to the terminal's DOM element. */ @@ -304,80 +190,71 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } } - private _setupOptionsListeners(): void { + protected _updateOptions(key: string): void { + super._updateOptions(key); + // TODO: These listeners should be owned by individual components - this.optionsService.onOptionChange(key => { - switch (key) { - case 'fontFamily': - case 'fontSize': - // When the font changes the size of the cells may change which requires a renderer clear - this._renderService?.clear(); - this._charSizeService?.measure(); - break; - case 'cursorBlink': - case 'cursorStyle': - // The DOM renderer needs a row refresh to update the cursor styles - this.refresh(this.buffer.y, this.buffer.y); - break; - case 'drawBoldTextInBrightColors': - case 'letterSpacing': - case 'lineHeight': - case 'fontWeight': - case 'fontWeightBold': - case 'minimumContrastRatio': - // When the font changes the size of the cells may change which requires a renderer clear - if (this._renderService) { - this._renderService.clear(); - this._renderService.onResize(this.cols, this.rows); - this.refresh(0, this.rows - 1); + switch (key) { + case 'fontFamily': + case 'fontSize': + // When the font changes the size of the cells may change which requires a renderer clear + this._renderService?.clear(); + this._charSizeService?.measure(); + break; + case 'cursorBlink': + case 'cursorStyle': + // The DOM renderer needs a row refresh to update the cursor styles + this.refresh(this.buffer.y, this.buffer.y); + break; + case 'drawBoldTextInBrightColors': + case 'letterSpacing': + case 'lineHeight': + case 'fontWeight': + case 'fontWeightBold': + case 'minimumContrastRatio': + // When the font changes the size of the cells may change which requires a renderer clear + if (this._renderService) { + this._renderService.clear(); + this._renderService.onResize(this.cols, this.rows); + this.refresh(0, this.rows - 1); + } + break; + case 'rendererType': + if (this._renderService) { + this._renderService.setRenderer(this._createRenderer()); + this._renderService.onResize(this.cols, this.rows); + } + break; + case 'scrollback': + this.viewport?.syncScrollArea(); + break; + case 'screenReaderMode': + if (this.optionsService.options.screenReaderMode) { + if (!this._accessibilityManager && this._renderService) { + this._accessibilityManager = new AccessibilityManager(this, this._renderService); } - break; - case 'rendererType': - if (this._renderService) { - this._renderService.setRenderer(this._createRenderer()); - this._renderService.onResize(this.cols, this.rows); - } - break; - case 'scrollback': - this.buffers.resize(this.cols, this.rows); - this.viewport?.syncScrollArea(); - break; - case 'screenReaderMode': - if (this.optionsService.options.screenReaderMode) { - if (!this._accessibilityManager && this._renderService) { - this._accessibilityManager = new AccessibilityManager(this, this._renderService); - } - } else { - this._accessibilityManager?.dispose(); - this._accessibilityManager = null; - } - break; - case 'tabStopWidth': this.buffers.setupTabStops(); break; - case 'theme': - this._setTheme(this.optionsService.options.theme); - break; - case 'windowsMode': - if (this.optionsService.options.windowsMode) { - this._enableWindowsMode(); - } else { - this._windowsMode?.dispose(); - this._windowsMode = undefined; - } - break; - } - }); + } else { + this._accessibilityManager?.dispose(); + this._accessibilityManager = undefined; + } + break; + case 'tabStopWidth': this.buffers.setupTabStops(); break; + case 'theme': + this._setTheme(this.optionsService.options.theme); + break; + } } /** * Binds the desired focus behavior on a given terminal object. */ private _onTextAreaFocus(ev: KeyboardEvent): void { - if (this.sendFocus) { + if (this._coreService.decPrivateModes.sendFocus) { this._coreService.triggerDataEvent(C0.ESC + '[I'); } this.updateCursorStyle(ev); - this.element.classList.add('focus'); - this.showCursor(); + this.element!.classList.add('focus'); + this._showCursor(); this._onFocus.fire(); } @@ -386,7 +263,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * textarea. */ public blur(): void { - return this.textarea.blur(); + return this.textarea?.blur(); } /** @@ -395,12 +272,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _onTextAreaBlur(): void { // Text can safely be removed on blur. Doing it earlier could interfere with // screen readers reading it out. - this.textarea.value = ''; + this.textarea!.value = ''; this.refresh(this.buffer.y, this.buffer.y); - if (this.sendFocus) { + if (this._coreService.decPrivateModes.sendFocus) { this._coreService.triggerDataEvent(C0.ESC + '[O'); } - this.element.classList.remove('focus'); + this.element!.classList.remove('focus'); this._onBlur.fire(); } @@ -411,29 +288,29 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._bindKeys(); // Bind clipboard functionality - this.register(addDisposableDomListener(this.element, 'copy', (event: ClipboardEvent) => { + this.register(addDisposableDomListener(this.element!, 'copy', (event: ClipboardEvent) => { // If mouse events are active it means the selection manager is disabled and // copy should be handled by the host program. if (!this.hasSelection()) { return; } - copyHandler(event, this._selectionService); + copyHandler(event, this._selectionService!); })); - const pasteHandlerWrapper = (event: ClipboardEvent): void => handlePasteEvent(event, this.textarea, this.bracketedPasteMode, this._coreService); - this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper)); - this.register(addDisposableDomListener(this.element, 'paste', pasteHandlerWrapper)); + const pasteHandlerWrapper = (event: ClipboardEvent): void => handlePasteEvent(event, this.textarea!, this._coreService); + this.register(addDisposableDomListener(this.textarea!, 'paste', pasteHandlerWrapper)); + this.register(addDisposableDomListener(this.element!, 'paste', pasteHandlerWrapper)); // Handle right click context menus if (Browser.isFirefox) { // Firefox doesn't appear to fire the contextmenu event on right click - this.register(addDisposableDomListener(this.element, 'mousedown', (event: MouseEvent) => { + this.register(addDisposableDomListener(this.element!, 'mousedown', (event: MouseEvent) => { if (event.button === 2) { - rightClickHandler(event, this.textarea, this.screenElement, this._selectionService, this.options.rightClickSelectsWord); + rightClickHandler(event, this.textarea!, this.screenElement!, this._selectionService!, this.options.rightClickSelectsWord); } })); } else { - this.register(addDisposableDomListener(this.element, 'contextmenu', (event: MouseEvent) => { - rightClickHandler(event, this.textarea, this.screenElement, this._selectionService, this.options.rightClickSelectsWord); + this.register(addDisposableDomListener(this.element!, 'contextmenu', (event: MouseEvent) => { + rightClickHandler(event, this.textarea!, this.screenElement!, this._selectionService!, this.options.rightClickSelectsWord); })); } @@ -443,9 +320,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp if (Browser.isLinux) { // Use auxclick event over mousedown the latter doesn't seem to work. Note // that the regular click event doesn't fire for the middle mouse button. - this.register(addDisposableDomListener(this.element, 'auxclick', (event: MouseEvent) => { + this.register(addDisposableDomListener(this.element!, 'auxclick', (event: MouseEvent) => { if (event.button === 1) { - moveTextAreaUnderMouseCursor(event, this.textarea, this.screenElement); + moveTextAreaUnderMouseCursor(event, this.textarea!, this.screenElement!); } })); } @@ -455,13 +332,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * Apply key handling to the terminal */ private _bindKeys(): void { - this.register(addDisposableDomListener(this.textarea, 'keyup', (ev: KeyboardEvent) => this._keyUp(ev), true)); - this.register(addDisposableDomListener(this.textarea, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true)); - this.register(addDisposableDomListener(this.textarea, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true)); - this.register(addDisposableDomListener(this.textarea, 'compositionstart', () => this._compositionHelper.compositionstart())); - this.register(addDisposableDomListener(this.textarea, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper.compositionupdate(e))); - this.register(addDisposableDomListener(this.textarea, 'compositionend', () => this._compositionHelper.compositionend())); - this.register(this.onRender(() => this._compositionHelper.updateCompositionElements())); + this.register(addDisposableDomListener(this.textarea!, 'keyup', (ev: KeyboardEvent) => this._keyUp(ev), true)); + this.register(addDisposableDomListener(this.textarea!, 'keydown', (ev: KeyboardEvent) => this._keyDown(ev), true)); + this.register(addDisposableDomListener(this.textarea!, 'keypress', (ev: KeyboardEvent) => this._keyPress(ev), true)); + this.register(addDisposableDomListener(this.textarea!, 'compositionstart', () => this._compositionHelper!.compositionstart())); + this.register(addDisposableDomListener(this.textarea!, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper!.compositionupdate(e))); + this.register(addDisposableDomListener(this.textarea!, 'compositionend', () => this._compositionHelper!.compositionend())); + this.register(this.onRender(() => this._compositionHelper!.updateCompositionElements())); this.register(this.onRender(e => this._queueLinkification(e.start, e.end))); } @@ -479,7 +356,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._logService.debug('Terminal.open was called on an element that was not attached to the DOM'); } - this._document = parent.ownerDocument; + this._document = parent.ownerDocument!; // Create main element container this.element = this._document.createElement('div'); @@ -535,16 +412,15 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.element.appendChild(fragment); this._theme = this.options.theme || this._theme; - this.options.theme = undefined; this._colorManager = new ColorManager(document, this.options.allowTransparency); - this.optionsService.onOptionChange(e => this._colorManager.onOptionsChange(e)); + this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e)); this._colorManager.setTheme(this._theme); const renderer = this._createRenderer(); this._renderService = this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement); this._instantiationService.setService(IRenderService, this._renderService); this._renderService.onRenderedBufferChange(e => this._onRender.fire(e)); - this.onResize(e => this._renderService.resize(e.cols, e.rows)); + this.onResize(e => this._renderService!.resize(e.cols, e.rows)); this._soundService = this._instantiationService.createInstance(SoundService); this._instantiationService.setService(ISoundService, this._soundService); @@ -557,13 +433,14 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._viewportScrollArea ); this.viewport.onThemeChange(this._colorManager.colors); + this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea())); this.register(this.viewport); - this.register(this.onCursorMove(() => this._renderService.onCursorMove())); - this.register(this.onResize(() => this._renderService.onResize(this.cols, this.rows))); - this.register(this.onBlur(() => this._renderService.onBlur())); - this.register(this.onFocus(() => this._renderService.onFocus())); - this.register(this._renderService.onDimensionsChange(() => this.viewport.syncScrollArea())); + this.register(this.onCursorMove(() => this._renderService!.onCursorMove())); + this.register(this.onResize(() => this._renderService!.onResize(this.cols, this.rows))); + this.register(this.onBlur(() => this._renderService!.onBlur())); + this.register(this.onFocus(() => this._renderService!.onFocus())); + this.register(this._renderService.onDimensionsChange(() => this.viewport!.syncScrollArea())); this._selectionService = this._instantiationService.createInstance(SelectionService, (amount: number, suppressEvent: boolean) => this.scrollLines(amount, suppressEvent), @@ -571,32 +448,32 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.screenElement); this._instantiationService.setService(ISelectionService, this._selectionService); this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire())); - this.register(this._selectionService.onRedrawRequest(e => this._renderService.onSelectionChanged(e.start, e.end, e.columnSelectMode))); + this.register(this._selectionService.onRedrawRequest(e => this._renderService!.onSelectionChanged(e.start, e.end, e.columnSelectMode))); this.register(this._selectionService.onLinuxMouseSelection(text => { // If there's a new selection, put it into the textarea, focus and select it // in order to register it as a selection on the OS. This event is fired // only on Linux to enable middle click to paste selection. - this.textarea.value = text; - this.textarea.focus(); - this.textarea.select(); + this.textarea!.value = text; + this.textarea!.focus(); + this.textarea!.select(); })); this.register(this.onScroll(() => { - this.viewport.syncScrollArea(); - this._selectionService.refresh(); + this.viewport!.syncScrollArea(); + this._selectionService!.refresh(); })); - this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService.refresh())); + this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService!.refresh())); this._mouseZoneManager = this._instantiationService.createInstance(MouseZoneManager, this.element, this.screenElement); this.register(this._mouseZoneManager); - this.register(this.onScroll(() => this._mouseZoneManager.clearAll())); + this.register(this.onScroll(() => this._mouseZoneManager!.clearAll())); this.linkifier.attachToDom(this.element, this._mouseZoneManager); this.linkifier2.attachToDom(this.element, this._mouseService, this._renderService); // This event listener must be registered aftre MouseZoneManager is created - this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService.onMouseDown(e))); + this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e))); // apply mouse event classes set by escape codes before terminal was attached - if (this.mouseEvents) { + if (this._coreMouseService.areMouseEventsActive) { this._selectionService.disable(); this.element.classList.add('enable-mouse-events'); } else { @@ -625,8 +502,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _createRenderer(): IRenderer { switch (this.options.rendererType) { - case 'canvas': return this._instantiationService.createInstance(Renderer, this._colorManager.colors, this.screenElement, this.linkifier, this.linkifier2); - case 'dom': return this._instantiationService.createInstance(DomRenderer, this._colorManager.colors, this.element, this.screenElement, this._viewportElement, this.linkifier, this.linkifier2); + case 'canvas': return this._instantiationService.createInstance(Renderer, this._colorManager!.colors, this.screenElement!, this.linkifier, this.linkifier2); + case 'dom': return this._instantiationService.createInstance(DomRenderer, this._colorManager!.colors, this.element!, this.screenElement!, this._viewportElement!, this.linkifier, this.linkifier2); default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); } } @@ -638,8 +515,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp private _setTheme(theme: ITheme): void { this._theme = theme; this._colorManager?.setTheme(theme); - this._renderService?.setColors(this._colorManager.colors); - this.viewport?.onThemeChange(this._colorManager.colors); + this._renderService?.setColors(this._colorManager!.colors); + this.viewport?.onThemeChange(this._colorManager!.colors); } /** @@ -659,18 +536,18 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp */ public bindMouse(): void { const self = this; - const el = this.element; + const el = this.element!; // send event to CoreMouseService function sendEvent(ev: MouseEvent | WheelEvent): boolean { // get mouse coordinates - const pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows); + const pos = self._mouseService!.getRawByteCoords(ev, self.screenElement!, self.cols, self.rows); if (!pos) { return false; } let but: CoreMouseButton; - let action: CoreMouseAction; + let action: CoreMouseAction | undefined; switch ((ev).overrideType || ev.type) { case 'mousemove': action = CoreMouseAction.MOVE; @@ -739,14 +616,14 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp mousedrag: null, mousemove: null }; - const eventListeners: { [key: string]: (ev: Event) => void } = { + const eventListeners: { [key: string]: (ev: any) => void | boolean } = { mouseup: (ev: MouseEvent) => { sendEvent(ev); if (!ev.buttons) { // if no other button is held remove global handlers - this._document.removeEventListener('mouseup', requestedEvents.mouseup); + this._document!.removeEventListener('mouseup', requestedEvents.mouseup!); if (requestedEvents.mousedrag) { - this._document.removeEventListener('mousemove', requestedEvents.mousedrag); + this._document!.removeEventListener('mousemove', requestedEvents.mousedrag); } } return this.cancel(ev); @@ -771,23 +648,22 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp }; this._coreMouseService.onProtocolChange(events => { // apply global changes on events - this.mouseEvents = events; if (events) { if (this.optionsService.options.logLevel === 'debug') { this._logService.debug('Binding to mouse events:', this._coreMouseService.explainEvents(events)); } - this.element.classList.add('enable-mouse-events'); - this._selectionService.disable(); + this.element!.classList.add('enable-mouse-events'); + this._selectionService!.disable(); } else { this._logService.debug('Unbinding from mouse events.'); - this.element.classList.remove('enable-mouse-events'); - this._selectionService.enable(); + this.element!.classList.remove('enable-mouse-events'); + this._selectionService!.enable(); } // add/remove handlers from requestedEvents if (!(events & CoreMouseEventType.MOVE)) { - el.removeEventListener('mousemove', requestedEvents.mousemove); + el.removeEventListener('mousemove', requestedEvents.mousemove!); requestedEvents.mousemove = null; } else if (!requestedEvents.mousemove) { el.addEventListener('mousemove', eventListeners.mousemove); @@ -795,7 +671,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } if (!(events & CoreMouseEventType.WHEEL)) { - el.removeEventListener('wheel', requestedEvents.wheel); + el.removeEventListener('wheel', requestedEvents.wheel!); requestedEvents.wheel = null; } else if (!requestedEvents.wheel) { el.addEventListener('wheel', eventListeners.wheel); @@ -803,14 +679,14 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } if (!(events & CoreMouseEventType.UP)) { - this._document.removeEventListener('mouseup', requestedEvents.mouseup); + this._document!.removeEventListener('mouseup', requestedEvents.mouseup!); requestedEvents.mouseup = null; } else if (!requestedEvents.mouseup) { requestedEvents.mouseup = eventListeners.mouseup; } if (!(events & CoreMouseEventType.DRAG)) { - this._document.removeEventListener('mousemove', requestedEvents.mousedrag); + this._document!.removeEventListener('mousemove', requestedEvents.mousedrag!); requestedEvents.mousedrag = null; } else if (!requestedEvents.mousedrag) { requestedEvents.mousedrag = eventListeners.mousedrag; @@ -829,7 +705,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // Don't send the mouse button to the pty if mouse events are disabled or // if the selection manager is having selection forced (ie. a modifier is // held). - if (!this.mouseEvents || this._selectionService.shouldForceSelection(ev)) { + if (!this._coreMouseService.areMouseEventsActive || this._selectionService!.shouldForceSelection(ev)) { return; } @@ -840,10 +716,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // Note: Other emulators also do this for 'mousedown' while a button // is held, we currently limit 'mousedown' to the terminal only. if (requestedEvents.mouseup) { - this._document.addEventListener('mouseup', requestedEvents.mouseup); + this._document!.addEventListener('mouseup', requestedEvents.mouseup); } if (requestedEvents.mousedrag) { - this._document.addEventListener('mousemove', requestedEvents.mousedrag); + this._document!.addEventListener('mousemove', requestedEvents.mousedrag); } return this.cancel(ev); @@ -854,7 +730,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // Convert wheel events into up/down events when the buffer does not have scrollback, this // enables scrolling in apps hosted in the alt buffer such as vim or tmux. if (!this.buffer.hasScrollback) { - const amount = this.viewport.getLinesScrolled(ev); + const amount = this.viewport!.getLinesScrolled(ev); // Do nothing if there's no vertical scroll if (amount === 0) { @@ -877,20 +753,20 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // the shell for example this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => { if (requestedEvents.wheel) return; - if (!this.viewport.onWheel(ev)) { + if (!this.viewport!.onWheel(ev)) { return this.cancel(ev); } })); this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => { - if (this.mouseEvents) return; - this.viewport.onTouchStart(ev); + if (this._coreMouseService.areMouseEventsActive) return; + this.viewport!.onTouchStart(ev); return this.cancel(ev); })); this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => { - if (this.mouseEvents) return; - if (!this.viewport.onTouchMove(ev)) { + if (this._coreMouseService.areMouseEventsActive) return; + if (!this.viewport!.onTouchMove(ev)) { return this.cancel(ev); } })); @@ -921,150 +797,29 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp */ public updateCursorStyle(ev: KeyboardEvent): void { if (this._selectionService && this._selectionService.shouldColumnSelect(ev)) { - this.element.classList.add('column-select'); + this.element!.classList.add('column-select'); } else { - this.element.classList.remove('column-select'); + this.element!.classList.remove('column-select'); } } /** * Display the cursor element */ - public showCursor(): void { + private _showCursor(): void { if (!this._coreService.isCursorInitialized) { this._coreService.isCursorInitialized = true; this.refresh(this.buffer.y, this.buffer.y); } } - /** - * Scroll the terminal down 1 row, creating a blank line. - * @param isWrapped Whether the new line is wrapped from the previous line. - */ - public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void { - let newLine: IBufferLine; - newLine = this._blankLine; - if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) { - newLine = this.buffer.getBlankLine(eraseAttr, isWrapped); - this._blankLine = newLine; - } - newLine.isWrapped = isWrapped; - - const topRow = this.buffer.ybase + this.buffer.scrollTop; - const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; - - if (this.buffer.scrollTop === 0) { - // Determine whether the buffer is going to be trimmed after insertion. - const willBufferBeTrimmed = this.buffer.lines.isFull; - - // Insert the line using the fastest method - if (bottomRow === this.buffer.lines.length - 1) { - if (willBufferBeTrimmed) { - this.buffer.lines.recycle().copyFrom(newLine); - } else { - this.buffer.lines.push(newLine.clone()); - } - } else { - this.buffer.lines.splice(bottomRow + 1, 0, newLine.clone()); - } - - // Only adjust ybase and ydisp when the buffer is not trimmed - if (!willBufferBeTrimmed) { - this.buffer.ybase++; - // Only scroll the ydisp with ybase if the user has not scrolled up - if (!this._userScrolling) { - this.buffer.ydisp++; - } - } else { - // When the buffer is full and the user has scrolled up, keep the text - // stable unless ydisp is right at the top - if (this._userScrolling) { - this.buffer.ydisp = Math.max(this.buffer.ydisp - 1, 0); - } - } - } else { - // scrollTop is non-zero which means no line will be going to the - // scrollback, instead we can just shift them in-place. - const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */; - this.buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); - this.buffer.lines.set(bottomRow, newLine.clone()); - } - - // Move the viewport to the bottom of the buffer unless the user is - // scrolling. - if (!this._userScrolling) { - this.buffer.ydisp = this.buffer.ybase; - } - - // Flag rows that need updating - this._dirtyRowService.markRangeDirty(this.buffer.scrollTop, this.buffer.scrollBottom); - - this._onScroll.fire(this.buffer.ydisp); - } - - /** - * Scroll the display of the terminal - * @param disp The number of lines to scroll down (negative scroll up). - * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used - * to avoid unwanted events being handled by the viewport when the event was triggered from the - * viewport originally. - */ public scrollLines(disp: number, suppressScrollEvent?: boolean): void { - if (disp < 0) { - if (this.buffer.ydisp === 0) { - return; - } - this._userScrolling = true; - } else if (disp + this.buffer.ydisp >= this.buffer.ybase) { - this._userScrolling = false; - } - - const oldYdisp = this.buffer.ydisp; - this.buffer.ydisp = Math.max(Math.min(this.buffer.ydisp + disp, this.buffer.ybase), 0); - - // No change occurred, don't trigger scroll/refresh - if (oldYdisp === this.buffer.ydisp) { - return; - } - - if (!suppressScrollEvent) { - this._onScroll.fire(this.buffer.ydisp); - } - + super.scrollLines(disp, suppressScrollEvent); this.refresh(0, this.rows - 1); } - /** - * Scroll the display of the terminal by a number of pages. - * @param pageCount The number of pages to scroll (negative scrolls up). - */ - public scrollPages(pageCount: number): void { - this.scrollLines(pageCount * (this.rows - 1)); - } - - /** - * Scrolls the display of the terminal to the top. - */ - public scrollToTop(): void { - this.scrollLines(-this.buffer.ydisp); - } - - /** - * Scrolls the display of the terminal to the bottom. - */ - public scrollToBottom(): void { - this.scrollLines(this.buffer.ybase - this.buffer.ydisp); - } - - public scrollToLine(line: number): void { - const scrollAmount = line - this.buffer.ydisp; - if (scrollAmount !== 0) { - this.scrollLines(scrollAmount); - } - } - public paste(data: string): void { - paste(data, this.textarea, this.bracketedPasteMode, this._coreService); + paste(data, this.textarea!, this._coreService); } /** @@ -1080,25 +835,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._customKeyEventHandler = customKeyEventHandler; } - /** Add handler for ESC escape sequence. See xterm.d.ts for details. */ - public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable { - return this._inputHandler.addEscHandler(id, callback); - } - - /** Add handler for DCS escape sequence. See xterm.d.ts for details. */ - public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { - return this._inputHandler.addDcsHandler(id, callback); - } - - /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ - public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { - return this._inputHandler.addCsiHandler(id, callback); - } - /** Add handler for OSC escape sequence. See xterm.d.ts for details. */ - public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { - return this._inputHandler.addOscHandler(ident, callback); - } - /** * Registers a link matcher, allowing custom link patterns to be matched and * handled. @@ -1130,13 +866,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } public registerCharacterJoiner(handler: CharacterJoinerHandler): number { - const joinerId = this._renderService.registerCharacterJoiner(handler); + const joinerId = this._renderService!.registerCharacterJoiner(handler); this.refresh(0, this.rows - 1); return joinerId; } public deregisterCharacterJoiner(joinerId: number): void { - if (this._renderService.deregisterCharacterJoiner(joinerId)) { + if (this._renderService!.deregisterCharacterJoiner(joinerId)) { this.refresh(0, this.rows - 1); } } @@ -1145,7 +881,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return this.buffer.markers; } - public addMarker(cursorYOffset: number): IMarker { + public addMarker(cursorYOffset: number): IMarker | undefined { // Disallow markers on the alt buffer if (this.buffer !== this.buffers.normal) { return; @@ -1168,7 +904,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * @param length The length of the selection. */ public select(column: number, row: number, length: number): void { - this._selectionService.setSelection(column, row, length); + this._selectionService!.setSelection(column, row, length); } /** @@ -1180,15 +916,15 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp } public getSelectionPosition(): ISelectionPosition | undefined { - if (!this._selectionService.hasSelection) { + if (!this._selectionService || !this._selectionService.hasSelection) { return undefined; } return { - startColumn: this._selectionService.selectionStart[0], - startRow: this._selectionService.selectionStart[1], - endColumn: this._selectionService.selectionEnd[0], - endRow: this._selectionService.selectionEnd[1] + startColumn: this._selectionService.selectionStart![0], + startRow: this._selectionService.selectionStart![1], + endColumn: this._selectionService.selectionEnd![0], + endRow: this._selectionService.selectionEnd![1] }; } @@ -1216,14 +952,14 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent * @param ev The keydown event to be handled. */ - protected _keyDown(event: KeyboardEvent): boolean { + protected _keyDown(event: KeyboardEvent): boolean | undefined { this._keyDownHandled = false; if (this._customKeyEventHandler && this._customKeyEventHandler(event) === false) { return false; } - if (!this._compositionHelper.keydown(event)) { + if (!this._compositionHelper!.keydown(event)) { if (this.buffer.ybase !== this.buffer.ydisp) { this.scrollToBottom(); } @@ -1261,11 +997,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // will announce deleted characters. This will not work 100% of the time but it should cover // most scenarios. if (result.key === C0.ETX || result.key === C0.CR) { - this.textarea.value = ''; + this.textarea!.value = ''; } this._onKey.fire({ key: result.key, domEvent: event }); - this.showCursor(); + this._showCursor(); this._coreService.triggerDataEvent(result.key, true); // Cancel events when not in screen reader mode so events don't get bubbled up and handled by @@ -1342,7 +1078,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp key = String.fromCharCode(key); this._onKey.fire({ key, domEvent: ev }); - this.showCursor(); + this._showCursor(); this._coreService.triggerDataEvent(key, true); return true; @@ -1354,16 +1090,16 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp */ public bell(): void { if (this._soundBell()) { - this._soundService.playBellSound(); + this._soundService!.playBellSound(); } - if (this._visualBell()) { - this.element.classList.add('visual-bell-active'); - clearTimeout(this._visualBellTimer); - this._visualBellTimer = window.setTimeout(() => { - this.element.classList.remove('visual-bell-active'); - }, 200); - } + // if (this._visualBell()) { + // this.element.classList.add('visual-bell-active'); + // clearTimeout(this._visualBellTimer); + // this._visualBellTimer = window.setTimeout(() => { + // this.element.classList.remove('visual-bell-active'); + // }, 200); + // } } /** @@ -1373,10 +1109,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp * @param y The number of rows to resize to. */ public resize(x: number, y: number): void { - if (isNaN(x) || isNaN(y)) { - return; - } - if (x === this.cols && y === this.rows) { // Check if we still need to measure the char size (fixes #785). if (this._charSizeService && !this._charSizeService.hasValidSize) { @@ -1385,22 +1117,15 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp return; } - if (x < MINIMUM_COLS) x = MINIMUM_COLS; - if (y < MINIMUM_ROWS) y = MINIMUM_ROWS; - - this.buffers.resize(x, y); - - this._bufferService.resize(x, y); - this.buffers.setupTabStops(this.cols); + super.resize(x, y); + } + private _afterResize(x: number, y: number): void { this._charSizeService?.measure(); // 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.refresh(0, this.rows - 1); - this._onResize.fire({ cols: x, rows: y }); } /** @@ -1411,7 +1136,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // Don't clear if it's already clear return; } - this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)); + this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!); this.buffer.lines.length = 1; this.buffer.ydisp = 0; this.buffer.ybase = 0; @@ -1423,44 +1148,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this._onScroll.fire(this.buffer.ydisp); } - /** - * Evaluate if the current terminal is the given argument. - * @param term The terminal name to evaluate - */ - public is(term: string): boolean { - return (this.options.termName + '').indexOf(term) === 0; - } - - /** - * Emit the data event and populate the given data. - * @param data The data to populate in the event. - */ - // public handler(data: string): void { - // // Prevents all events to pty process if stdin is disabled - // if (this.options.disableStdin) { - // return; - // } - - // // Clear the selection if the selection manager is available and has an active selection - // if (this.selectionService && this.selectionService.hasSelection) { - // this.selectionService.clearSelection(); - // } - - // // Input is being sent to the terminal, the terminal should focus the prompt. - // if (this.buffer.ybase !== this.buffer.ydisp) { - // this.scrollToBottom(); - // } - // this._onData.fire(data); - // } - - /** - * Emit the 'title' event and populate the given title. - * @param title The title to populate in the event. - */ - public handleTitle(title: string): void { - this._onTitleChange.fire(title); - } - /** * Reset terminal. * Note: Calling this directly from JS is synchronous but does not clear @@ -1477,26 +1164,40 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp this.options.rows = this.rows; this.options.cols = this.cols; const customKeyEventHandler = this._customKeyEventHandler; - const userScrolling = this._userScrolling; this._setup(); - this._bufferService.reset(); - this._charsetService.reset(); - this._coreService.reset(); - this._coreMouseService.reset(); + super.reset(); this._selectionService?.reset(); // reattach this._customKeyEventHandler = customKeyEventHandler; - this._userScrolling = userScrolling; // do a full screen refresh this.refresh(0, this.rows - 1); this.viewport?.syncScrollArea(); } + private _reportWindowsOptions(type: WindowsOptionsReportType): void { + if (!this._renderService) { + return; + } + + switch (type) { + case WindowsOptionsReportType.GET_WIN_SIZE_PIXELS: + const canvasWidth = this._renderService.dimensions.scaledCanvasWidth.toFixed(0); + const canvasHeight = this._renderService.dimensions.scaledCanvasHeight.toFixed(0); + this._coreService.triggerDataEvent(`${C0.ESC}[4;${canvasHeight};${canvasWidth}t`); + break; + case WindowsOptionsReportType.GET_CELL_SIZE_PIXELS: + const cellWidth = this._renderService.dimensions.scaledCellWidth.toFixed(0); + const cellHeight = this._renderService.dimensions.scaledCellHeight.toFixed(0); + this._coreService.triggerDataEvent(`${C0.ESC}[6;${cellHeight};${cellWidth}t`); + break; + } + } + // TODO: Remove cancel function and cancelEvents option - public cancel(ev: Event, force?: boolean): boolean { + public cancel(ev: Event, force?: boolean): boolean | undefined { if (!this.options.cancelEvents && !force) { return; } @@ -1516,14 +1217,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp // return this.options.bellStyle === 'sound' || // this.options.bellStyle === 'both'; } - - public write(data: string | Uint8Array, callback?: () => void): void { - this._writeBuffer.write(data, callback); - } - - public writeSync(data: string | Uint8Array): void { - this._writeBuffer.writeSync(data); - } } /** diff --git a/src/Terminal2.test.ts b/src/browser/Terminal2.test.ts similarity index 96% rename from src/Terminal2.test.ts rename to src/browser/Terminal2.test.ts index f7046240..32a47b21 100644 --- a/src/Terminal2.test.ts +++ b/src/browser/Terminal2.test.ts @@ -8,14 +8,14 @@ import * as path from 'path'; import * as os from 'os'; import * as fs from 'fs'; import * as pty from 'node-pty'; -import { Terminal } from './Terminal'; +import { Terminal } from 'browser/Terminal'; import { IDisposable } from 'xterm'; // all test files expect terminal in 80x25 const COLS = 80; const ROWS = 25; -const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')}); +const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '../..')}); const SKIP_FILES = [ 't0084-CBT.in', 't0101-NLM.in', @@ -124,7 +124,7 @@ function terminalToString(term: Terminal): string { let result = ''; let lineText = ''; for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) { - lineText = term.buffer.lines.get(line).translateToString(true); + lineText = term.buffer.lines.get(line)!.translateToString(true); // rtrim empty cells as xterm does lineText = lineText.replace(/\s+$/, ''); result += lineText; diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 96d1eae0..2d6f6cbb 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -3,13 +3,331 @@ * @license MIT */ +import { IDisposable, IMarker, ISelectionPosition, ILinkProvider } from 'xterm'; import { IEvent, EventEmitter } from 'common/EventEmitter'; -import { ICharSizeService, IMouseService, IRenderService } from 'browser/services/Services'; -import { IRenderDimensions, IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types'; -import { IColorSet } from 'browser/Types'; +import { ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services'; +import { IRenderDimensions, IRenderer, CharacterJoinerHandler, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper } from 'browser/Types'; +import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; +import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types'; +import { Buffer } from 'common/buffer/Buffer'; +import * as Browser from 'common/Platform'; +import { Terminal } from 'browser/Terminal'; +import { IUnicodeService, IOptionsService } from 'common/services/Services'; +import { IFunctionIdentifier, IParams } from 'common/parser/Types'; +import { AttributeData } from 'common/buffer/AttributeData'; + +export class TestTerminal extends Terminal { + public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; } + public keyDown(ev: any): boolean | undefined { return this._keyDown(ev); } + public keyPress(ev: any): boolean { return this._keyPress(ev); } +} + +export class MockTerminal implements ITerminal { + public onBlur!: IEvent; + public onFocus!: IEvent; + public onA11yChar!: IEvent; + public onA11yTab!: IEvent; + public onCursorMove!: IEvent; + public onLineFeed!: IEvent; + public onSelectionChange!: IEvent; + public onData!: IEvent; + public onBinary!: IEvent; + public onTitleChange!: IEvent; + public onScroll!: IEvent; + public onKey!: IEvent<{ key: string, domEvent: KeyboardEvent }>; + public onRender!: IEvent<{ start: number, end: number }>; + public onResize!: IEvent<{ cols: number, rows: number }>; + public markers!: IMarker[]; + public optionsService!: IOptionsService; + public unicodeService!: IUnicodeService; + public addMarker(cursorYOffset: number): IMarker { + throw new Error('Method not implemented.'); + } + public selectLines(start: number, end: number): void { + throw new Error('Method not implemented.'); + } + public scrollToLine(line: number): void { + throw new Error('Method not implemented.'); + } + public static string: any; + public setOption(key: any, value: any): void { + throw new Error('Method not implemented.'); + } + public blur(): void { + throw new Error('Method not implemented.'); + } + public focus(): void { + throw new Error('Method not implemented.'); + } + public resize(columns: number, rows: number): void { + throw new Error('Method not implemented.'); + } + public writeln(data: string): void { + throw new Error('Method not implemented.'); + } + public paste(data: string): void { + throw new Error('Method not implemented.'); + } + public open(parent: HTMLElement): void { + throw new Error('Method not implemented.'); + } + public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { + throw new Error('Method not implemented.'); + } + public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + throw new Error('Method not implemented.'); + } + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { + throw new Error('Method not implemented.'); + } + public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable { + throw new Error('Method not implemented.'); + } + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + throw new Error('Method not implemented.'); + } + public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number { + throw new Error('Method not implemented.'); + } + public deregisterLinkMatcher(matcherId: number): void { + throw new Error('Method not implemented.'); + } + public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { + throw new Error('Method not implemented.'); + } + public hasSelection(): boolean { + throw new Error('Method not implemented.'); + } + public getSelection(): string { + throw new Error('Method not implemented.'); + } + public getSelectionPosition(): ISelectionPosition | undefined { + throw new Error('Method not implemented.'); + } + public clearSelection(): void { + throw new Error('Method not implemented.'); + } + public select(column: number, row: number, length: number): void { + throw new Error('Method not implemented.'); + } + public selectAll(): void { + throw new Error('Method not implemented.'); + } + public dispose(): void { + throw new Error('Method not implemented.'); + } + public scrollPages(pageCount: number): void { + throw new Error('Method not implemented.'); + } + public scrollToTop(): void { + throw new Error('Method not implemented.'); + } + public scrollToBottom(): void { + throw new Error('Method not implemented.'); + } + public clear(): void { + throw new Error('Method not implemented.'); + } + public write(data: string): void { + throw new Error('Method not implemented.'); + } + public writeUtf8(data: Uint8Array): void { + throw new Error('Method not implemented.'); + } + public bracketedPasteMode!: boolean; + public renderer!: IRenderer; + public linkifier!: ILinkifier; + public linkifier2!: ILinkifier2; + public isFocused!: boolean; + public options: ITerminalOptions = {}; + public element!: HTMLElement; + public screenElement!: HTMLElement; + public rowContainer!: HTMLElement; + public selectionContainer!: HTMLElement; + public selectionService!: ISelectionService; + public textarea!: HTMLTextAreaElement; + public rows!: number; + public cols!: number; + public browser: IBrowser = Browser; + public writeBuffer!: string[]; + public children!: HTMLElement[]; + public cursorHidden!: boolean; + public cursorState!: number; + public scrollback!: number; + public buffers!: IBufferSet; + public buffer!: IBuffer; + public viewport!: IViewport; + public applicationCursor!: boolean; + public handler(data: string): void { + throw new Error('Method not implemented.'); + } + public on(event: string, callback: (...args: any[]) => void): void { + throw new Error('Method not implemented.'); + } + public off(type: string, listener: XtermListener): void { + throw new Error('Method not implemented.'); + } + public addDisposableListener(type: string, handler: XtermListener): IDisposable { + throw new Error('Method not implemented.'); + } + public scrollLines(disp: number): void { + throw new Error('Method not implemented.'); + } + public scrollToRow(absoluteRow: number): number { + throw new Error('Method not implemented.'); + } + public cancel(ev: Event, force?: boolean): void { + throw new Error('Method not implemented.'); + } + public log(text: string): void { + throw new Error('Method not implemented.'); + } + public emit(event: string, data: any): void { + throw new Error('Method not implemented.'); + } + public reset(): void { + throw new Error('Method not implemented.'); + } + public refresh(start: number, end: number): void { + throw new Error('Method not implemented.'); + } + public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; } + public deregisterCharacterJoiner(joinerId: number): void { } +} + +export class MockBuffer implements IBuffer { + public markers!: IMarker[]; + public addMarker(y: number): IMarker { + throw new Error('Method not implemented.'); + } + public isCursorInViewport!: boolean; + public lines!: ICircularList; + public ydisp!: number; + public ybase!: number; + public hasScrollback!: boolean; + public y!: number; + public x!: number; + public tabs: any; + public scrollBottom!: number; + public scrollTop!: number; + public savedY!: number; + public savedX!: number; + public savedCharset: ICharset | undefined; + public savedCurAttrData = new AttributeData(); + public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string { + return Buffer.prototype.translateBufferLineToString.apply(this, arguments as any); + } + public getWrappedRangeForLine(y: number): { first: number, last: number } { + return Buffer.prototype.getWrappedRangeForLine.apply(this, arguments as any); + } + public nextStop(x?: number): number { + throw new Error('Method not implemented.'); + } + public prevStop(x?: number): number { + throw new Error('Method not implemented.'); + } + public setLines(lines: ICircularList): void { + this.lines = lines; + } + public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine { + return Buffer.prototype.getBlankLine.apply(this, arguments as any); + } + public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] { + return Buffer.prototype.stringIndexToBufferIndex.apply(this, arguments as any); + } + public iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator { + return Buffer.prototype.iterator.apply(this, arguments as any); + } + public getNullCell(attr?: IAttributeData): ICellData { + throw new Error('Method not implemented.'); + } + public getWhitespaceCell(attr?: IAttributeData): ICellData { + throw new Error('Method not implemented.'); + } +} + +export class MockRenderer implements IRenderer { + public onRequestRedraw!: IEvent; + public onCanvasResize!: IEvent<{ width: number, height: number }>; + public onRender!: IEvent<{ start: number, end: number }>; + public dispose(): void { + throw new Error('Method not implemented.'); + } + public colorManager!: IColorManager; + public on(type: string, listener: XtermListener): void { + throw new Error('Method not implemented.'); + } + public off(type: string, listener: XtermListener): void { + throw new Error('Method not implemented.'); + } + public emit(type: string, data?: any): void { + throw new Error('Method not implemented.'); + } + public addDisposableListener(type: string, handler: XtermListener): IDisposable { + throw new Error('Method not implemented.'); + } + public dimensions!: IRenderDimensions; + public setColors(colors: IColorSet): void { + throw new Error('Method not implemented.'); + } + public onResize(cols: number, rows: number): void { } + public onCharSizeChanged(): void { } + public onBlur(): void { } + public onFocus(): void { } + public onSelectionChanged(start: [number, number], end: [number, number]): void { } + public onCursorMove(): void { } + public onOptionsChanged(): void { } + public onDevicePixelRatioChange(): void { } + public clear(): void { } + public renderRows(start: number, end: number): void { } + public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; } + public deregisterCharacterJoiner(): boolean { return true; } +} + +export class MockViewport implements IViewport { + public dispose(): void { + throw new Error('Method not implemented.'); + } + public scrollBarWidth: number = 0; + public onThemeChange(colors: IColorSet): void { + throw new Error('Method not implemented.'); + } + public onWheel(ev: WheelEvent): boolean { + throw new Error('Method not implemented.'); + } + public onTouchStart(ev: TouchEvent): void { + throw new Error('Method not implemented.'); + } + public onTouchMove(ev: TouchEvent): boolean { + throw new Error('Method not implemented.'); + } + public syncScrollArea(): void { } + public getLinesScrolled(ev: WheelEvent): number { + throw new Error('Method not implemented.'); + } +} + +export class MockCompositionHelper implements ICompositionHelper { + public compositionstart(): void { + throw new Error('Method not implemented.'); + } + public compositionupdate(ev: CompositionEvent): void { + throw new Error('Method not implemented.'); + } + public compositionend(): void { + throw new Error('Method not implemented.'); + } + public updateCompositionElements(dontRecurse?: boolean): void { + throw new Error('Method not implemented.'); + } + public keydown(ev: KeyboardEvent): boolean { + return true; + } +} export class MockCharSizeService implements ICharSizeService { - public serviceBrand: any; + public serviceBrand: undefined; public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; } public onCharSizeChange: IEvent = new EventEmitter().event; constructor(public width: number, public height: number) {} @@ -17,7 +335,7 @@ export class MockCharSizeService implements ICharSizeService { } export class MockMouseService implements IMouseService { - public serviceBrand: any; + public serviceBrand: undefined; public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined { throw new Error('Not implemented'); } @@ -28,7 +346,7 @@ export class MockMouseService implements IMouseService { } export class MockRenderService implements IRenderService { - public serviceBrand: any; + public serviceBrand: undefined; public onDimensionsChange: IEvent = new EventEmitter().event; public onRenderedBufferChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event; public onRefreshRequest: IEvent<{ start: number, end: number}, void> = new EventEmitter<{ start: number, end: number }>().event; diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index f4645543..76aa0c8c 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -3,9 +3,107 @@ * @license MIT */ +import { IDisposable, IMarker, ISelectionPosition } from 'xterm'; import { IEvent } from 'common/EventEmitter'; -import { IDisposable } from 'common/Types'; +import { ICoreTerminal, CharData, ITerminalOptions } from 'common/Types'; import { IMouseService, IRenderService } from './services/Services'; +import { IBuffer, IBufferSet } from 'common/buffer/Types'; +import { IFunctionIdentifier, IParams } from 'common/parser/Types'; + +export interface ITerminal extends IPublicTerminal, ICoreTerminal { + element: HTMLElement | undefined; + screenElement: HTMLElement | undefined; + browser: IBrowser; + buffer: IBuffer; + buffers: IBufferSet; + viewport: IViewport | undefined; + // TODO: We should remove options once components adopt optionsService + options: ITerminalOptions; + linkifier: ILinkifier; + linkifier2: ILinkifier2; + + onBlur: IEvent; + onFocus: IEvent; + onA11yChar: IEvent; + onA11yTab: IEvent; + + cancel(ev: Event, force?: boolean): boolean | void; +} + +// Portions of the public API that are required by the internal Terminal +export interface IPublicTerminal extends IDisposable { + textarea: HTMLTextAreaElement | undefined; + rows: number; + cols: number; + buffer: IBuffer; + markers: IMarker[]; + onCursorMove: IEvent; + onData: IEvent; + onBinary: IEvent; + onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>; + onLineFeed: IEvent; + onScroll: IEvent; + onSelectionChange: IEvent; + onRender: IEvent<{ start: number, end: number }>; + onResize: IEvent<{ cols: number, rows: number }>; + onTitleChange: IEvent; + blur(): void; + focus(): void; + resize(columns: number, rows: number): void; + open(parent: HTMLElement): void; + attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; + addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; + addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable; + addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable; + addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable; + registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; + deregisterLinkMatcher(matcherId: number): void; + registerLinkProvider(linkProvider: ILinkProvider): IDisposable; + registerCharacterJoiner(handler: (text: string) => [number, number][]): number; + deregisterCharacterJoiner(joinerId: number): void; + addMarker(cursorYOffset: number): IMarker | undefined; + hasSelection(): boolean; + getSelection(): string; + getSelectionPosition(): ISelectionPosition | undefined; + clearSelection(): void; + select(column: number, row: number, length: number): void; + selectAll(): void; + selectLines(start: number, end: number): void; + dispose(): void; + scrollLines(amount: number): void; + scrollPages(pageCount: number): void; + scrollToTop(): void; + scrollToBottom(): void; + scrollToLine(line: number): void; + clear(): void; + write(data: string | Uint8Array, callback?: () => void): void; + paste(data: string): void; + refresh(start: number, end: number): void; + reset(): void; +} + +export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; + +export type LineData = CharData[]; + +export interface ICompositionHelper { + compositionstart(): void; + compositionupdate(ev: CompositionEvent): void; + compositionend(): void; + updateCompositionElements(dontRecurse?: boolean): void; + keydown(ev: KeyboardEvent): boolean; +} + +export interface IBrowser { + isNode: boolean; + userAgent: string; + platform: string; + isFirefox: boolean; + isMac: boolean; + isIpad: boolean; + isIphone: boolean; + isWindows: boolean; +} export interface IColorManager { colors: IColorSet; diff --git a/src/public/AddonManager.test.ts b/src/browser/public/AddonManager.test.ts similarity index 90% rename from src/public/AddonManager.test.ts rename to src/browser/public/AddonManager.test.ts index ea229095..e947976a 100644 --- a/src/public/AddonManager.test.ts +++ b/src/browser/public/AddonManager.test.ts @@ -42,9 +42,9 @@ describe('AddonManager', () => { public activate(): void {} public dispose(): void { called++; } } - manager.loadAddon(null, new Addon()); - manager.loadAddon(null, new Addon()); - manager.loadAddon(null, new Addon()); + manager.loadAddon(null!, new Addon()); + manager.loadAddon(null!, new Addon()); + manager.loadAddon(null!, new Addon()); assert.equal(manager.addons.length, 3); manager.dispose(); assert.equal(called, 3); diff --git a/src/public/AddonManager.ts b/src/browser/public/AddonManager.ts similarity index 100% rename from src/public/AddonManager.ts rename to src/browser/public/AddonManager.ts diff --git a/src/public/Terminal.ts b/src/browser/public/Terminal.ts similarity index 97% rename from src/public/Terminal.ts rename to src/browser/public/Terminal.ts index 61334f08..b49fc6b8 100644 --- a/src/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -4,12 +4,12 @@ */ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier, ILinkProvider, IUnicodeHandling, IUnicodeVersionProvider } from 'xterm'; -import { ITerminal } from '../Types'; +import { ITerminal } from 'browser/Types'; import { IBufferLine, ICellData } from 'common/Types'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; import { CellData } from 'common/buffer/CellData'; import { Terminal as TerminalCore } from '../Terminal'; -import * as Strings from '../browser/LocalizableStrings'; +import * as Strings from '../LocalizableStrings'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { AddonManager } from './AddonManager'; import { IParams } from 'common/parser/Types'; @@ -17,7 +17,7 @@ import { IParams } from 'common/parser/Types'; export class Terminal implements ITerminalApi { private _core: ITerminal; private _addonManager: AddonManager; - private _parser: IParser; + private _parser: IParser | undefined; constructor(options?: ITerminalOptions) { this._core = new TerminalCore(options); @@ -81,11 +81,11 @@ export class Terminal implements ITerminalApi { public deregisterCharacterJoiner(joinerId: number): void { this._core.deregisterCharacterJoiner(joinerId); } - public registerMarker(cursorYOffset: number): IMarker { + public registerMarker(cursorYOffset: number): IMarker | undefined { this._verifyIntegers(cursorYOffset); return this._core.addMarker(cursorYOffset); } - public addMarker(cursorYOffset: number): IMarker { + public addMarker(cursorYOffset: number): IMarker | undefined { return this.registerMarker(cursorYOffset); } public hasSelection(): boolean { diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts index 7623109c..79696a28 100644 --- a/src/browser/services/CharSizeService.ts +++ b/src/browser/services/CharSizeService.ts @@ -8,7 +8,7 @@ import { IEvent, EventEmitter } from 'common/EventEmitter'; import { ICharSizeService } from 'browser/services/Services'; export class CharSizeService implements ICharSizeService { - public serviceBrand: any; + public serviceBrand: undefined; public width: number = 0; public height: number = 0; diff --git a/src/browser/services/CoreBrowserService.ts b/src/browser/services/CoreBrowserService.ts index e3207d7b..985253d9 100644 --- a/src/browser/services/CoreBrowserService.ts +++ b/src/browser/services/CoreBrowserService.ts @@ -6,7 +6,7 @@ import { ICoreBrowserService } from './Services'; export class CoreBrowserService implements ICoreBrowserService { - public serviceBrand: any; + public serviceBrand: undefined; constructor( private _textarea: HTMLTextAreaElement diff --git a/src/browser/services/MouseService.ts b/src/browser/services/MouseService.ts index 39df6383..348ba64e 100644 --- a/src/browser/services/MouseService.ts +++ b/src/browser/services/MouseService.ts @@ -7,7 +7,7 @@ import { ICharSizeService, IRenderService, IMouseService } from './Services'; import { getCoords, getRawByteCoords } from 'browser/input/Mouse'; export class MouseService implements IMouseService { - public serviceBrand: any; + public serviceBrand: undefined; constructor( @IRenderService private readonly _renderService: IRenderService, diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 7c8a13cc..d6b30524 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -10,7 +10,7 @@ import { Disposable } from 'common/Lifecycle'; import { ScreenDprMonitor } from 'browser/ScreenDprMonitor'; import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet } from 'browser/Types'; -import { IOptionsService } from 'common/services/Services'; +import { IOptionsService, IBufferService } from 'common/services/Services'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; interface ISelectionState { @@ -20,7 +20,7 @@ interface ISelectionState { } export class RenderService extends Disposable implements IRenderService { - public serviceBrand: any; + public serviceBrand: undefined; private _renderDebouncer: RenderDebouncer; private _screenDprMonitor: ScreenDprMonitor; @@ -51,7 +51,8 @@ export class RenderService extends Disposable implements IRenderService { private _rowCount: number, screenElement: HTMLElement, @IOptionsService optionsService: IOptionsService, - @ICharSizeService charSizeService: ICharSizeService + @ICharSizeService charSizeService: ICharSizeService, + @IBufferService private readonly _bufferService: IBufferService ) { super(); this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end)); @@ -61,6 +62,7 @@ export class RenderService extends Disposable implements IRenderService { this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange()); this.register(this._screenDprMonitor); + this.register(this._bufferService.onResize(e => this._fullRefresh())); this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged())); this.register(charSizeService.onCharSizeChange(() => this.onCharSizeChanged())); diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 43875f1b..d70cfa11 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -67,7 +67,7 @@ export const enum SelectionMode { * when the selection is ready to be redrawn (on an animation frame). */ export class SelectionService implements ISelectionService { - public serviceBrand: any; + public serviceBrand: undefined; protected _model: SelectionModel; diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 7a559dab..445cab05 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -12,7 +12,7 @@ import { IDisposable } from 'common/Types'; export const ICharSizeService = createDecorator('CharSizeService'); export interface ICharSizeService { - serviceBrand: any; + serviceBrand: undefined; readonly width: number; readonly height: number; @@ -25,14 +25,14 @@ export interface ICharSizeService { export const ICoreBrowserService = createDecorator('CoreBrowserService'); export interface ICoreBrowserService { - serviceBrand: any; + serviceBrand: undefined; readonly isFocused: boolean; } export const IMouseService = createDecorator('MouseService'); export interface IMouseService { - serviceBrand: any; + serviceBrand: undefined; getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, isSelection?: boolean): [number, number] | undefined; getRawByteCoords(event: MouseEvent, element: HTMLElement, colCount: number, rowCount: number): { x: number, y: number } | undefined; @@ -40,7 +40,7 @@ export interface IMouseService { export const IRenderService = createDecorator('RenderService'); export interface IRenderService extends IDisposable { - serviceBrand: any; + serviceBrand: undefined; onDimensionsChange: IEvent; /** @@ -72,7 +72,7 @@ export interface IRenderService extends IDisposable { export const ISelectionService = createDecorator('SelectionService'); export interface ISelectionService { - serviceBrand: any; + serviceBrand: undefined; readonly selectionText: string; readonly hasSelection: boolean; @@ -100,7 +100,7 @@ export interface ISelectionService { export const ISoundService = createDecorator('SoundService'); export interface ISoundService { - serviceBrand: any; + serviceBrand: undefined; playBellSound(): void; } diff --git a/src/browser/services/SoundService.ts b/src/browser/services/SoundService.ts index 4b54c999..8d940c13 100644 --- a/src/browser/services/SoundService.ts +++ b/src/browser/services/SoundService.ts @@ -7,7 +7,7 @@ import { IOptionsService } from 'common/services/Services'; import { ISoundService } from 'browser/services/Services'; export class SoundService implements ISoundService { - public serviceBrand: any; + public serviceBrand: undefined; private static _audioContext: AudioContext; diff --git a/src/browser/tsconfig.json b/src/browser/tsconfig.json index 7465bbd3..74b4c057 100644 --- a/src/browser/tsconfig.json +++ b/src/browser/tsconfig.json @@ -14,7 +14,10 @@ "common/*": [ "./common/*" ] } }, - "include": [ "./**/*" ], + "include": [ + "./**/*", + "../../typings/xterm.d.ts" + ], "references": [ { "path": "../common" } ] diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts new file mode 100644 index 00000000..3793ae25 --- /dev/null +++ b/src/common/CoreTerminal.ts @@ -0,0 +1,336 @@ +/** + * Copyright (c) 2014-2020 The xterm.js authors. All rights reserved. + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * @license MIT + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + * + * Terminal Emulation References: + * http://vt100.net/ + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + * http://invisible-island.net/vttest/ + * http://www.inwap.com/pdp10/ansicode.txt + * http://linux.die.net/man/4/console_codes + * http://linux.die.net/man/7/urxvt + */ + +import { Disposable } from 'common/Lifecycle'; +import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService } from 'common/services/Services'; +import { InstantiationService } from 'common/services/InstantiationService'; +import { LogService } from 'common/services/LogService'; +import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; +import { OptionsService } from 'common/services/OptionsService'; +import { ITerminalOptions, IDisposable, IBufferLine, IAttributeData, ICoreTerminal } from 'common/Types'; +import { CoreService } from 'common/services/CoreService'; +import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; +import { CoreMouseService } from 'common/services/CoreMouseService'; +import { DirtyRowService } from 'common/services/DirtyRowService'; +import { UnicodeService } from 'common/services/UnicodeService'; +import { CharsetService } from 'common/services/CharsetService'; +import { updateWindowsModeWrappedState } from 'common/WindowsMode'; +import { IFunctionIdentifier, IParams } from 'common/parser/Types'; +import { IBufferSet } from 'common/buffer/Types'; +import { InputHandler } from 'common/InputHandler'; +import { WriteBuffer } from 'common/input/WriteBuffer'; + +export abstract class CoreTerminal extends Disposable implements ICoreTerminal { + protected readonly _instantiationService: IInstantiationService; + protected readonly _bufferService: IBufferService; + protected readonly _logService: ILogService; + protected readonly _coreService: ICoreService; + protected readonly _charsetService: ICharsetService; + protected readonly _coreMouseService: ICoreMouseService; + protected readonly _dirtyRowService: IDirtyRowService; + + public readonly unicodeService: IUnicodeService; + public readonly optionsService: IOptionsService; + + protected _inputHandler: InputHandler; + private _writeBuffer: WriteBuffer; + private _windowsMode: IDisposable | undefined; + /** An IBufferline to clone/copy from for new blank lines */ + private _cachedBlankLine: IBufferLine | undefined; + + private _onBinary = new EventEmitter(); + public get onBinary(): IEvent { return this._onBinary.event; } + private _onData = new EventEmitter(); + public get onData(): IEvent { return this._onData.event; } + protected _onLineFeed = new EventEmitter(); + public get onLineFeed(): IEvent { return this._onLineFeed.event; } + private _onResize = new EventEmitter<{ cols: number, rows: number }>(); + public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } + protected _onScroll = new EventEmitter(); + public get onScroll(): IEvent { return this._onScroll.event; } + + public get cols(): number { return this._bufferService.cols; } + public get rows(): number { return this._bufferService.rows; } + public get buffers(): IBufferSet { return this._bufferService.buffers; } + + constructor( + options: ITerminalOptions + ) { + super(); + + // Setup and initialize services + this._instantiationService = new InstantiationService(); + this.optionsService = new OptionsService(options); + this._instantiationService.setService(IOptionsService, this.optionsService); + this._bufferService = this._instantiationService.createInstance(BufferService); + this._instantiationService.setService(IBufferService, this._bufferService); + this._logService = this._instantiationService.createInstance(LogService); + this._instantiationService.setService(ILogService, this._logService); + this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom()); + this._instantiationService.setService(ICoreService, this._coreService); + this._coreMouseService = this._instantiationService.createInstance(CoreMouseService); + this._instantiationService.setService(ICoreMouseService, this._coreMouseService); + this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); + this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); + this.unicodeService = this._instantiationService.createInstance(UnicodeService); + this._instantiationService.setService(IUnicodeService, this.unicodeService); + this._charsetService = this._instantiationService.createInstance(CharsetService); + this._instantiationService.setService(ICharsetService, this._charsetService); + + // Register input handler and handle/forward events + this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService, this.unicodeService); + this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed)); + this.register(this._inputHandler); + + // Setup listeners + this.register(forwardEvent(this._bufferService.onResize, this._onResize)); + this.register(forwardEvent(this._coreService.onData, this._onData)); + this.register(forwardEvent(this._coreService.onBinary, this._onBinary)); + this.register(this.optionsService.onOptionChange(key => this._updateOptions(key))); + + // Setup WriteBuffer + this._writeBuffer = new WriteBuffer(data => this._inputHandler.parse(data)); + } + + public dispose(): void { + if (this._isDisposed) { + return; + } + super.dispose(); + this._windowsMode?.dispose(); + this._windowsMode = undefined; + } + + public write(data: string | Uint8Array, callback?: () => void): void { + this._writeBuffer.write(data, callback); + } + + public writeSync(data: string | Uint8Array): void { + this._writeBuffer.writeSync(data); + } + + public resize(x: number, y: number): void { + if (isNaN(x) || isNaN(y)) { + return; + } + + x = Math.max(x, MINIMUM_COLS); + y = Math.max(y, MINIMUM_ROWS); + + this._bufferService.resize(x, y); + } + + /** + * Scroll the terminal down 1 row, creating a blank line. + * @param isWrapped Whether the new line is wrapped from the previous line. + */ + public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void { + const buffer = this._bufferService.buffer; + + let newLine: IBufferLine | undefined; + newLine = this._cachedBlankLine; + if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) { + newLine = buffer.getBlankLine(eraseAttr, isWrapped); + this._cachedBlankLine = newLine; + } + newLine.isWrapped = isWrapped; + + const topRow = buffer.ybase + buffer.scrollTop; + const bottomRow = buffer.ybase + buffer.scrollBottom; + + if (buffer.scrollTop === 0) { + // Determine whether the buffer is going to be trimmed after insertion. + const willBufferBeTrimmed = buffer.lines.isFull; + + // Insert the line using the fastest method + if (bottomRow === buffer.lines.length - 1) { + if (willBufferBeTrimmed) { + buffer.lines.recycle().copyFrom(newLine); + } else { + buffer.lines.push(newLine.clone()); + } + } else { + buffer.lines.splice(bottomRow + 1, 0, newLine.clone()); + } + + // Only adjust ybase and ydisp when the buffer is not trimmed + if (!willBufferBeTrimmed) { + buffer.ybase++; + // Only scroll the ydisp with ybase if the user has not scrolled up + if (!this._bufferService.isUserScrolling) { + buffer.ydisp++; + } + } else { + // When the buffer is full and the user has scrolled up, keep the text + // stable unless ydisp is right at the top + if (this._bufferService.isUserScrolling) { + buffer.ydisp = Math.max(buffer.ydisp - 1, 0); + } + } + } else { + // scrollTop is non-zero which means no line will be going to the + // scrollback, instead we can just shift them in-place. + const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */; + buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); + buffer.lines.set(bottomRow, newLine.clone()); + } + + // Move the viewport to the bottom of the buffer unless the user is + // scrolling. + if (!this._bufferService.isUserScrolling) { + buffer.ydisp = buffer.ybase; + } + + // Flag rows that need updating + this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + + this._onScroll.fire(buffer.ydisp); + } + + /** + * Scroll the display of the terminal + * @param disp The number of lines to scroll down (negative scroll up). + * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used + * to avoid unwanted events being handled by the viewport when the event was triggered from the + * viewport originally. + */ + public scrollLines(disp: number, suppressScrollEvent?: boolean): void { + const buffer = this._bufferService.buffer; + if (disp < 0) { + if (buffer.ydisp === 0) { + return; + } + this._bufferService.isUserScrolling = true; + } else if (disp + buffer.ydisp >= buffer.ybase) { + this._bufferService.isUserScrolling = false; + } + + const oldYdisp = buffer.ydisp; + buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0); + + // No change occurred, don't trigger scroll/refresh + if (oldYdisp === buffer.ydisp) { + return; + } + + if (!suppressScrollEvent) { + this._onScroll.fire(buffer.ydisp); + } + } + + /** + * Scroll the display of the terminal by a number of pages. + * @param pageCount The number of pages to scroll (negative scrolls up). + */ + public scrollPages(pageCount: number): void { + this.scrollLines(pageCount * (this.rows - 1)); + } + + /** + * Scrolls the display of the terminal to the top. + */ + public scrollToTop(): void { + this.scrollLines(-this._bufferService.buffer.ydisp); + } + + /** + * Scrolls the display of the terminal to the bottom. + */ + public scrollToBottom(): void { + this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp); + } + + public scrollToLine(line: number): void { + const scrollAmount = line - this._bufferService.buffer.ydisp; + if (scrollAmount !== 0) { + this.scrollLines(scrollAmount); + } + } + + /** Add handler for ESC escape sequence. See xterm.d.ts for details. */ + public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable { + return this._inputHandler.addEscHandler(id, callback); + } + + /** Add handler for DCS escape sequence. See xterm.d.ts for details. */ + public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable { + return this._inputHandler.addDcsHandler(id, callback); + } + + /** Add handler for CSI escape sequence. See xterm.d.ts for details. */ + public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable { + return this._inputHandler.addCsiHandler(id, callback); + } + + /** Add handler for OSC escape sequence. See xterm.d.ts for details. */ + public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable { + return this._inputHandler.addOscHandler(ident, callback); + } + + protected _setup(): void { + if (this.optionsService.options.windowsMode) { + this._enableWindowsMode(); + } + } + + public reset(): void { + this._inputHandler.reset(); + this._bufferService.reset(); + this._charsetService.reset(); + this._coreService.reset(); + this._coreMouseService.reset(); + } + + protected _updateOptions(key: string): void { + // TODO: These listeners should be owned by individual components + switch (key) { + case 'scrollback': + this.buffers.resize(this.cols, this.rows); + break; + case 'windowsMode': + if (this.optionsService.options.windowsMode) { + this._enableWindowsMode(); + } else { + this._windowsMode?.dispose(); + this._windowsMode = undefined; + } + break; + } + } + + protected _enableWindowsMode(): void { + if (!this._windowsMode) { + const disposables: IDisposable[] = []; + disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService))); + disposables.push(this.addCsiHandler({ final: 'H' }, () => { + updateWindowsModeWrappedState(this._bufferService); + return false; + })); + this._windowsMode = { + dispose: () => { + disposables.forEach(d => d.dispose()); + } + }; + } + } +} diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts index 301574e4..4684809f 100644 --- a/src/common/EventEmitter.ts +++ b/src/common/EventEmitter.ts @@ -63,3 +63,7 @@ export class EventEmitter implements IEventEmitter { this._disposed = true; } } + +export function forwardEvent(from: IEvent, to: IEventEmitter): IDisposable { + return from(e => to.fire(e)); +} diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts new file mode 100644 index 00000000..8f3ff9af --- /dev/null +++ b/src/common/InputHandler.test.ts @@ -0,0 +1,1427 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert, expect } from 'chai'; +import { InputHandler } from 'common/InputHandler'; +import { IBufferLine, IAttributeData } from 'common/Types'; +import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; +import { CellData } from 'common/buffer/CellData'; +import { Attributes } from 'common/buffer/Constants'; +import { AttributeData } from 'common/buffer/AttributeData'; +import { Params } from 'common/parser/Params'; +import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService } from 'common/TestUtils.test'; +import { IBufferService, ICoreService } from 'common/services/Services'; +import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; +import { clone } from 'common/Clone'; +import { BufferService } from 'common/services/BufferService'; +import { CoreService } from 'common/services/CoreService'; + +function getCursor(bufferService: IBufferService): number[] { + return [ + bufferService.buffer.x, + bufferService.buffer.y + ]; +} + +function getLines(bufferService: IBufferService, limit: number = bufferService.rows): string[] { + const res: string[] = []; + for (let i = 0; i < limit; ++i) { + res.push(bufferService.buffer.lines.get(i)!.translateToString(true)); + } + return res; +} + +class TestInputHandler extends InputHandler { + public get curAttrData(): IAttributeData { return (this as any)._curAttrData; } + public get windowTitleStack(): string[] { return this._windowTitleStack; } + public get iconNameStack(): string[] { return this._iconNameStack; } +} + +describe('InputHandler', () => { + let bufferService: IBufferService; + let coreService: ICoreService; + let optionsService: MockOptionsService; + let inputHandler: TestInputHandler; + + beforeEach(() => { + optionsService = new MockOptionsService(); + bufferService = new BufferService(optionsService); + bufferService.resize(80, 30); + coreService = new CoreService(() => {}, bufferService, new MockLogService(), optionsService); + + inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService()); + }); + + it('save and restore cursor', () => { + bufferService.buffer.x = 1; + bufferService.buffer.y = 2; + bufferService.buffer.ybase = 0; + inputHandler.curAttrData.fg = 3; + // Save cursor position + inputHandler.saveCursor(); + assert.equal(bufferService.buffer.x, 1); + assert.equal(bufferService.buffer.y, 2); + assert.equal(inputHandler.curAttrData.fg, 3); + // Change cursor position + bufferService.buffer.x = 10; + bufferService.buffer.y = 20; + inputHandler.curAttrData.fg = 30; + // Restore cursor position + inputHandler.restoreCursor(); + assert.equal(bufferService.buffer.x, 1); + assert.equal(bufferService.buffer.y, 2); + assert.equal(inputHandler.curAttrData.fg, 3); + }); + describe('setCursorStyle', () => { + it('should call Terminal.setOption with correct params', () => { + inputHandler.setCursorStyle(Params.fromArray([0])); + assert.equal(optionsService.options['cursorStyle'], 'block'); + assert.equal(optionsService.options['cursorBlink'], true); + + optionsService.options = clone(DEFAULT_OPTIONS); + inputHandler.setCursorStyle(Params.fromArray([1])); + assert.equal(optionsService.options['cursorStyle'], 'block'); + assert.equal(optionsService.options['cursorBlink'], true); + + optionsService.options = clone(DEFAULT_OPTIONS); + inputHandler.setCursorStyle(Params.fromArray([2])); + assert.equal(optionsService.options['cursorStyle'], 'block'); + assert.equal(optionsService.options['cursorBlink'], false); + + optionsService.options = clone(DEFAULT_OPTIONS); + inputHandler.setCursorStyle(Params.fromArray([3])); + assert.equal(optionsService.options['cursorStyle'], 'underline'); + assert.equal(optionsService.options['cursorBlink'], true); + + optionsService.options = clone(DEFAULT_OPTIONS); + inputHandler.setCursorStyle(Params.fromArray([4])); + assert.equal(optionsService.options['cursorStyle'], 'underline'); + assert.equal(optionsService.options['cursorBlink'], false); + + optionsService.options = clone(DEFAULT_OPTIONS); + inputHandler.setCursorStyle(Params.fromArray([5])); + assert.equal(optionsService.options['cursorStyle'], 'bar'); + assert.equal(optionsService.options['cursorBlink'], true); + + optionsService.options = clone(DEFAULT_OPTIONS); + inputHandler.setCursorStyle(Params.fromArray([6])); + assert.equal(optionsService.options['cursorStyle'], 'bar'); + assert.equal(optionsService.options['cursorBlink'], false); + }); + }); + describe('setMode', () => { + it('should toggle bracketedPasteMode', () => { + const coreService = new MockCoreService(); + const inputHandler = new InputHandler(new MockBufferService(80, 30), new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + // Set bracketed paste mode + inputHandler.setModePrivate(Params.fromArray([2004])); + assert.equal(coreService.decPrivateModes.bracketedPasteMode, true); + // Reset bracketed paste mode + inputHandler.resetModePrivate(Params.fromArray([2004])); + assert.equal(coreService.decPrivateModes.bracketedPasteMode, false); + }); + }); + describe('regression tests', function(): void { + function termContent(bufferService: IBufferService, trim: boolean): string[] { + const result = []; + for (let i = 0; i < bufferService.rows; ++i) result.push(bufferService.buffer.lines.get(i)!.translateToString(trim)); + return result; + } + + it('insertChars', function(): void { + const bufferService = new MockBufferService(80, 30); + const inputHandler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + + // insert some data in first and second line + inputHandler.parse(Array(bufferService.cols - 9).join('a')); + inputHandler.parse('1234567890'); + inputHandler.parse(Array(bufferService.cols - 9).join('a')); + inputHandler.parse('1234567890'); + const line1: IBufferLine = bufferService.buffer.lines.get(0)!; + expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '1234567890'); + + // insert one char from params = [0] + bufferService.buffer.y = 0; + bufferService.buffer.x = 70; + inputHandler.insertChars(Params.fromArray([0])); + expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' 123456789'); + + // insert one char from params = [1] + bufferService.buffer.y = 0; + bufferService.buffer.x = 70; + inputHandler.insertChars(Params.fromArray([1])); + expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' 12345678'); + + // insert two chars from params = [2] + bufferService.buffer.y = 0; + bufferService.buffer.x = 70; + inputHandler.insertChars(Params.fromArray([2])); + expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' 123456'); + + // insert 10 chars from params = [10] + bufferService.buffer.y = 0; + bufferService.buffer.x = 70; + inputHandler.insertChars(Params.fromArray([10])); + expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' '); + expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a')); + }); + it('deleteChars', function(): void { + const bufferService = new MockBufferService(80, 30); + const inputHandler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + + // insert some data in first and second line + inputHandler.parse(Array(bufferService.cols - 9).join('a')); + inputHandler.parse('1234567890'); + inputHandler.parse(Array(bufferService.cols - 9).join('a')); + inputHandler.parse('1234567890'); + const line1: IBufferLine = bufferService.buffer.lines.get(0)!; + expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '1234567890'); + + // delete one char from params = [0] + bufferService.buffer.y = 0; + bufferService.buffer.x = 70; + inputHandler.deleteChars(Params.fromArray([0])); + expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '234567890 '); + expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a') + '234567890'); + + // insert one char from params = [1] + bufferService.buffer.y = 0; + bufferService.buffer.x = 70; + inputHandler.deleteChars(Params.fromArray([1])); + expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '34567890 '); + expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a') + '34567890'); + + // insert two chars from params = [2] + bufferService.buffer.y = 0; + bufferService.buffer.x = 70; + inputHandler.deleteChars(Params.fromArray([2])); + expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + '567890 '); + expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a') + '567890'); + + // insert 10 chars from params = [10] + bufferService.buffer.y = 0; + bufferService.buffer.x = 70; + inputHandler.deleteChars(Params.fromArray([10])); + expect(line1.translateToString(false)).equals(Array(bufferService.cols - 9).join('a') + ' '); + expect(line1.translateToString(true)).equals(Array(bufferService.cols - 9).join('a')); + }); + it('eraseInLine', function(): void { + const bufferService = new MockBufferService(80, 30); + const inputHandler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + + // fill 6 lines to test 3 different states + inputHandler.parse(Array(bufferService.cols + 1).join('a')); + inputHandler.parse(Array(bufferService.cols + 1).join('a')); + inputHandler.parse(Array(bufferService.cols + 1).join('a')); + + // params[0] - right erase + bufferService.buffer.y = 0; + bufferService.buffer.x = 70; + inputHandler.eraseInLine(Params.fromArray([0])); + expect(bufferService.buffer.lines.get(0)!.translateToString(false)).equals(Array(71).join('a') + ' '); + + // params[1] - left erase + bufferService.buffer.y = 1; + bufferService.buffer.x = 70; + inputHandler.eraseInLine(Params.fromArray([1])); + expect(bufferService.buffer.lines.get(1)!.translateToString(false)).equals(Array(71).join(' ') + ' aaaaaaaaa'); + + // params[1] - left erase + bufferService.buffer.y = 2; + bufferService.buffer.x = 70; + inputHandler.eraseInLine(Params.fromArray([2])); + expect(bufferService.buffer.lines.get(2)!.translateToString(false)).equals(Array(bufferService.cols + 1).join(' ')); + + }); + it('eraseInDisplay', function(): void { + const bufferService = new MockBufferService(80, 7); + const inputHandler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + + // fill display with a's + for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + + // params [0] - right and below erase + bufferService.buffer.y = 5; + bufferService.buffer.x = 40; + inputHandler.eraseInDisplay(Params.fromArray([0])); + expect(termContent(bufferService, false)).eql([ + Array(bufferService.cols + 1).join('a'), + Array(bufferService.cols + 1).join('a'), + Array(bufferService.cols + 1).join('a'), + Array(bufferService.cols + 1).join('a'), + Array(bufferService.cols + 1).join('a'), + Array(40 + 1).join('a') + Array(bufferService.cols - 40 + 1).join(' '), + Array(bufferService.cols + 1).join(' ') + ]); + expect(termContent(bufferService, true)).eql([ + Array(bufferService.cols + 1).join('a'), + Array(bufferService.cols + 1).join('a'), + Array(bufferService.cols + 1).join('a'), + Array(bufferService.cols + 1).join('a'), + Array(bufferService.cols + 1).join('a'), + Array(40 + 1).join('a'), + '' + ]); + + // reset + bufferService.buffer.y = 0; + bufferService.buffer.x = 0; + for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + + // params [1] - left and above + bufferService.buffer.y = 5; + bufferService.buffer.x = 40; + inputHandler.eraseInDisplay(Params.fromArray([1])); + expect(termContent(bufferService, false)).eql([ + Array(bufferService.cols + 1).join(' '), + Array(bufferService.cols + 1).join(' '), + Array(bufferService.cols + 1).join(' '), + Array(bufferService.cols + 1).join(' '), + Array(bufferService.cols + 1).join(' '), + Array(41 + 1).join(' ') + Array(bufferService.cols - 41 + 1).join('a'), + Array(bufferService.cols + 1).join('a') + ]); + expect(termContent(bufferService, true)).eql([ + '', + '', + '', + '', + '', + Array(41 + 1).join(' ') + Array(bufferService.cols - 41 + 1).join('a'), + Array(bufferService.cols + 1).join('a') + ]); + + // reset + bufferService.buffer.y = 0; + bufferService.buffer.x = 0; + for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + + // params [2] - whole screen + bufferService.buffer.y = 5; + bufferService.buffer.x = 40; + inputHandler.eraseInDisplay(Params.fromArray([2])); + expect(termContent(bufferService, false)).eql([ + Array(bufferService.cols + 1).join(' '), + Array(bufferService.cols + 1).join(' '), + Array(bufferService.cols + 1).join(' '), + Array(bufferService.cols + 1).join(' '), + Array(bufferService.cols + 1).join(' '), + Array(bufferService.cols + 1).join(' '), + Array(bufferService.cols + 1).join(' ') + ]); + expect(termContent(bufferService, true)).eql([ + '', + '', + '', + '', + '', + '', + '' + ]); + + // reset and add a wrapped line + bufferService.buffer.y = 0; + bufferService.buffer.x = 0; + inputHandler.parse(Array(bufferService.cols + 1).join('a')); // line 0 + inputHandler.parse(Array(bufferService.cols + 10).join('a')); // line 1 and 2 + for (let i = 3; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + + // params[1] left and above with wrap + // confirm precondition that line 2 is wrapped + expect(bufferService.buffer.lines.get(2)!.isWrapped).true; + bufferService.buffer.y = 2; + bufferService.buffer.x = 40; + inputHandler.eraseInDisplay(Params.fromArray([1])); + expect(bufferService.buffer.lines.get(2)!.isWrapped).false; + + // reset and add a wrapped line + bufferService.buffer.y = 0; + bufferService.buffer.x = 0; + inputHandler.parse(Array(bufferService.cols + 1).join('a')); // line 0 + inputHandler.parse(Array(bufferService.cols + 10).join('a')); // line 1 and 2 + for (let i = 3; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a')); + + // params[1] left and above with wrap + // confirm precondition that line 2 is wrapped + expect(bufferService.buffer.lines.get(2)!.isWrapped).true; + bufferService.buffer.y = 1; + bufferService.buffer.x = 90; // Cursor is beyond last column + inputHandler.eraseInDisplay(Params.fromArray([1])); + expect(bufferService.buffer.lines.get(2)!.isWrapped).false; + }); + }); + describe('print', () => { + it('should not cause an infinite loop (regression test)', () => { + const inputHandler = new InputHandler(new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + const container = new Uint32Array(10); + container[0] = 0x200B; + inputHandler.print(container, 0, 1); + }); + }); + + describe('alt screen', () => { + let bufferService: IBufferService; + let handler: InputHandler; + + beforeEach(() => { + bufferService = new MockBufferService(80, 30); + handler = new InputHandler(bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService()); + }); + it('should handle DECSET/DECRST 47 (alt screen buffer)', () => { + handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST'); + expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal(''); + expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); + // Text color of 'TEST' should be red + expect((bufferService.buffer.lines.get(1)!.loadCell(4, new CellData()).getFgColor())).to.equal(1); + }); + it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => { + handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST'); + expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal(''); + expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal(' TEST'); + // Text color of 'TEST' should be red + expect((bufferService.buffer.lines.get(1)!.loadCell(4, new CellData()).getFgColor())).to.equal(1); + }); + it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => { + handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST'); + expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); + expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal('JUNK'); + // Text color of 'TEST' should be default + expect(bufferService.buffer.lines.get(0)!.loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); + // Text color of 'JUNK' should be red + expect((bufferService.buffer.lines.get(1)!.loadCell(0, new CellData()).getFgColor())).to.equal(1); + }); + it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => { + handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST'); + expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); + expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal(''); + // Text color of 'TEST' should be default + expect(bufferService.buffer.lines.get(0)!.loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); + }); + it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', () => { + handler.parse('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST'); + expect(bufferService.buffer.translateBufferLineToString(0, true)).to.equal('TEST'); + // Text color of 'TEST' should be default + expect(bufferService.buffer.lines.get(0)!.loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg); + handler.parse('\x1b[?1049h\x1b[uTEST'); + expect(bufferService.buffer.translateBufferLineToString(1, true)).to.equal('TEST'); + // Text color of 'TEST' should be red + expect((bufferService.buffer.lines.get(1)!.loadCell(0, new CellData()).getFgColor())).to.equal(1); + }); + it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', () => { + handler.parse('\x1b[42m\x1b[?1049h'); + // Buffer should be filled with green background + expect(bufferService.buffer.lines.get(20)!.loadCell(10, new CellData()).getBgColor()).to.equal(2); + }); + }); + + describe('text attributes', () => { + it('bold', () => { + inputHandler.parse('\x1b[1m'); + assert.equal(!!inputHandler.curAttrData.isBold(), true); + inputHandler.parse('\x1b[22m'); + assert.equal(!!inputHandler.curAttrData.isBold(), false); + }); + it('dim', () => { + inputHandler.parse('\x1b[2m'); + assert.equal(!!inputHandler.curAttrData.isDim(), true); + inputHandler.parse('\x1b[22m'); + assert.equal(!!inputHandler.curAttrData.isDim(), false); + }); + it('italic', () => { + inputHandler.parse('\x1b[3m'); + assert.equal(!!inputHandler.curAttrData.isItalic(), true); + inputHandler.parse('\x1b[23m'); + assert.equal(!!inputHandler.curAttrData.isItalic(), false); + }); + it('underline', () => { + inputHandler.parse('\x1b[4m'); + assert.equal(!!inputHandler.curAttrData.isUnderline(), true); + inputHandler.parse('\x1b[24m'); + assert.equal(!!inputHandler.curAttrData.isUnderline(), false); + }); + it('blink', () => { + inputHandler.parse('\x1b[5m'); + assert.equal(!!inputHandler.curAttrData.isBlink(), true); + inputHandler.parse('\x1b[25m'); + assert.equal(!!inputHandler.curAttrData.isBlink(), false); + }); + it('inverse', () => { + inputHandler.parse('\x1b[7m'); + assert.equal(!!inputHandler.curAttrData.isInverse(), true); + inputHandler.parse('\x1b[27m'); + assert.equal(!!inputHandler.curAttrData.isInverse(), false); + }); + it('invisible', () => { + inputHandler.parse('\x1b[8m'); + assert.equal(!!inputHandler.curAttrData.isInvisible(), true); + inputHandler.parse('\x1b[28m'); + assert.equal(!!inputHandler.curAttrData.isInvisible(), false); + }); + it('colormode palette 16', () => { + assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); // DEFAULT + assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); // DEFAULT + // lower 8 colors + for (let i = 0; i < 8; ++i) { + inputHandler.parse(`\x1b[${i + 30};${i + 40}m`); + assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P16); + assert.equal(inputHandler.curAttrData.getFgColor(), i); + assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P16); + assert.equal(inputHandler.curAttrData.getBgColor(), i); + } + // reset to DEFAULT + inputHandler.parse(`\x1b[39;49m`); + assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); + assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); + }); + it('colormode palette 256', () => { + assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); // DEFAULT + assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); // DEFAULT + // lower 8 colors + for (let i = 0; i < 256; ++i) { + inputHandler.parse(`\x1b[38;5;${i};48;5;${i}m`); + assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P256); + assert.equal(inputHandler.curAttrData.getFgColor(), i); + assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P256); + assert.equal(inputHandler.curAttrData.getBgColor(), i); + } + // reset to DEFAULT + inputHandler.parse(`\x1b[39;49m`); + assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); + assert.equal(inputHandler.curAttrData.getFgColor(), -1); + assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); + assert.equal(inputHandler.curAttrData.getBgColor(), -1); + }); + it('colormode RGB', () => { + assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); // DEFAULT + assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); // DEFAULT + inputHandler.parse(`\x1b[38;2;1;2;3;48;2;4;5;6m`); + assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_RGB); + assert.equal(inputHandler.curAttrData.getFgColor(), 1 << 16 | 2 << 8 | 3); + assert.deepEqual(AttributeData.toColorRGB(inputHandler.curAttrData.getFgColor()), [1, 2, 3]); + assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_RGB); + assert.deepEqual(AttributeData.toColorRGB(inputHandler.curAttrData.getBgColor()), [4, 5, 6]); + // reset to DEFAULT + inputHandler.parse(`\x1b[39;49m`); + assert.equal(inputHandler.curAttrData.getFgColorMode(), 0); + assert.equal(inputHandler.curAttrData.getFgColor(), -1); + assert.equal(inputHandler.curAttrData.getBgColorMode(), 0); + assert.equal(inputHandler.curAttrData.getBgColor(), -1); + }); + it('colormode transition RGB to 256', () => { + // enter RGB for FG and BG + inputHandler.parse(`\x1b[38;2;1;2;3;48;2;4;5;6m`); + // enter 256 for FG and BG + inputHandler.parse(`\x1b[38;5;255;48;5;255m`); + assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P256); + assert.equal(inputHandler.curAttrData.getFgColor(), 255); + assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P256); + assert.equal(inputHandler.curAttrData.getBgColor(), 255); + }); + it('colormode transition RGB to 16', () => { + // enter RGB for FG and BG + inputHandler.parse(`\x1b[38;2;1;2;3;48;2;4;5;6m`); + // enter 16 for FG and BG + inputHandler.parse(`\x1b[37;47m`); + assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P16); + assert.equal(inputHandler.curAttrData.getFgColor(), 7); + assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P16); + assert.equal(inputHandler.curAttrData.getBgColor(), 7); + }); + it('colormode transition 16 to 256', () => { + // enter 16 for FG and BG + inputHandler.parse(`\x1b[37;47m`); + // enter 256 for FG and BG + inputHandler.parse(`\x1b[38;5;255;48;5;255m`); + assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P256); + assert.equal(inputHandler.curAttrData.getFgColor(), 255); + assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P256); + assert.equal(inputHandler.curAttrData.getBgColor(), 255); + }); + it('colormode transition 256 to 16', () => { + // enter 256 for FG and BG + inputHandler.parse(`\x1b[38;5;255;48;5;255m`); + // enter 16 for FG and BG + inputHandler.parse(`\x1b[37;47m`); + assert.equal(inputHandler.curAttrData.getFgColorMode(), Attributes.CM_P16); + assert.equal(inputHandler.curAttrData.getFgColor(), 7); + assert.equal(inputHandler.curAttrData.getBgColorMode(), Attributes.CM_P16); + assert.equal(inputHandler.curAttrData.getBgColor(), 7); + }); + it('should zero missing RGB values', () => { + inputHandler.parse(`\x1b[38;2;1;2;3m`); + inputHandler.parse(`\x1b[38;2;5m`); + assert.deepEqual(AttributeData.toColorRGB(inputHandler.curAttrData.getFgColor()), [5, 0, 0]); + }); + }); + describe('colon notation', () => { + let inputHandler2: TestInputHandler; + beforeEach(() => { + inputHandler2 = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService()); + }); + describe('should equal to semicolon', () => { + it('CSI 38:2::50:100:150 m', () => { + inputHandler.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.parse('\x1b[38;2;50;100;150m'); + inputHandler.parse('\x1b[38:2::50:100:150m'); + assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + it('CSI 38:2::50:100: m', () => { + inputHandler.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.parse('\x1b[38;2;50;100;m'); + inputHandler.parse('\x1b[38:2::50:100:m'); + assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + it('CSI 38:2::50:: m', () => { + inputHandler.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.parse('\x1b[38;2;50;;m'); + inputHandler.parse('\x1b[38:2::50::m'); + assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 0 << 8 | 0); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + it('CSI 38:2:::: m', () => { + inputHandler.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.parse('\x1b[38;2;;;m'); + inputHandler.parse('\x1b[38:2::::m'); + assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0 << 16 | 0 << 8 | 0); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + it('CSI 38;2::50:100:150 m', () => { + inputHandler.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.parse('\x1b[38;2;50;100;150m'); + inputHandler.parse('\x1b[38;2::50:100:150m'); + assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + it('CSI 38;2;50:100:150 m', () => { + inputHandler.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.parse('\x1b[38;2;50;100;150m'); + inputHandler.parse('\x1b[38;2;50:100:150m'); + assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + it('CSI 38;2;50;100:150 m', () => { + inputHandler.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.parse('\x1b[38;2;50;100;150m'); + inputHandler.parse('\x1b[38;2;50;100:150m'); + assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + it('CSI 38:5:50 m', () => { + inputHandler.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.parse('\x1b[38;5;50m'); + inputHandler.parse('\x1b[38:5:50m'); + assert.equal(inputHandler2.curAttrData.fg & 0xFF, 50); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + it('CSI 38:5: m', () => { + inputHandler.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.parse('\x1b[38;5;m'); + inputHandler.parse('\x1b[38:5:m'); + assert.equal(inputHandler2.curAttrData.fg & 0xFF, 0); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + it('CSI 38;5:50 m', () => { + inputHandler.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.parse('\x1b[38;5;50m'); + inputHandler.parse('\x1b[38;5:50m'); + assert.equal(inputHandler2.curAttrData.fg & 0xFF, 50); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + }); + describe('should fill early sequence end with default of 0', () => { + it('CSI 38:2 m', () => { + inputHandler.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.parse('\x1b[38;2m'); + inputHandler.parse('\x1b[38:2m'); + assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0 << 16 | 0 << 8 | 0); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + it('CSI 38:5 m', () => { + inputHandler.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.curAttrData.fg = 0xFFFFFFFF; + inputHandler2.parse('\x1b[38;5m'); + inputHandler.parse('\x1b[38:5m'); + assert.equal(inputHandler2.curAttrData.fg & 0xFF, 0); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + }); + describe('should not interfere with leading/following SGR attrs', () => { + it('CSI 1 ; 38:2::50:100:150 ; 4 m', () => { + inputHandler2.parse('\x1b[1;38;2;50;100;150;4m'); + inputHandler.parse('\x1b[1;38:2::50:100:150;4m'); + assert.equal(!!inputHandler2.curAttrData.isBold(), true); + assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); + assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 150); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + it('CSI 1 ; 38:2::50:100: ; 4 m', () => { + inputHandler2.parse('\x1b[1;38;2;50;100;;4m'); + inputHandler.parse('\x1b[1;38:2::50:100:;4m'); + assert.equal(!!inputHandler2.curAttrData.isBold(), true); + assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); + assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + it('CSI 1 ; 38:2::50:100 ; 4 m', () => { + inputHandler2.parse('\x1b[1;38;2;50;100;;4m'); + inputHandler.parse('\x1b[1;38:2::50:100;4m'); + assert.equal(!!inputHandler2.curAttrData.isBold(), true); + assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); + assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 50 << 16 | 100 << 8 | 0); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + it('CSI 1 ; 38:2:: ; 4 m', () => { + inputHandler2.parse('\x1b[1;38;2;;;;4m'); + inputHandler.parse('\x1b[1;38:2::;4m'); + assert.equal(!!inputHandler2.curAttrData.isBold(), true); + assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); + assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + it('CSI 1 ; 38;2:: ; 4 m', () => { + inputHandler2.parse('\x1b[1;38;2;;;;4m'); + inputHandler.parse('\x1b[1;38;2::;4m'); + assert.equal(!!inputHandler2.curAttrData.isBold(), true); + assert.equal(!!inputHandler2.curAttrData.isUnderline(), true); + assert.equal(inputHandler2.curAttrData.fg & 0xFFFFFF, 0); + assert.equal(inputHandler.curAttrData.fg, inputHandler2.curAttrData.fg); + }); + }); + }); + describe('cursor positioning', () => { + beforeEach(() => { + bufferService.resize(10, 10); + }); + it('cursor forward (CUF)', () => { + inputHandler.parse('\x1b[C'); + assert.deepEqual(getCursor(bufferService), [1, 0]); + inputHandler.parse('\x1b[1C'); + assert.deepEqual(getCursor(bufferService), [2, 0]); + inputHandler.parse('\x1b[4C'); + assert.deepEqual(getCursor(bufferService), [6, 0]); + inputHandler.parse('\x1b[100C'); + assert.deepEqual(getCursor(bufferService), [9, 0]); + // should not change y + bufferService.buffer.x = 8; + bufferService.buffer.y = 4; + inputHandler.parse('\x1b[C'); + assert.deepEqual(getCursor(bufferService), [9, 4]); + }); + it('cursor backward (CUB)', () => { + inputHandler.parse('\x1b[D'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + inputHandler.parse('\x1b[1D'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + // place cursor at end of first line + inputHandler.parse('\x1b[100C'); + inputHandler.parse('\x1b[D'); + assert.deepEqual(getCursor(bufferService), [8, 0]); + inputHandler.parse('\x1b[1D'); + assert.deepEqual(getCursor(bufferService), [7, 0]); + inputHandler.parse('\x1b[4D'); + assert.deepEqual(getCursor(bufferService), [3, 0]); + inputHandler.parse('\x1b[100D'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + // should not change y + bufferService.buffer.x = 4; + bufferService.buffer.y = 4; + inputHandler.parse('\x1b[D'); + assert.deepEqual(getCursor(bufferService), [3, 4]); + }); + it('cursor down (CUD)', () => { + inputHandler.parse('\x1b[B'); + assert.deepEqual(getCursor(bufferService), [0, 1]); + inputHandler.parse('\x1b[1B'); + assert.deepEqual(getCursor(bufferService), [0, 2]); + inputHandler.parse('\x1b[4B'); + assert.deepEqual(getCursor(bufferService), [0, 6]); + inputHandler.parse('\x1b[100B'); + assert.deepEqual(getCursor(bufferService), [0, 9]); + // should not change x + bufferService.buffer.x = 8; + bufferService.buffer.y = 0; + inputHandler.parse('\x1b[B'); + assert.deepEqual(getCursor(bufferService), [8, 1]); + }); + it('cursor up (CUU)', () => { + inputHandler.parse('\x1b[A'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + inputHandler.parse('\x1b[1A'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + // place cursor at beginning of last row + inputHandler.parse('\x1b[100B'); + inputHandler.parse('\x1b[A'); + assert.deepEqual(getCursor(bufferService), [0, 8]); + inputHandler.parse('\x1b[1A'); + assert.deepEqual(getCursor(bufferService), [0, 7]); + inputHandler.parse('\x1b[4A'); + assert.deepEqual(getCursor(bufferService), [0, 3]); + inputHandler.parse('\x1b[100A'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + // should not change x + bufferService.buffer.x = 8; + bufferService.buffer.y = 9; + inputHandler.parse('\x1b[A'); + assert.deepEqual(getCursor(bufferService), [8, 8]); + }); + it('cursor next line (CNL)', () => { + inputHandler.parse('\x1b[E'); + assert.deepEqual(getCursor(bufferService), [0, 1]); + inputHandler.parse('\x1b[1E'); + assert.deepEqual(getCursor(bufferService), [0, 2]); + inputHandler.parse('\x1b[4E'); + assert.deepEqual(getCursor(bufferService), [0, 6]); + inputHandler.parse('\x1b[100E'); + assert.deepEqual(getCursor(bufferService), [0, 9]); + // should reset x to zero + bufferService.buffer.x = 8; + bufferService.buffer.y = 0; + inputHandler.parse('\x1b[E'); + assert.deepEqual(getCursor(bufferService), [0, 1]); + }); + it('cursor previous line (CPL)', () => { + inputHandler.parse('\x1b[F'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + inputHandler.parse('\x1b[1F'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + // place cursor at beginning of last row + inputHandler.parse('\x1b[100E'); + inputHandler.parse('\x1b[F'); + assert.deepEqual(getCursor(bufferService), [0, 8]); + inputHandler.parse('\x1b[1F'); + assert.deepEqual(getCursor(bufferService), [0, 7]); + inputHandler.parse('\x1b[4F'); + assert.deepEqual(getCursor(bufferService), [0, 3]); + inputHandler.parse('\x1b[100F'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + // should reset x to zero + bufferService.buffer.x = 8; + bufferService.buffer.y = 9; + inputHandler.parse('\x1b[F'); + assert.deepEqual(getCursor(bufferService), [0, 8]); + }); + it('cursor character absolute (CHA)', () => { + inputHandler.parse('\x1b[G'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + inputHandler.parse('\x1b[1G'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + inputHandler.parse('\x1b[2G'); + assert.deepEqual(getCursor(bufferService), [1, 0]); + inputHandler.parse('\x1b[5G'); + assert.deepEqual(getCursor(bufferService), [4, 0]); + inputHandler.parse('\x1b[100G'); + assert.deepEqual(getCursor(bufferService), [9, 0]); + }); + it('cursor position (CUP)', () => { + bufferService.buffer.x = 5; + bufferService.buffer.y = 5; + inputHandler.parse('\x1b[H'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + bufferService.buffer.x = 5; + bufferService.buffer.y = 5; + inputHandler.parse('\x1b[1H'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + bufferService.buffer.x = 5; + bufferService.buffer.y = 5; + inputHandler.parse('\x1b[1;1H'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + bufferService.buffer.x = 5; + bufferService.buffer.y = 5; + inputHandler.parse('\x1b[8H'); + assert.deepEqual(getCursor(bufferService), [0, 7]); + bufferService.buffer.x = 5; + bufferService.buffer.y = 5; + inputHandler.parse('\x1b[;8H'); + assert.deepEqual(getCursor(bufferService), [7, 0]); + bufferService.buffer.x = 5; + bufferService.buffer.y = 5; + inputHandler.parse('\x1b[100;100H'); + assert.deepEqual(getCursor(bufferService), [9, 9]); + }); + it('horizontal position absolute (HPA)', () => { + inputHandler.parse('\x1b[`'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + inputHandler.parse('\x1b[1`'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + inputHandler.parse('\x1b[2`'); + assert.deepEqual(getCursor(bufferService), [1, 0]); + inputHandler.parse('\x1b[5`'); + assert.deepEqual(getCursor(bufferService), [4, 0]); + inputHandler.parse('\x1b[100`'); + assert.deepEqual(getCursor(bufferService), [9, 0]); + }); + it('horizontal position relative (HPR)', () => { + inputHandler.parse('\x1b[a'); + assert.deepEqual(getCursor(bufferService), [1, 0]); + inputHandler.parse('\x1b[1a'); + assert.deepEqual(getCursor(bufferService), [2, 0]); + inputHandler.parse('\x1b[4a'); + assert.deepEqual(getCursor(bufferService), [6, 0]); + inputHandler.parse('\x1b[100a'); + assert.deepEqual(getCursor(bufferService), [9, 0]); + // should not change y + bufferService.buffer.x = 8; + bufferService.buffer.y = 4; + inputHandler.parse('\x1b[a'); + assert.deepEqual(getCursor(bufferService), [9, 4]); + }); + it('vertical position absolute (VPA)', () => { + inputHandler.parse('\x1b[d'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + inputHandler.parse('\x1b[1d'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + inputHandler.parse('\x1b[2d'); + assert.deepEqual(getCursor(bufferService), [0, 1]); + inputHandler.parse('\x1b[5d'); + assert.deepEqual(getCursor(bufferService), [0, 4]); + inputHandler.parse('\x1b[100d'); + assert.deepEqual(getCursor(bufferService), [0, 9]); + // should not change x + bufferService.buffer.x = 8; + bufferService.buffer.y = 4; + inputHandler.parse('\x1b[d'); + assert.deepEqual(getCursor(bufferService), [8, 0]); + }); + it('vertical position relative (VPR)', () => { + inputHandler.parse('\x1b[e'); + assert.deepEqual(getCursor(bufferService), [0, 1]); + inputHandler.parse('\x1b[1e'); + assert.deepEqual(getCursor(bufferService), [0, 2]); + inputHandler.parse('\x1b[4e'); + assert.deepEqual(getCursor(bufferService), [0, 6]); + inputHandler.parse('\x1b[100e'); + assert.deepEqual(getCursor(bufferService), [0, 9]); + // should not change x + bufferService.buffer.x = 8; + bufferService.buffer.y = 4; + inputHandler.parse('\x1b[e'); + assert.deepEqual(getCursor(bufferService), [8, 5]); + }); + describe('should clamp cursor into addressible range', () => { + it('CUF', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[C'); + assert.deepEqual(getCursor(bufferService), [9, 9]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[C'); + assert.deepEqual(getCursor(bufferService), [1, 0]); + }); + it('CUB', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[D'); + assert.deepEqual(getCursor(bufferService), [8, 9]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[D'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + }); + it('CUD', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[B'); + assert.deepEqual(getCursor(bufferService), [9, 9]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[B'); + assert.deepEqual(getCursor(bufferService), [0, 1]); + }); + it('CUU', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[A'); + assert.deepEqual(getCursor(bufferService), [9, 8]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[A'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + }); + it('CNL', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[E'); + assert.deepEqual(getCursor(bufferService), [0, 9]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[E'); + assert.deepEqual(getCursor(bufferService), [0, 1]); + }); + it('CPL', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[F'); + assert.deepEqual(getCursor(bufferService), [0, 8]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[F'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + }); + it('CHA', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[5G'); + assert.deepEqual(getCursor(bufferService), [4, 9]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[5G'); + assert.deepEqual(getCursor(bufferService), [4, 0]); + }); + it('CUP', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[5;5H'); + assert.deepEqual(getCursor(bufferService), [4, 4]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[5;5H'); + assert.deepEqual(getCursor(bufferService), [4, 4]); + }); + it('HPA', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[5`'); + assert.deepEqual(getCursor(bufferService), [4, 9]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[5`'); + assert.deepEqual(getCursor(bufferService), [4, 0]); + }); + it('HPR', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[a'); + assert.deepEqual(getCursor(bufferService), [9, 9]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[a'); + assert.deepEqual(getCursor(bufferService), [1, 0]); + }); + it('VPA', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[5d'); + assert.deepEqual(getCursor(bufferService), [9, 4]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[5d'); + assert.deepEqual(getCursor(bufferService), [0, 4]); + }); + it('VPR', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[e'); + assert.deepEqual(getCursor(bufferService), [9, 9]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[e'); + assert.deepEqual(getCursor(bufferService), [0, 1]); + }); + it('DCH', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[P'); + assert.deepEqual(getCursor(bufferService), [9, 9]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[P'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + }); + it('DCH - should delete last cell', () => { + inputHandler.parse('0123456789\x1b[P'); + assert.equal(bufferService.buffer.lines.get(0)!.translateToString(false), '012345678 '); + }); + it('ECH', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[X'); + assert.deepEqual(getCursor(bufferService), [9, 9]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[X'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + }); + it('ECH - should delete last cell', () => { + inputHandler.parse('0123456789\x1b[X'); + assert.equal(bufferService.buffer.lines.get(0)!.translateToString(false), '012345678 '); + }); + it('ICH', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[@'); + assert.deepEqual(getCursor(bufferService), [9, 9]); + bufferService.buffer.x = -10000; + bufferService.buffer.y = -10000; + inputHandler.parse('\x1b[@'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + }); + it('ICH - should delete last cell', () => { + inputHandler.parse('0123456789\x1b[@'); + assert.equal(bufferService.buffer.lines.get(0)!.translateToString(false), '012345678 '); + }); + }); + }); + describe('DECSTBM - scroll margins', () => { + beforeEach(() => { + bufferService.resize(10, 10); + }); + it('should default to whole viewport', () => { + inputHandler.parse('\x1b[r'); + assert.equal(bufferService.buffer.scrollTop, 0); + assert.equal(bufferService.buffer.scrollBottom, 9); + inputHandler.parse('\x1b[3;7r'); + assert.equal(bufferService.buffer.scrollTop, 2); + assert.equal(bufferService.buffer.scrollBottom, 6); + inputHandler.parse('\x1b[0;0r'); + assert.equal(bufferService.buffer.scrollTop, 0); + assert.equal(bufferService.buffer.scrollBottom, 9); + }); + it('should clamp bottom', () => { + inputHandler.parse('\x1b[3;1000r'); + assert.equal(bufferService.buffer.scrollTop, 2); + assert.equal(bufferService.buffer.scrollBottom, 9); + }); + it('should only apply for top < bottom', () => { + inputHandler.parse('\x1b[7;2r'); + assert.equal(bufferService.buffer.scrollTop, 0); + assert.equal(bufferService.buffer.scrollBottom, 9); + }); + it('should home cursor', () => { + bufferService.buffer.x = 10000; + bufferService.buffer.y = 10000; + inputHandler.parse('\x1b[2;7r'); + assert.deepEqual(getCursor(bufferService), [0, 0]); + }); + }); + describe('scroll margins', () => { + beforeEach(() => { + bufferService.resize(10, 10); + }); + it('scrollUp', () => { + inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Sm'); + assert.deepEqual(getLines(bufferService), ['m', '3', '', '', '4', '5', '6', '7', '8', '9']); + }); + it('scrollDown', () => { + inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Tm'); + assert.deepEqual(getLines(bufferService), ['m', '', '', '1', '4', '5', '6', '7', '8', '9']); + }); + it('insertLines - out of margins', () => { + inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); + assert.equal(bufferService.buffer.scrollTop, 2); + assert.equal(bufferService.buffer.scrollBottom, 5); + inputHandler.parse('\x1b[2Lm'); + assert.deepEqual(getLines(bufferService), ['m', '1', '2', '3', '4', '5', '6', '7', '8', '9']); + inputHandler.parse('\x1b[2H\x1b[2Ln'); + assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', '6', '7', '8', '9']); + // skip below scrollbottom + inputHandler.parse('\x1b[7H\x1b[2Lo'); + assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', '7', '8', '9']); + inputHandler.parse('\x1b[8H\x1b[2Lp'); + assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', '9']); + inputHandler.parse('\x1b[100H\x1b[2Lq'); + assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', 'q']); + }); + it('insertLines - within margins', () => { + inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); + assert.equal(bufferService.buffer.scrollTop, 2); + assert.equal(bufferService.buffer.scrollBottom, 5); + inputHandler.parse('\x1b[3H\x1b[2Lm'); + assert.deepEqual(getLines(bufferService), ['0', '1', 'm', '', '2', '3', '6', '7', '8', '9']); + inputHandler.parse('\x1b[6H\x1b[2Ln'); + assert.deepEqual(getLines(bufferService), ['0', '1', 'm', '', '2', 'n', '6', '7', '8', '9']); + }); + it('deleteLines - out of margins', () => { + inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); + assert.equal(bufferService.buffer.scrollTop, 2); + assert.equal(bufferService.buffer.scrollBottom, 5); + inputHandler.parse('\x1b[2Mm'); + assert.deepEqual(getLines(bufferService), ['m', '1', '2', '3', '4', '5', '6', '7', '8', '9']); + inputHandler.parse('\x1b[2H\x1b[2Mn'); + assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', '6', '7', '8', '9']); + // skip below scrollbottom + inputHandler.parse('\x1b[7H\x1b[2Mo'); + assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', '7', '8', '9']); + inputHandler.parse('\x1b[8H\x1b[2Mp'); + assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', '9']); + inputHandler.parse('\x1b[100H\x1b[2Mq'); + assert.deepEqual(getLines(bufferService), ['m', 'n', '2', '3', '4', '5', 'o', 'p', '8', 'q']); + }); + it('deleteLines - within margins', () => { + inputHandler.parse('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[3;6r'); + assert.equal(bufferService.buffer.scrollTop, 2); + assert.equal(bufferService.buffer.scrollBottom, 5); + inputHandler.parse('\x1b[6H\x1b[2Mm'); + assert.deepEqual(getLines(bufferService), ['0', '1', '2', '3', '4', 'm', '6', '7', '8', '9']); + inputHandler.parse('\x1b[3H\x1b[2Mn'); + assert.deepEqual(getLines(bufferService), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']); + }); + }); + it('should parse big chunks in smaller subchunks', () => { + // max single chunk size is hardcoded as 131072 + const calls: any[] = []; + bufferService.resize(10, 10); + (inputHandler as any)._parser.parse = (data: Uint32Array, length: number) => { + calls.push([data.length, length]); + }; + inputHandler.parse('12345'); + inputHandler.parse('a'.repeat(10000)); + inputHandler.parse('a'.repeat(200000)); + inputHandler.parse('a'.repeat(300000)); + assert.deepEqual(calls, [ + [4096, 5], + [10000, 10000], + [131072, 131072], [131072, 200000 - 131072], + [131072, 131072], [131072, 131072], [131072, 300000 - 131072 - 131072] + ]); + }); + describe('windowOptions', () => { + it('all should be disabled by default and not report', () => { + bufferService.resize(10, 10); + const stack: string[] = []; + coreService.onData(data => stack.push(data)); + inputHandler.parse('\x1b[14t'); + inputHandler.parse('\x1b[16t'); + inputHandler.parse('\x1b[18t'); + inputHandler.parse('\x1b[20t'); + inputHandler.parse('\x1b[21t'); + assert.deepEqual(stack, []); + }); + it('14 - GetWinSizePixels', () => { + bufferService.resize(10, 10); + optionsService.options.windowOptions.getWinSizePixels = true; + const stack: string[] = []; + coreService.onData(data => stack.push(data)); + inputHandler.parse('\x1b[14t'); + // does not report in test terminal due to missing renderer + assert.deepEqual(stack, []); + }); + it('16 - GetCellSizePixels', () => { + bufferService.resize(10, 10); + optionsService.options.windowOptions.getCellSizePixels = true; + const stack: string[] = []; + coreService.onData(data => stack.push(data)); + inputHandler.parse('\x1b[16t'); + // does not report in test terminal due to missing renderer + assert.deepEqual(stack, []); + }); + it('18 - GetWinSizeChars', () => { + bufferService.resize(10, 10); + optionsService.options.windowOptions.getWinSizeChars = true; + const stack: string[] = []; + coreService.onData(data => stack.push(data)); + inputHandler.parse('\x1b[18t'); + assert.deepEqual(stack, ['\x1b[8;10;10t']); + bufferService.resize(50, 20); + inputHandler.parse('\x1b[18t'); + assert.deepEqual(stack, ['\x1b[8;10;10t', '\x1b[8;20;50t']); + }); + it('22/23 - PushTitle/PopTitle', () => { + bufferService.resize(10, 10); + optionsService.options.windowOptions.pushTitle = true; + optionsService.options.windowOptions.popTitle = true; + const stack: string[] = []; + inputHandler.onTitleChange(data => stack.push(data)); + inputHandler.parse('\x1b]0;1\x07'); + inputHandler.parse('\x1b[22t'); + inputHandler.parse('\x1b]0;2\x07'); + inputHandler.parse('\x1b[22t'); + inputHandler.parse('\x1b]0;3\x07'); + inputHandler.parse('\x1b[22t'); + assert.deepEqual(inputHandler.windowTitleStack, ['1', '2', '3']); + assert.deepEqual(inputHandler.iconNameStack, ['1', '2', '3']); + assert.deepEqual(stack, ['1', '2', '3']); + inputHandler.parse('\x1b[23t'); + inputHandler.parse('\x1b[23t'); + inputHandler.parse('\x1b[23t'); + inputHandler.parse('\x1b[23t'); // one more to test "overflow" + assert.deepEqual(inputHandler.windowTitleStack, []); + assert.deepEqual(inputHandler.iconNameStack, []); + assert.deepEqual(stack, ['1', '2', '3', '3', '2', '1']); + }); + it('22/23 - PushTitle/PopTitle with ;1', () => { + bufferService.resize(10, 10); + optionsService.options.windowOptions.pushTitle = true; + optionsService.options.windowOptions.popTitle = true; + const stack: string[] = []; + inputHandler.onTitleChange(data => stack.push(data)); + inputHandler.parse('\x1b]0;1\x07'); + inputHandler.parse('\x1b[22;1t'); + inputHandler.parse('\x1b]0;2\x07'); + inputHandler.parse('\x1b[22;1t'); + inputHandler.parse('\x1b]0;3\x07'); + inputHandler.parse('\x1b[22;1t'); + assert.deepEqual(inputHandler.windowTitleStack, []); + assert.deepEqual(inputHandler.iconNameStack, ['1', '2', '3']); + assert.deepEqual(stack, ['1', '2', '3']); + inputHandler.parse('\x1b[23;1t'); + inputHandler.parse('\x1b[23;1t'); + inputHandler.parse('\x1b[23;1t'); + inputHandler.parse('\x1b[23;1t'); // one more to test "overflow" + assert.deepEqual(inputHandler.windowTitleStack, []); + assert.deepEqual(inputHandler.iconNameStack, []); + assert.deepEqual(stack, ['1', '2', '3']); + }); + it('22/23 - PushTitle/PopTitle with ;2', () => { + bufferService.resize(10, 10); + optionsService.options.windowOptions.pushTitle = true; + optionsService.options.windowOptions.popTitle = true; + const stack: string[] = []; + inputHandler.onTitleChange(data => stack.push(data)); + inputHandler.parse('\x1b]0;1\x07'); + inputHandler.parse('\x1b[22;2t'); + inputHandler.parse('\x1b]0;2\x07'); + inputHandler.parse('\x1b[22;2t'); + inputHandler.parse('\x1b]0;3\x07'); + inputHandler.parse('\x1b[22;2t'); + assert.deepEqual(inputHandler.windowTitleStack, ['1', '2', '3']); + assert.deepEqual(inputHandler.iconNameStack, []); + assert.deepEqual(stack, ['1', '2', '3']); + inputHandler.parse('\x1b[23;2t'); + inputHandler.parse('\x1b[23;2t'); + inputHandler.parse('\x1b[23;2t'); + inputHandler.parse('\x1b[23;2t'); // one more to test "overflow" + assert.deepEqual(inputHandler.windowTitleStack, []); + assert.deepEqual(inputHandler.iconNameStack, []); + assert.deepEqual(stack, ['1', '2', '3', '3', '2', '1']); + }); + it('DECCOLM - should only work with "SetWinLines" (24) enabled', () => { + // disabled + bufferService.resize(10, 10); + inputHandler.parse('\x1b[?3l'); + assert.equal(bufferService.cols, 10); + inputHandler.parse('\x1b[?3h'); + assert.equal(bufferService.cols, 10); + // enabled + inputHandler.reset(); + optionsService.options.windowOptions.setWinLines = true; + inputHandler.parse('\x1b[?3l'); + assert.equal(bufferService.cols, 80); + inputHandler.parse('\x1b[?3h'); + assert.equal(bufferService.cols, 132); + }); + }); + describe('should correctly reset cells taken by wide chars', () => { + beforeEach(() => { + bufferService.resize(10, 5); + optionsService.options.scrollback = 1; + inputHandler.parse('¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥'); + }); + it('print', () => { + inputHandler.parse('\x1b[H#'); + assert.deepEqual(getLines(bufferService), ['# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + inputHandler.parse('\x1b[1;6H######'); + assert.deepEqual(getLines(bufferService), ['# ¥ #####', '# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + inputHandler.parse('#'); + assert.deepEqual(getLines(bufferService), ['# ¥ #####', '##¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + inputHandler.parse('#'); + assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + inputHandler.parse('\x1b[3;9H#'); + assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥#', '¥¥¥¥¥', '']); + inputHandler.parse('#'); + assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '¥¥¥¥¥', '']); + inputHandler.parse('#'); + assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥¥', '']); + inputHandler.parse('\x1b[4;10H#'); + assert.deepEqual(getLines(bufferService), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥ #', '']); + }); + it('EL', () => { + inputHandler.parse('\x1b[1;6H\x1b[K#'); + assert.deepEqual(getLines(bufferService), ['¥¥ #', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + inputHandler.parse('\x1b[2;5H\x1b[1K'); + assert.deepEqual(getLines(bufferService), ['¥¥ #', ' ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + inputHandler.parse('\x1b[3;6H\x1b[1K'); + assert.deepEqual(getLines(bufferService), ['¥¥ #', ' ¥¥', ' ¥¥', '¥¥¥¥¥', '']); + }); + it('ICH', () => { + inputHandler.parse('\x1b[1;6H\x1b[@'); + assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + inputHandler.parse('\x1b[2;4H\x1b[2@'); + assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + inputHandler.parse('\x1b[3;4H\x1b[3@'); + assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥¥¥¥¥', '']); + inputHandler.parse('\x1b[4;4H\x1b[4@'); + assert.deepEqual(getLines(bufferService), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥ ¥', '']); + }); + it('DCH', () => { + inputHandler.parse('\x1b[1;6H\x1b[P'); + assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + inputHandler.parse('\x1b[2;6H\x1b[2P'); + assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + inputHandler.parse('\x1b[3;6H\x1b[3P'); + assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']); + }); + it('ECH', () => { + inputHandler.parse('\x1b[1;6H\x1b[X'); + assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + inputHandler.parse('\x1b[2;6H\x1b[2X'); + assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']); + inputHandler.parse('\x1b[3;6H\x1b[3X'); + assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']); + }); + }); + describe('DECSTR', () => { + beforeEach(() => { + bufferService.resize(10, 5); + optionsService.options.scrollback = 1; + inputHandler.parse('01234567890123'); + }); + it('should reset IRM', () => { + inputHandler.parse('\x1b[4h'); + assert.equal(coreService.modes.insertMode, true); + inputHandler.parse('\x1b[!p'); + assert.equal(coreService.modes.insertMode, false); + }); + it('should reset cursor visibility', () => { + inputHandler.parse('\x1b[?25l'); + assert.equal(coreService.isCursorHidden, true); + inputHandler.parse('\x1b[!p'); + assert.equal(coreService.isCursorHidden, false); + }); + it('should reset scroll margins', () => { + inputHandler.parse('\x1b[2;4r'); + assert.equal(bufferService.buffer.scrollTop, 1); + assert.equal(bufferService.buffer.scrollBottom, 3); + inputHandler.parse('\x1b[!p'); + assert.equal(bufferService.buffer.scrollTop, 0); + assert.equal(bufferService.buffer.scrollBottom, bufferService.rows - 1); + }); + it('should reset text attributes', () => { + inputHandler.parse('\x1b[1;2;32;43m'); + assert.equal(!!inputHandler.curAttrData.isBold(), true); + inputHandler.parse('\x1b[!p'); + assert.equal(!!inputHandler.curAttrData.isBold(), false); + assert.equal(inputHandler.curAttrData.fg, 0); + assert.equal(inputHandler.curAttrData.bg, 0); + }); + it('should reset DECSC data', () => { + inputHandler.parse('\x1b7'); + assert.equal(bufferService.buffer.savedX, 4); + assert.equal(bufferService.buffer.savedY, 1); + inputHandler.parse('\x1b[!p'); + assert.equal(bufferService.buffer.savedX, 0); + assert.equal(bufferService.buffer.savedY, 0); + }); + it('should reset DECOM', () => { + inputHandler.parse('\x1b[?6h'); + assert.equal(coreService.decPrivateModes.origin, true); + inputHandler.parse('\x1b[!p'); + assert.equal(coreService.decPrivateModes.origin, false); + }); + }); +}); diff --git a/src/InputHandler.ts b/src/common/InputHandler.ts similarity index 96% rename from src/InputHandler.ts rename to src/common/InputHandler.ts index 3717256e..1ec2a68d 100644 --- a/src/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -4,7 +4,7 @@ * @license MIT */ -import { IInputHandler, IInputHandlingTerminal } from './Types'; +import { IInputHandler, IAttributeData, IDisposable, IWindowOptions } from 'common/Types'; import { C0, C1 } from 'common/data/EscapeSequences'; import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets'; import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser'; @@ -17,11 +17,9 @@ 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, IWindowOptions } from 'common/Types'; -import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IInstantiationService } from 'common/services/Services'; +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'; -import { IRenderService } from 'browser/services/Services'; /** * Map collect to glevel. Used in `selectCharset`. @@ -94,7 +92,10 @@ function paramToWindowOption(n: number, opts: IWindowOptions): boolean { return false; } - +export enum WindowsOptionsReportType { + GET_WIN_SIZE_PIXELS = 0, + GET_CELL_SIZE_PIXELS = 1 +} /** * DCS subparser implementations @@ -218,27 +219,39 @@ export class InputHandler extends Disposable implements IInputHandler { private _workCell: CellData = new CellData(); private _windowTitle = ''; private _iconName = ''; - private _windowTitleStack: string[] = []; - private _iconNameStack: string[] = []; + protected _windowTitleStack: string[] = []; + protected _iconNameStack: string[] = []; private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone(); private _eraseAttrDataInternal: IAttributeData = DEFAULT_ATTR_DATA.clone(); + private _onRequestBell = new EventEmitter(); + public get onRequestBell(): IEvent { return this._onRequestBell.event; } private _onRequestRefreshRows = new EventEmitter(); public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } private _onRequestReset = new EventEmitter(); public get onRequestReset(): IEvent { return this._onRequestReset.event; } - private _onRequestBell = new EventEmitter(); - public get onRequestBell(): IEvent { return this._onRequestBell.event; } + private _onRequestScroll = new EventEmitter(); + public get onRequestScroll(): IEvent { return this._onRequestScroll.event; } + private _onRequestSyncScrollBar = new EventEmitter(); + public get onRequestSyncScrollBar(): IEvent { return this._onRequestSyncScrollBar.event; } + private _onRequestWindowsOptionsReport = new EventEmitter(); + public get onRequestWindowsOptionsReport(): IEvent { return this._onRequestWindowsOptionsReport.event; } + + private _onA11yChar = new EventEmitter(); + public get onA11yChar(): IEvent { return this._onA11yChar.event; } + private _onA11yTab = new EventEmitter(); + public get onA11yTab(): IEvent { return this._onA11yTab.event; } private _onCursorMove = new EventEmitter(); public get onCursorMove(): IEvent { return this._onCursorMove.event; } private _onLineFeed = new EventEmitter(); public get onLineFeed(): IEvent { return this._onLineFeed.event; } private _onScroll = new EventEmitter(); public get onScroll(): IEvent { return this._onScroll.event; } + private _onTitleChange = new EventEmitter(); + public get onTitleChange(): IEvent { return this._onTitleChange.event; } constructor( - private _terminal: IInputHandlingTerminal, private readonly _bufferService: IBufferService, private readonly _charsetService: ICharsetService, private readonly _coreService: ICoreService, @@ -247,9 +260,8 @@ 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()) - { + private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser() + ) { super(); this.register(this._parser); @@ -489,9 +501,9 @@ export class InputHandler extends Disposable implements IInputHandler { const screenReaderMode = this._optionsService.options.screenReaderMode; const cols = this._bufferService.cols; const wraparoundMode = this._coreService.decPrivateModes.wraparound; - const insertMode = this._terminal.insertMode; + const insertMode = this._coreService.modes.insertMode; const curAttr = this._curAttrData; - let bufferRow = buffer.lines.get(buffer.y + buffer.ybase); + let bufferRow = buffer.lines.get(buffer.ybase + buffer.y)!; this._dirtyRowService.markDirty(buffer.y); @@ -518,7 +530,7 @@ export class InputHandler extends Disposable implements IInputHandler { } if (screenReaderMode) { - this._terminal.onA11yCharEmitter.fire(stringFromCodePoint(code)); + this._onA11yChar.fire(stringFromCodePoint(code)); } // insert combining char at last cursor position @@ -548,17 +560,17 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.y++; if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; - this._terminal.scroll(this._eraseAttrData(), true); + this._onRequestScroll.fire(this._eraseAttrData(), true); } else { if (buffer.y >= this._bufferService.rows) { buffer.y = this._bufferService.rows - 1; } // The line already exists (eg. the initial viewport), mark it as a // wrapped line - buffer.lines.get(buffer.y).isWrapped = true; + buffer.lines.get(buffer.ybase + buffer.y)!.isWrapped = true; } // row changed, get it again - bufferRow = buffer.lines.get(buffer.y + buffer.ybase); + bufferRow = buffer.lines.get(buffer.ybase + buffer.y)!; } else { buffer.x = cols - 1; if (chWidth === 2) { @@ -687,7 +699,7 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.y++; if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; - this._terminal.scroll(this._eraseAttrData()); + this._onRequestScroll.fire(this._eraseAttrData()); } else if (buffer.y >= this._bufferService.rows) { buffer.y = this._bufferService.rows - 1; } @@ -736,7 +748,7 @@ export class InputHandler extends Disposable implements IInputHandler { const originalX = this._bufferService.buffer.x; this._bufferService.buffer.x = this._bufferService.buffer.nextStop(); if (this._optionsService.options.screenReaderMode) { - this._terminal.onA11yTabEmitter.fire(this._bufferService.buffer.x - originalX); + this._onA11yTab.fire(this._bufferService.buffer.x - originalX); } } @@ -1024,7 +1036,7 @@ export class InputHandler extends Disposable implements IInputHandler { * @param end end - 1 is last erased cell */ private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false): void { - const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + y); + const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + y)!; line.replaceCells( start, end, @@ -1042,7 +1054,7 @@ export class InputHandler extends Disposable implements IInputHandler { * @param y row index */ private _resetBufferLine(y: number): void { - const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + y); + const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + y)!; line.fill(this._bufferService.buffer.getNullCell(this._eraseAttrData())); line.isWrapped = false; } @@ -1091,7 +1103,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._eraseInBufferLine(j, 0, this._bufferService.buffer.x + 1, true); if (this._bufferService.buffer.x + 1 >= this._bufferService.cols) { // Deleted entire previous line. This next line can no longer be wrapped. - this._bufferService.buffer.lines.get(j + 1).isWrapped = false; + this._bufferService.buffer.lines.get(j + 1)!.isWrapped = false; } while (j--) { this._resetBufferLine(j); @@ -1178,7 +1190,7 @@ export class InputHandler extends Disposable implements IInputHandler { return; } - const row: number = buffer.y + buffer.ybase; + const row: number = buffer.ybase + buffer.y; const scrollBottomRowsOffset = this._bufferService.rows - 1 - buffer.scrollBottom; const scrollBottomAbsolute = this._bufferService.rows - 1 + buffer.ybase - scrollBottomRowsOffset + 1; @@ -1213,7 +1225,7 @@ export class InputHandler extends Disposable implements IInputHandler { return; } - const row: number = buffer.y + buffer.ybase; + const row: number = buffer.ybase + buffer.y; let j: number; j = this._bufferService.rows - 1 - buffer.scrollBottom; @@ -1242,7 +1254,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public insertChars(params: IParams): void { this._restrictCursor(); - const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.y + this._bufferService.buffer.ybase); + const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + this._bufferService.buffer.y); if (line) { line.insertCells( this._bufferService.buffer.x, @@ -1267,7 +1279,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public deleteChars(params: IParams): void { this._restrictCursor(); - const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.y + this._bufferService.buffer.ybase); + const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + this._bufferService.buffer.y); if (line) { line.deleteCells( this._bufferService.buffer.x, @@ -1343,7 +1355,7 @@ export class InputHandler extends Disposable implements IInputHandler { } const param = params.params[0] || 1; for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { - const line = buffer.lines.get(buffer.ybase + y); + const line = buffer.lines.get(buffer.ybase + y)!; line.deleteCells(0, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } @@ -1376,7 +1388,7 @@ export class InputHandler extends Disposable implements IInputHandler { } const param = params.params[0] || 1; for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { - const line = buffer.lines.get(buffer.ybase + y); + const line = buffer.lines.get(buffer.ybase + y)!; line.insertCells(0, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } @@ -1399,7 +1411,7 @@ export class InputHandler extends Disposable implements IInputHandler { } const param = params.params[0] || 1; for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { - const line = this._bufferService.buffer.lines.get(buffer.ybase + y); + const line = this._bufferService.buffer.lines.get(buffer.ybase + y)!; line.insertCells(buffer.x, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } @@ -1422,7 +1434,7 @@ export class InputHandler extends Disposable implements IInputHandler { } const param = params.params[0] || 1; for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { - const line = buffer.lines.get(buffer.ybase + y); + const line = buffer.lines.get(buffer.ybase + y)!; line.deleteCells(buffer.x, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } @@ -1439,7 +1451,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public eraseChars(params: IParams): void { this._restrictCursor(); - const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.y + this._bufferService.buffer.ybase); + const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + this._bufferService.buffer.y); if (line) { line.replaceCells( this._bufferService.buffer.x, @@ -1520,9 +1532,9 @@ export class InputHandler extends Disposable implements IInputHandler { if (params.params[0] > 0) { return; } - if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) { + if (this._is('xterm') || this._is('rxvt-unicode') || this._is('screen')) { this._coreService.triggerDataEvent(C0.ESC + '[?1;2c'); - } else if (this._terminal.is('linux')) { + } else if (this._is('linux')) { this._coreService.triggerDataEvent(C0.ESC + '[?6c'); } } @@ -1558,19 +1570,27 @@ export class InputHandler extends Disposable implements IInputHandler { // xterm and urxvt // seem to spit this // out around ~370 times (?). - if (this._terminal.is('xterm')) { + if (this._is('xterm')) { this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c'); - } else if (this._terminal.is('rxvt-unicode')) { + } else if (this._is('rxvt-unicode')) { this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c'); - } else if (this._terminal.is('linux')) { + } else if (this._is('linux')) { // not supported by linux console. // linux console echoes parameters. this._coreService.triggerDataEvent(params.params[0] + 'c'); - } else if (this._terminal.is('screen')) { + } else if (this._is('screen')) { this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c'); } } + /** + * Evaluate if the current terminal is the given argument. + * @param term The terminal name to evaluate + */ + private _is(term: string): boolean { + return (this._optionsService.options.termName + '').indexOf(term) === 0; + } + /** * CSI Pm h Set Mode (SM). * Ps = 2 -> Keyboard Action Mode (AM). @@ -1587,15 +1607,12 @@ export class InputHandler extends Disposable implements IInputHandler { * | 4 | Insert Mode (IRM). | #Y | * | 12 | Send/receive (SRM). Always off. | #N | * | 20 | Automatic Newline (LNM). Always off. | #N | - * - * - * FIXME: why is LNM commented out? */ public setMode(params: IParams): void { for (let i = 0; i < params.length; i++) { switch (params.params[i]) { case 4: - this._terminal.insertMode = true; + this._coreService.modes.insertMode = true; break; case 20: // this._t.convertEol = true; @@ -1736,7 +1753,7 @@ export class InputHandler extends Disposable implements IInputHandler { * through `options.windowsOptions`. */ if (this._optionsService.options.windowOptions.setWinLines) { - this._terminal.resize(132, this._bufferService.rows); + this._bufferService.resize(132, this._bufferService.rows); this._onRequestReset.fire(); } break; @@ -1753,7 +1770,7 @@ export class InputHandler extends Disposable implements IInputHandler { case 66: this._logService.debug('Serial port requested application keypad.'); this._coreService.decPrivateModes.applicationKeypad = true; - this._terminal.viewport?.syncScrollArea(); + this._onRequestSyncScrollBar.fire(); break; case 9: // X10 Mouse // no release, no motion, no wheel, no modifiers. @@ -1774,7 +1791,7 @@ export class InputHandler extends Disposable implements IInputHandler { case 1004: // send focusin/focusout events // focusin: ^[[I // focusout: ^[[O - this._terminal.sendFocus = true; + this._coreService.decPrivateModes.sendFocus = true; break; case 1005: // utf8 ext mode mouse - removed in #2507 this._logService.debug('DECSET 1005 not supported (see #2507)'); @@ -1797,12 +1814,12 @@ export class InputHandler extends Disposable implements IInputHandler { case 47: // alt screen buffer case 1047: // alt screen buffer this._bufferService.buffers.activateAltBuffer(this._eraseAttrData()); + this._coreService.isCursorInitialized = true; this._onRequestRefreshRows.fire(0, this._bufferService.rows - 1); - this._terminal.viewport?.syncScrollArea(); - this._terminal.showCursor(); + this._onRequestSyncScrollBar.fire(); break; case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) - this._terminal.bracketedPasteMode = true; + this._coreService.decPrivateModes.bracketedPasteMode = true; break; } } @@ -1833,7 +1850,7 @@ export class InputHandler extends Disposable implements IInputHandler { for (let i = 0; i < params.length; i++) { switch (params.params[i]) { case 4: - this._terminal.insertMode = false; + this._coreService.modes.insertMode = false; break; case 20: // this._t.convertEol = false; @@ -1963,7 +1980,7 @@ export class InputHandler extends Disposable implements IInputHandler { * through `options.windowsOptions`. */ if (this._optionsService.options.windowOptions.setWinLines) { - this._terminal.resize(80, this._bufferService.rows); + this._bufferService.resize(80, this._bufferService.rows); this._onRequestReset.fire(); } break; @@ -1980,7 +1997,7 @@ export class InputHandler extends Disposable implements IInputHandler { case 66: this._logService.debug('Switching back to normal keypad.'); this._coreService.decPrivateModes.applicationKeypad = false; - this._terminal.viewport?.syncScrollArea(); + this._onRequestSyncScrollBar.fire(); break; case 9: // X10 Mouse case 1000: // vt200 mouse @@ -1989,7 +2006,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._coreMouseService.activeProtocol = 'NONE'; break; case 1004: // send focusin/focusout events - this._terminal.sendFocus = false; + this._coreService.decPrivateModes.sendFocus = false; break; case 1005: // utf8 ext mode mouse - removed in #2507 this._logService.debug('DECRST 1005 not supported (see #2507)'); @@ -2015,12 +2032,12 @@ export class InputHandler extends Disposable implements IInputHandler { if (params.params[i] === 1049) { this.restoreCursor(); } + this._coreService.isCursorInitialized = true; this._onRequestRefreshRows.fire(0, this._bufferService.rows - 1); - this._terminal.viewport?.syncScrollArea(); - this._terminal.showCursor(); + this._onRequestSyncScrollBar.fire(); break; case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) - this._terminal.bracketedPasteMode = false; + this._coreService.decPrivateModes.bracketedPasteMode = false; break; } } @@ -2046,7 +2063,7 @@ export class InputHandler extends Disposable implements IInputHandler { do { accu[advance + cSpace] = params.params[pos + advance]; if (params.hasSubParams(pos + advance)) { - const subparams = params.getSubParams(pos + advance); + const subparams = params.getSubParams(pos + advance)!; let i = 0; do { if (accu[1] === 5) { @@ -2356,8 +2373,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public softReset(params: IParams): void { this._coreService.isCursorHidden = false; - this._terminal.insertMode = false; - this._terminal.viewport?.syncScrollArea(); + this._onRequestSyncScrollBar.fire(); this._bufferService.buffer.scrollTop = 0; this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; this._curAttrData = DEFAULT_ATTR_DATA.clone(); @@ -2471,22 +2487,14 @@ export class InputHandler extends Disposable implements IInputHandler { 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`); + if (second !== 2) { + this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_WIN_SIZE_PIXELS); } 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`); - } + this._onRequestWindowsOptionsReport.fire(WindowsOptionsReportType.GET_CELL_SIZE_PIXELS); break; case 18: // GetWinSizeChars, returns CSI 8 ; height ; width t if (this._bufferService) { @@ -2510,12 +2518,12 @@ export class InputHandler extends Disposable implements IInputHandler { case 23: // PopTitle if (second === 0 || second === 2) { if (this._windowTitleStack.length) { - this.setTitle(this._windowTitleStack.pop()); + this.setTitle(this._windowTitleStack.pop()!); } } if (second === 0 || second === 1) { if (this._iconNameStack.length) { - this.setIconName(this._iconNameStack.pop()); + this.setIconName(this._iconNameStack.pop()!); } } break; @@ -2573,7 +2581,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public setTitle(data: string): void { this._windowTitle = data; - this._terminal.handleTitle(data); + this._onTitleChange.fire(data); } /** @@ -2606,7 +2614,7 @@ export class InputHandler extends Disposable implements IInputHandler { public keypadApplicationMode(): void { this._logService.debug('Serial port requested application keypad.'); this._coreService.decPrivateModes.applicationKeypad = true; - this._terminal.viewport?.syncScrollArea(); + this._onRequestSyncScrollBar.fire(); } /** @@ -2617,7 +2625,7 @@ export class InputHandler extends Disposable implements IInputHandler { public keypadNumericMode(): void { this._logService.debug('Switching back to normal keypad.'); this._coreService.decPrivateModes.applicationKeypad = false; - this._terminal.viewport?.syncScrollArea(); + this._onRequestSyncScrollBar.fire(); } /** @@ -2674,7 +2682,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._bufferService.buffer.y++; if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; - this._terminal.scroll(this._eraseAttrData()); + this._onRequestScroll.fire(this._eraseAttrData()); } else if (buffer.y >= this._bufferService.rows) { buffer.y = this._bufferService.rows - 1; } @@ -2712,8 +2720,8 @@ export class InputHandler extends Disposable implements IInputHandler { // test: echo -ne '\e[1;1H\e[44m\eM\e[0m' // blankLine(true) is xterm/linux behavior const scrollRegionHeight = buffer.scrollBottom - buffer.scrollTop; - buffer.lines.shiftElements(buffer.y + buffer.ybase, scrollRegionHeight, 1); - buffer.lines.set(buffer.y + buffer.ybase, buffer.getBlankLine(this._eraseAttrData())); + buffer.lines.shiftElements(buffer.ybase + buffer.y, scrollRegionHeight, 1); + buffer.lines.set(buffer.ybase + buffer.y, buffer.getBlankLine(this._eraseAttrData())); this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); } else { buffer.y--; @@ -2778,9 +2786,12 @@ export class InputHandler extends Disposable implements IInputHandler { this._setCursor(0, 0); for (let yOffset = 0; yOffset < this._bufferService.rows; ++yOffset) { - const row = buffer.y + buffer.ybase + yOffset; - buffer.lines.get(row).fill(cell); - buffer.lines.get(row).isWrapped = false; + const row = buffer.ybase + buffer.y + yOffset; + const line = buffer.lines.get(row); + if (line) { + line.fill(cell); + line.isWrapped = false; + } } this._dirtyRowService.markAllDirty(); this._setCursor(0, 0); diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index cfa1abd7..71faed99 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -9,13 +9,15 @@ import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { BufferSet } from 'common/buffer/BufferSet'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes } from 'common/Types'; import { UnicodeV6 } from 'common/input/UnicodeV6'; export class MockBufferService implements IBufferService { public serviceBrand: any; public get buffer(): IBuffer { return this.buffers.active; } public buffers: IBufferSet = {} as any; + public onResize: IEvent<{ cols: number, rows: number }> = new EventEmitter<{ cols: number, rows: number }>().event; + public isUserScrolling: boolean = false; constructor( public cols: number, public rows: number, @@ -31,6 +33,7 @@ export class MockBufferService implements IBufferService { } export class MockCoreMouseService implements ICoreMouseService { + public areMouseEventsActive: boolean = false; public activeEncoding: string = ''; public activeProtocol: string = ''; public addEncoding(name: string): void {} @@ -47,7 +50,6 @@ export class MockCharsetService implements ICharsetService { public serviceBrand: any; public charset: ICharset | undefined; public glevel: number = 0; - public charsets: ReadonlyArray = []; public reset(): void {} public setgLevel(g: number): void {} public setgCharset(g: number, charset: ICharset): void {} @@ -58,10 +60,15 @@ export class MockCoreService implements ICoreService { public isCursorInitialized: boolean = false; public isCursorHidden: boolean = false; public isFocused: boolean = false; + public modes: IModes = { + insertMode: false + }; public decPrivateModes: IDecPrivateModes = { applicationCursorKeys: false, applicationKeypad: false, + bracketedPasteMode: false, origin: false, + sendFocus: false, wraparound: true }; public onData: IEvent = new EventEmitter().event; diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index b250b45e..4fcb627b 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -3,13 +3,29 @@ * @license MIT */ +import { ITerminalOptions as IPublicTerminalOptions } from 'xterm'; import { IEvent, IEventEmitter } from 'common/EventEmitter'; import { IDeleteEvent, IInsertEvent } from 'common/CircularList'; +import { IParams } from 'common/parser/Types'; +import { IOptionsService, IUnicodeService } from 'common/services/Services'; + +export interface ICoreTerminal { + optionsService: IOptionsService; + unicodeService: IUnicodeService; +} export interface IDisposable { dispose(): void; } +// TODO: The options that are not in the public API should be reviewed +export interface ITerminalOptions extends IPublicTerminalOptions { + [key: string]: any; + cancelEvents?: boolean; + convertEol?: boolean; + termName?: string; +} + export type XtermListener = (...args: any[]) => void; /** @@ -41,7 +57,7 @@ export interface ICircularList { get(index: number): T | undefined; set(index: number, value: T): void; push(value: T): void; - recycle(): T | undefined; + recycle(): T; pop(): T | undefined; splice(start: number, deleteCount: number, ...items: T[]): void; trimStart(count: number): void; @@ -150,11 +166,16 @@ export interface IMarker extends IDisposable { readonly isDisposed: boolean; readonly line: number; } +export interface IModes { + insertMode: boolean; +} export interface IDecPrivateModes { applicationCursorKeys: boolean; applicationKeypad: boolean; + bracketedPasteMode: boolean; origin: boolean; + sendFocus: boolean; wraparound: boolean; // defaults: xterm - true, vt100 - false } @@ -286,3 +307,88 @@ export interface IWindowOptions { popTitle?: boolean; setWinLines?: boolean; } + +/** + * Calls the parser and handles actions generated by the parser. + */ +export interface IInputHandler { + onTitleChange: IEvent; + onRequestScroll: IEvent; + + parse(data: string | Uint8Array): void; + print(data: Uint32Array, start: number, end: number): void; + + /** C0 BEL */ bell(): void; + /** C0 LF */ lineFeed(): void; + /** C0 CR */ carriageReturn(): void; + /** C0 BS */ backspace(): void; + /** C0 HT */ tab(): void; + /** C0 SO */ shiftOut(): void; + /** C0 SI */ shiftIn(): void; + + /** CSI @ */ insertChars(params: IParams): void; + /** CSI SP @ */ scrollLeft(params: IParams): void; + /** CSI A */ cursorUp(params: IParams): void; + /** CSI SP A */ scrollRight(params: IParams): void; + /** CSI B */ cursorDown(params: IParams): void; + /** CSI C */ cursorForward(params: IParams): void; + /** CSI D */ cursorBackward(params: IParams): void; + /** CSI E */ cursorNextLine(params: IParams): void; + /** CSI F */ cursorPrecedingLine(params: IParams): void; + /** CSI G */ cursorCharAbsolute(params: IParams): void; + /** CSI H */ cursorPosition(params: IParams): void; + /** CSI I */ cursorForwardTab(params: IParams): void; + /** CSI J */ eraseInDisplay(params: IParams): void; + /** CSI K */ eraseInLine(params: IParams): void; + /** CSI L */ insertLines(params: IParams): void; + /** CSI M */ deleteLines(params: IParams): void; + /** CSI P */ deleteChars(params: IParams): void; + /** CSI S */ scrollUp(params: IParams): void; + /** CSI T */ scrollDown(params: IParams, collect?: string): void; + /** CSI X */ eraseChars(params: IParams): void; + /** CSI Z */ cursorBackwardTab(params: IParams): void; + /** CSI ` */ charPosAbsolute(params: IParams): void; + /** CSI a */ hPositionRelative(params: IParams): void; + /** CSI b */ repeatPrecedingCharacter(params: IParams): void; + /** CSI c */ sendDeviceAttributesPrimary(params: IParams): void; + /** CSI > c */ sendDeviceAttributesSecondary(params: IParams): void; + /** CSI d */ linePosAbsolute(params: IParams): void; + /** CSI e */ vPositionRelative(params: IParams): void; + /** CSI f */ hVPosition(params: IParams): void; + /** CSI g */ tabClear(params: IParams): void; + /** CSI h */ setMode(params: IParams, collect?: string): void; + /** CSI l */ resetMode(params: IParams, collect?: string): void; + /** CSI m */ charAttributes(params: IParams): void; + /** CSI n */ deviceStatus(params: IParams, collect?: string): void; + /** CSI p */ softReset(params: IParams, collect?: string): void; + /** CSI q */ setCursorStyle(params: IParams, collect?: string): void; + /** CSI r */ setScrollRegion(params: IParams, collect?: string): void; + /** CSI s */ saveCursor(params: IParams): void; + /** CSI u */ restoreCursor(params: IParams): void; + /** CSI ' } */ insertColumns(params: IParams): void; + /** CSI ' ~ */ deleteColumns(params: IParams): void; + /** OSC 0 + OSC 2 */ setTitle(data: string): void; + /** ESC E */ nextLine(): void; + /** ESC = */ keypadApplicationMode(): void; + /** ESC > */ keypadNumericMode(): void; + /** ESC % G + ESC % @ */ selectDefaultCharset(): void; + /** ESC ( C + ESC ) C + ESC * C + ESC + C + ESC - C + ESC . C + ESC / C */ selectCharset(collectAndFlag: string): void; + /** ESC D */ index(): void; + /** ESC H */ tabSet(): void; + /** ESC M */ reverseIndex(): void; + /** ESC c */ fullReset(): void; + /** ESC n + ESC o + ESC | + ESC } + ESC ~ */ setgLevel(level: number): void; + /** ESC # 8 */ screenAlignmentPattern(): void; +} diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts index 152bab7a..1de39dbd 100644 --- a/src/common/buffer/Buffer.ts +++ b/src/common/buffer/Buffer.ts @@ -36,7 +36,7 @@ export class Buffer implements IBuffer { public savedY: number = 0; public savedX: number = 0; public savedCurAttrData = DEFAULT_ATTR_DATA.clone(); - public savedCharset: ICharset | null = DEFAULT_CHARSET; + public savedCharset: ICharset | undefined = DEFAULT_CHARSET; public markers: Marker[] = []; private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]); private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]); diff --git a/src/common/buffer/Types.d.ts b/src/common/buffer/Types.d.ts index e229d69c..c271f33c 100644 --- a/src/common/buffer/Types.d.ts +++ b/src/common/buffer/Types.d.ts @@ -31,7 +31,7 @@ export interface IBuffer { hasScrollback: boolean; savedY: number; savedX: number; - savedCharset: ICharset | null; + savedCharset: ICharset | undefined; savedCurAttrData: IAttributeData; isCursorInViewport: boolean; markers: IMarker[]; diff --git a/src/common/data/Charsets.ts b/src/common/data/Charsets.ts index 01087d8b..c72d5a23 100644 --- a/src/common/data/Charsets.ts +++ b/src/common/data/Charsets.ts @@ -10,12 +10,12 @@ import { ICharset } from 'common/Types'; * to be represented within the terminal with only 8-bit encoding. See ISO 2022 * for a discussion on character sets. Only VT100 character sets are supported. */ -export const CHARSETS: { [key: string]: ICharset | null } = {}; +export const CHARSETS: { [key: string]: ICharset | undefined } = {}; /** * The default character set, US. */ -export const DEFAULT_CHARSET: ICharset | null = CHARSETS['B']; +export const DEFAULT_CHARSET: ICharset | undefined = CHARSETS['B']; /** * DEC Special Character and Line Drawing Set. @@ -74,7 +74,7 @@ CHARSETS['A'] = { * United States character set * ESC (B */ -CHARSETS['B'] = null; +CHARSETS['B'] = undefined; /** * Dutch character set diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 1b6da792..8de44e43 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -6,6 +6,7 @@ import { IBufferService, IOptionsService } from 'common/services/Services'; import { BufferSet } from 'common/buffer/BufferSet'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; +import { EventEmitter, IEvent } from 'common/EventEmitter'; export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars export const MINIMUM_ROWS = 1; @@ -16,6 +17,11 @@ export class BufferService implements IBufferService { public cols: number; public rows: number; public buffers: IBufferSet; + /** Whether the user is scrolling (locks the scroll position) */ + public isUserScrolling: boolean = false; + + private _onResize = new EventEmitter<{ cols: number, rows: number }>(); + public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } public get buffer(): IBuffer { return this.buffers.active; } @@ -30,9 +36,13 @@ export class BufferService implements IBufferService { public resize(cols: number, rows: number): void { this.cols = cols; this.rows = rows; + this.buffers.resize(cols, rows); + this.buffers.setupTabStops(this.cols); + this._onResize.fire({ cols, rows }); } public reset(): void { this.buffers = new BufferSet(this._optionsService, this); + this.isUserScrolling = false; } } diff --git a/src/common/services/CharsetService.ts b/src/common/services/CharsetService.ts index 47a015f6..c5381065 100644 --- a/src/common/services/CharsetService.ts +++ b/src/common/services/CharsetService.ts @@ -10,22 +10,23 @@ export class CharsetService implements ICharsetService { public serviceBrand: any; public charset: ICharset | undefined; - public charsets: ICharset[] = []; public glevel: number = 0; + private _charsets: (ICharset | undefined)[] = []; + public reset(): void { this.charset = undefined; - this.charsets = []; + this._charsets = []; this.glevel = 0; } public setgLevel(g: number): void { this.glevel = g; - this.charset = this.charsets[g]; + this.charset = this._charsets[g]; } - public setgCharset(g: number, charset: ICharset): void { - this.charsets[g] = charset; + public setgCharset(g: number, charset: ICharset | undefined): void { + this._charsets[g] = charset; if (this.glevel === g) { this.charset = charset; } diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts index c0ed3c6a..29a84761 100644 --- a/src/common/services/CoreMouseService.ts +++ b/src/common/services/CoreMouseService.ts @@ -192,6 +192,10 @@ export class CoreMouseService implements ICoreMouseService { return this._activeProtocol; } + public get areMouseEventsActive(): boolean { + return this._protocols[this._activeProtocol].events !== 0; + } + public set activeProtocol(name: string) { if (!this._protocols[name]) { throw new Error(`unknown protocol "${name}"`); diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts index 2f0ff72f..55d425aa 100644 --- a/src/common/services/CoreService.ts +++ b/src/common/services/CoreService.ts @@ -5,13 +5,19 @@ import { ICoreService, ILogService, IOptionsService, IBufferService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { IDecPrivateModes } from 'common/Types'; +import { IDecPrivateModes, IModes } from 'common/Types'; import { clone } from 'common/Clone'; +const DEFAULT_MODES: IModes = Object.freeze({ + insertMode: false +}); + const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({ applicationCursorKeys: false, applicationKeypad: false, + bracketedPasteMode: false, origin: false, + sendFocus: false, wraparound: true // defaults: xterm - true, vt100 - false }); @@ -20,6 +26,7 @@ export class CoreService implements ICoreService { public isCursorInitialized: boolean = false; public isCursorHidden: boolean = false; + public modes: IModes; public decPrivateModes: IDecPrivateModes; private _onData = new EventEmitter(); @@ -36,10 +43,12 @@ export class CoreService implements ICoreService { @ILogService private readonly _logService: ILogService, @IOptionsService private readonly _optionsService: IOptionsService ) { + this.modes = clone(DEFAULT_MODES); this.decPrivateModes = clone(DEFAULT_DEC_PRIVATE_MODES); } public reset(): void { + this.modes = clone(DEFAULT_MODES); this.decPrivateModes = clone(DEFAULT_DEC_PRIVATE_MODES); } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 8a3e9b28..e123bd8b 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,19 +5,20 @@ import { IEvent } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; export const IBufferService = createDecorator('BufferService'); export interface IBufferService { - serviceBrand: any; + serviceBrand: undefined; readonly cols: number; readonly rows: number; readonly buffer: IBuffer; readonly buffers: IBufferSet; + isUserScrolling: boolean; - // TODO: Move resize event here + onResize: IEvent<{ cols: number, rows: number }>; resize(cols: number, rows: number): void; reset(): void; @@ -27,6 +28,7 @@ export const ICoreMouseService = createDecorator('CoreMouseSe export interface ICoreMouseService { activeProtocol: string; activeEncoding: string; + areMouseEventsActive: boolean; addProtocol(name: string, protocol: ICoreMouseProtocol): void; addEncoding(name: string, encoding: CoreMouseEncoding): void; reset(): void; @@ -51,12 +53,12 @@ export interface ICoreMouseService { /** * Human readable version of mouse events. */ - explainEvents(events: CoreMouseEventType): {[event: string]: boolean}; + explainEvents(events: CoreMouseEventType): { [event: string]: boolean }; } export const ICoreService = createDecorator('CoreService'); export interface ICoreService { - serviceBrand: any; + serviceBrand: undefined; /** * Initially the cursor will not be visible until the first time the terminal @@ -65,6 +67,7 @@ export interface ICoreService { isCursorInitialized: boolean; isCursorHidden: boolean; + readonly modes: IModes; readonly decPrivateModes: IDecPrivateModes; readonly onData: IEvent; @@ -92,11 +95,10 @@ export interface ICoreService { export const ICharsetService = createDecorator('CharsetService'); export interface ICharsetService { - serviceBrand: any; + serviceBrand: undefined; charset: ICharset | undefined; readonly glevel: number; - readonly charsets: ReadonlyArray; reset(): void; @@ -111,12 +113,12 @@ export interface ICharsetService { * @param g * @param charset */ - setgCharset(g: number, charset: ICharset): void; + setgCharset(g: number, charset: ICharset | undefined): void; } export const IDirtyRowService = createDecorator('DirtyRowService'); export interface IDirtyRowService { - serviceBrand: any; + serviceBrand: undefined; readonly start: number; readonly end: number; @@ -132,61 +134,32 @@ export interface IServiceIdentifier { type: T; } -export interface IConstructorSignature0 { - new(...services: { serviceBrand: any }[]): T; -} +export interface IBrandedService { + serviceBrand: undefined; +}; -export interface IConstructorSignature1 { - new(first: A1, ...services: { serviceBrand: any }[]): T; -} - -export interface IConstructorSignature2 { - new(first: A1, second: A2, ...services: { serviceBrand: any }[]): T; -} - -export interface IConstructorSignature3 { - new(first: A1, second: A2, third: A3, ...services: { serviceBrand: any }[]): T; -} - -export interface IConstructorSignature4 { - new(first: A1, second: A2, third: A3, fourth: A4, ...services: { serviceBrand: any }[]): T; -} - -export interface IConstructorSignature5 { - new(first: A1, second: A2, third: A3, fourth: A4, fifth: A5, ...services: { serviceBrand: any }[]): T; -} - -export interface IConstructorSignature6 { - new(first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, ...services: { serviceBrand: any }[]): T; -} - -export interface IConstructorSignature7 { - new(first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, seventh: A7, ...services: { serviceBrand: any }[]): T; -} - -export interface IConstructorSignature8 { - new(first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, seventh: A7, eigth: A8, ...services: { serviceBrand: any }[]): T; -} +type GetLeadingNonServiceArgs = + Args extends [...IBrandedService[]] ? [] + : Args extends [infer A1, ...IBrandedService[]] ? [A1] + : Args extends [infer A1, infer A2, ...IBrandedService[]] ? [A1, A2] + : Args extends [infer A1, infer A2, infer A3, ...IBrandedService[]] ? [A1, A2, A3] + : Args extends [infer A1, infer A2, infer A3, infer A4, ...IBrandedService[]] ? [A1, A2, A3, A4] + : Args extends [infer A1, infer A2, infer A3, infer A4, infer A5, ...IBrandedService[]] ? [A1, A2, A3, A4, A5] + : Args extends [infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, ...IBrandedService[]] ? [A1, A2, A3, A4, A5, A6] + : Args extends [infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7, ...IBrandedService[]] ? [A1, A2, A3, A4, A5, A6, A7] + : Args extends [infer A1, infer A2, infer A3, infer A4, infer A5, infer A6, infer A7, infer A8, ...IBrandedService[]] ? [A1, A2, A3, A4, A5, A6, A7, A8] + : never; 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; - createInstance(ctor: IConstructorSignature2, first: A1, second: A2): T; - createInstance(ctor: IConstructorSignature3, first: A1, second: A2, third: A3): T; - createInstance(ctor: IConstructorSignature4, first: A1, second: A2, third: A3, fourth: A4): T; - createInstance(ctor: IConstructorSignature5, first: A1, second: A2, third: A3, fourth: A4, fifth: A5): T; - createInstance(ctor: IConstructorSignature6, first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6): T; - createInstance(ctor: IConstructorSignature7, first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, seventh: A7): T; - createInstance(ctor: IConstructorSignature8, first: A1, second: A2, third: A3, fourth: A4, fifth: A5, sixth: A6, seventh: A7, eigth: A8): T; + createInstance any, R extends InstanceType>(t: Ctor, ...args: GetLeadingNonServiceArgs>): R; } export const ILogService = createDecorator('LogService'); export interface ILogService { - serviceBrand: any; + serviceBrand: undefined; debug(message: any, ...optionalParams: any[]): void; info(message: any, ...optionalParams: any[]): void; @@ -196,7 +169,7 @@ export interface ILogService { export const IOptionsService = createDecorator('OptionsService'); export interface IOptionsService { - serviceBrand: any; + serviceBrand: undefined; readonly options: ITerminalOptions; @@ -311,7 +284,7 @@ export interface ITheme { export const IUnicodeService = createDecorator('UnicodeService'); export interface IUnicodeService { - serviceBrand: any; + serviceBrand: undefined; /** Register an Unicode version provider. */ register(provider: IUnicodeVersionProvider): void; /** Registered Unicode versions. */ diff --git a/src/common/tsconfig.json b/src/common/tsconfig.json index 59050a0b..7f35c80c 100644 --- a/src/common/tsconfig.json +++ b/src/common/tsconfig.json @@ -10,5 +10,8 @@ ], "baseUrl": ".." }, - "include": [ "./**/*" ] + "include": [ + "./**/*", + "../../typings/xterm.d.ts" + ] } diff --git a/src/tsconfig-base.json b/src/tsconfig-base.json index 84d0c924..0cd951a7 100644 --- a/src/tsconfig-base.json +++ b/src/tsconfig-base.json @@ -8,6 +8,7 @@ "removeComments": true, "pretty": true, - "incremental": true + "incremental": true, + "experimentalDecorators": true } } diff --git a/src/tsconfig.json b/src/tsconfig.json deleted file mode 100644 index 97576668..00000000 --- a/src/tsconfig.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "extends": "./tsconfig-base", - "compilerOptions": { - "module": "commonjs", - "lib": [ - "dom", - "es5", - "es6", - "scripthost", - "es2015.promise" - ], - "rootDir": ".", - "outDir": "../out", - "baseUrl": ".", - "paths": { - "common/*": [ "./common/*" ], - "browser/*": [ "./browser/*" ] - }, - - "noUnusedLocals": true, - "noImplicitAny": true - }, - "include": [ - "./**/*", - "../typings/xterm.d.ts" - ], - "exclude": [ - "./addons/**/*" - ], - "references": [ - { "path": "./common" }, - { "path": "./browser" } - ] -} diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index 6829fd0a..75e00152 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -539,7 +539,7 @@ describe('API Integration Tests', function(): void { assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).translateToString()`), 'norm '); assert.equal(await page.evaluate(`window.term.buffer.normal.getLine(0).translateToString()`), 'norm '); assert.equal(await page.evaluate(`window.term.buffer.alternate.getLine(0)`), undefined); - }) + }); }); it('dispose', async () => { diff --git a/test/benchmark/EscapeSequenceParser.benchmark.ts b/test/benchmark/EscapeSequenceParser.benchmark.ts index 68247715..e4591dde 100644 --- a/test/benchmark/EscapeSequenceParser.benchmark.ts +++ b/test/benchmark/EscapeSequenceParser.benchmark.ts @@ -19,9 +19,9 @@ function toUtf32(s: string): Uint32Array { } class DcsHandler implements IDcsHandler { - hook(params: IParams): void {} - put(data: Uint32Array, start: number, end: number): void {} - unhook(): void {} + public hook(params: IParams): void {} + public put(data: Uint32Array, start: number, end: number): void {} + public unhook(): void {} } diff --git a/test/benchmark/Terminal.benchmark.ts b/test/benchmark/Terminal.benchmark.ts index 71a36cc7..76139cb6 100644 --- a/test/benchmark/Terminal.benchmark.ts +++ b/test/benchmark/Terminal.benchmark.ts @@ -7,7 +7,7 @@ import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark'; import { spawn } from 'node-pty'; import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder'; -import { Terminal } from 'Terminal'; +import { Terminal } from 'browser/Terminal'; perfContext('Terminal: ls -lR /usr/lib', () => { let content = ''; diff --git a/tsconfig.all.json b/tsconfig.all.json index 8143ea91..a8febb72 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -2,7 +2,7 @@ "files": [], "include": [], "references": [ - { "path": "./src" }, + { "path": "./src/browser" }, { "path": "./test/api" }, { "path": "./test/benchmark" }, { "path": "./addons/xterm-addon-attach/src" }, diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 3487c410..dfda8583 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -799,13 +799,14 @@ declare module 'xterm' { * (EXPERIMENTAL) Adds a marker to the normal buffer and returns it. If the * alt buffer is active, undefined is returned. * @param cursorYOffset The y position offset of the marker from the cursor. + * @returns The new marker or undefined. */ - registerMarker(cursorYOffset: number): IMarker; + registerMarker(cursorYOffset: number): IMarker | undefined; /** * @deprecated use `registerMarker` instead. */ - addMarker(cursorYOffset: number): IMarker; + addMarker(cursorYOffset: number): IMarker | undefined; /** * Gets whether the terminal has an active selection. diff --git a/webpack.config.js b/webpack.config.js index a9a60241..4f9087b5 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,7 +12,7 @@ const path = require('path'); * output by tsc (because of `baseUrl` and `paths` in `tsconfig.json`. */ module.exports = { - entry: './out/public/Terminal.js', + entry: './out/browser/public/Terminal.js', devtool: 'source-map', module: { rules: [