diff --git a/.mocha.env b/.mocha.env new file mode 100644 index 00000000..78a9f930 --- /dev/null +++ b/.mocha.env @@ -0,0 +1 @@ +NODE_PATH=./out diff --git a/.mocharc.yml b/.mocharc.yml new file mode 100644 index 00000000..c19fe15a --- /dev/null +++ b/.mocharc.yml @@ -0,0 +1,11 @@ +require: + - source-map-support/register +spec: + - out/**/*.test.js + - addons/**/out/*.test.js +watch-files: + - out/**/*.js + - addons/**/out/*.js +reporter: spec +color: true +check-leaks: true diff --git a/.vscode/settings.json b/.vscode/settings.json index f07ca545..9fa94d5f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "typescript.preferences.importModuleSpecifier": "non-relative", - "typescript.preferences.quoteStyle": "single" + "typescript.preferences.quoteStyle": "single", + "mochaExplorer.envPath": ".mocha.env" } diff --git a/README.md b/README.md index 720e703c..e6d8f9a9 100644 --- a/README.md +++ b/README.md @@ -183,6 +183,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**HashiCorp Nomad**](https://www.nomadproject.io/): A container orchestrator with the ability to connect to remote tasks via a web interface using websockets and xterm.js. - [**TermPair**](https://github.com/cs01/termpair): View and control terminals from your browser with end-to-end encryption - [**gdbgui**](https://github.com/cs01/gdbgui): Browser-based frontend to gdb (gnu debugger) +- [**goormIDE**](https://ide.goorm.io/): Run almost every programming languages with real-time collaboration, live pair programming, and built-in messenger. - [And much more...](https://github.com/xtermjs/xterm.js/network/dependents) Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only. diff --git a/addons/xterm-addon-attach/test/AttachAddon.api.ts b/addons/xterm-addon-attach/test/AttachAddon.api.ts index ef26cfd2..8335cf0f 100644 --- a/addons/xterm-addon-attach/test/AttachAddon.api.ts +++ b/addons/xterm-addon-attach/test/AttachAddon.api.ts @@ -4,7 +4,7 @@ */ import WebSocket = require('ws'); -import { openTerminal, pollFor, getBrowserType } from '../../../out-test/api/TestUtils'; +import { openTerminal, pollFor, launchBrowser } from '../../../out-test/api/TestUtils'; import { Browser, Page } from 'playwright'; const APP = 'http://127.0.0.1:3001/test'; @@ -16,10 +16,7 @@ const height = 600; describe('AttachAddon', () => { before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.includes('--headless') - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); }); diff --git a/addons/xterm-addon-fit/test/FitAddon.api.ts b/addons/xterm-addon-fit/test/FitAddon.api.ts index 8859b5a6..987092ad 100644 --- a/addons/xterm-addon-fit/test/FitAddon.api.ts +++ b/addons/xterm-addon-fit/test/FitAddon.api.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { openTerminal, getBrowserType } from '../../../out-test/api/TestUtils'; +import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils'; import { Browser, Page } from 'playwright'; const APP = 'http://127.0.0.1:3001/test'; @@ -16,10 +16,7 @@ const height = 768; describe('FitAddon', () => { before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.includes('--headless') - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); diff --git a/addons/xterm-addon-ligatures/package.json b/addons/xterm-addon-ligatures/package.json index ecbcc006..b1688772 100644 --- a/addons/xterm-addon-ligatures/package.json +++ b/addons/xterm-addon-ligatures/package.json @@ -36,7 +36,7 @@ }, "devDependencies": { "@types/sinon": "^5.0.1", - "axios": "^0.18.0", + "axios": "^0.21.2", "mkdirp": "0.5.5", "sinon": "6.3.5", "yauzl": "^2.10.0" diff --git a/addons/xterm-addon-ligatures/yarn.lock b/addons/xterm-addon-ligatures/yarn.lock index 2aac858b..f6ce913b 100644 --- a/addons/xterm-addon-ligatures/yarn.lock +++ b/addons/xterm-addon-ligatures/yarn.lock @@ -45,23 +45,17 @@ array-from@^2.1.1: resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= -axios@^0.18.0: - version "0.18.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.1.tgz#ff3f0de2e7b5d180e757ad98000f1081b87bcea3" +axios@^0.21.2: + version "0.21.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017" + integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg== dependencies: - follow-redirects "1.5.10" - is-buffer "^2.0.2" + follow-redirects "^1.14.0" buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" -debug@=3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - dependencies: - ms "2.0.0" - diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -72,11 +66,10 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" - dependencies: - debug "=3.1.0" +follow-redirects@^1.14.0: + version "1.14.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e" + integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw== font-finder@^1.0.3: version "1.0.4" @@ -110,10 +103,6 @@ has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" -is-buffer@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" - isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" @@ -163,10 +152,6 @@ mkdirp@0.5.5: dependencies: minimist "^1.2.5" -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - nise@^1.4.5: version "1.5.3" resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.3.tgz#9d2cfe37d44f57317766c6e9408a359c5d3ac1f7" diff --git a/addons/xterm-addon-search/package.json b/addons/xterm-addon-search/package.json index 659bd834..08fe195f 100644 --- a/addons/xterm-addon-search/package.json +++ b/addons/xterm-addon-search/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-search", - "version": "0.8.0", + "version": "0.8.1", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-search/test/SearchAddon.api.ts b/addons/xterm-addon-search/test/SearchAddon.api.ts index 3c94d416..8bdf61cf 100644 --- a/addons/xterm-addon-search/test/SearchAddon.api.ts +++ b/addons/xterm-addon-search/test/SearchAddon.api.ts @@ -6,7 +6,7 @@ import { assert } from 'chai'; import { readFile } from 'fs'; import { resolve } from 'path'; -import { openTerminal, writeSync, getBrowserType } from '../../../out-test/api/TestUtils'; +import { openTerminal, writeSync, launchBrowser } from '../../../out-test/api/TestUtils'; import { Browser, Page } from 'playwright'; const APP = 'http://127.0.0.1:3001/test'; @@ -18,10 +18,7 @@ const height = 600; describe('Search Tests', function(): void { before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.includes('--headless') - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); diff --git a/addons/xterm-addon-serialize/package.json b/addons/xterm-addon-serialize/package.json index 770771c4..77a54dbf 100644 --- a/addons/xterm-addon-serialize/package.json +++ b/addons/xterm-addon-serialize/package.json @@ -1,12 +1,13 @@ { "name": "xterm-addon-serialize", - "version": "0.5.0", + "version": "0.6.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" }, "main": "lib/xterm-addon-serialize.js", "types": "typings/xterm-addon-serialize.d.ts", + "repository": "https://github.com/xtermjs/xterm.js", "license": "MIT", "scripts": { "build": "../../node_modules/.bin/tsc -p .", diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index a54b4325..24020b74 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -72,7 +72,8 @@ function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean { && cell1.isBlink() === cell2.isBlink() && cell1.isInvisible() === cell2.isInvisible() && cell1.isItalic() === cell2.isItalic() - && cell1.isDim() === cell2.isDim(); + && cell1.isDim() === cell2.isDim() + && cell1.isStrikethrough() === cell2.isStrikethrough(); } class StringSerializeHandler extends BaseSerializeHandler { @@ -160,7 +161,7 @@ class StringSerializeHandler extends BaseSerializeHandler { if ( // you must output character to cause overflow, control sequence can't do this nextRowFirstChar.getChars() && - isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0 + isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0 ) { if ( // the last character can't be null, @@ -259,6 +260,7 @@ class StringSerializeHandler extends BaseSerializeHandler { if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); } if (cell.isItalic() !== oldCell.isItalic()) { sgrSeq.push(cell.isItalic() ? 3 : 23); } if (cell.isDim() !== oldCell.isDim()) { sgrSeq.push(cell.isDim() ? 2 : 22); } + if (cell.isStrikethrough() !== oldCell.isStrikethrough()) { sgrSeq.push(cell.isStrikethrough() ? 9 : 29); } } } } @@ -433,26 +435,37 @@ export class SerializeAddon implements ITerminalAddon { return content; } - public serialize(scrollback?: number): string { + public serialize(options?: ISerializeOptions): string { // TODO: Add combinedData support if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } // Normal buffer - let content = this._serializeBuffer(this._terminal, this._terminal.buffer.normal, scrollback); + let content = this._serializeBuffer(this._terminal, this._terminal.buffer.normal, options?.scrollback); // Alternate buffer - if (this._terminal.buffer.active.type === 'alternate') { - const alternativeScreenContent = this._serializeBuffer(this._terminal, this._terminal.buffer.alternate, undefined); - content += `\u001b[?1049h\u001b[H${alternativeScreenContent}`; + if (!options?.excludeAltBuffer) { + if (this._terminal.buffer.active.type === 'alternate') { + const alternativeScreenContent = this._serializeBuffer(this._terminal, this._terminal.buffer.alternate, undefined); + content += `\u001b[?1049h\u001b[H${alternativeScreenContent}`; + } } // Modes - content += this._serializeModes(this._terminal); + if (!options?.excludeModes) { + content += this._serializeModes(this._terminal); + } return content; } public dispose(): void { } } + + +interface ISerializeOptions { + scrollback?: number; + excludeModes?: boolean; + excludeAltBuffer?: boolean; +} diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index 87f77f59..334016e6 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { openTerminal, writeSync, getBrowserType } from '../../../out-test/api/TestUtils'; +import { openTerminal, writeSync, launchBrowser } from '../../../out-test/api/TestUtils'; import { Browser, Page } from 'playwright'; const APP = 'http://127.0.0.1:3001/test'; @@ -38,10 +38,7 @@ async function testSerializeEquals(writeContent: string, expectedSerialized: str describe('SerializeAddon', () => { before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.includes('--headless') - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); @@ -149,7 +146,7 @@ describe('SerializeAddon', () => { const cols = 10; const lines = newArray((index: number) => digitsString(cols, index), rows); await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize(${halfScrollback});`), lines.slice(halfScrollback, rows).join('\r\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: ${halfScrollback} });`), lines.slice(halfScrollback, rows).join('\r\n')); }); it('serialize 0 rows of scrollback', async function(): Promise { @@ -157,7 +154,19 @@ describe('SerializeAddon', () => { const cols = 10; const lines = newArray((index: number) => digitsString(cols, index), rows); await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), lines.slice(rows - 10, rows).join('\r\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: 0 });`), lines.slice(rows - 10, rows).join('\r\n')); + }); + + it('serialize exclude modes', async () => { + await writeSync(page, 'before\\x1b[?1hafter'); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), 'beforeafter\x1b[?1h'); + assert.equal(await page.evaluate(`serializeAddon.serialize({ excludeModes: true });`), 'beforeafter'); + }); + + it('serialize exclude alt buffer', async () => { + await writeSync(page, 'normal\\x1b[?1049h\\x1b[Halt'); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), 'normal\x1b[?1049h\x1b[Halt'); + assert.equal(await page.evaluate(`serializeAddon.serialize({ excludeAltBuffer: true });`), 'normal'); }); it('serialize all rows of content with color16', async function(): Promise { @@ -187,11 +196,13 @@ describe('SerializeAddon', () => { sgr(UNDERLINED) + line, sgr(BLINK) + line, sgr(INVISIBLE) + line, + sgr(STRIKETHROUGH) + line, sgr(NO_INVERSE) + line, sgr(NO_BOLD) + line, sgr(NO_UNDERLINED) + line, sgr(NO_BLINK) + line, - sgr(NO_INVISIBLE) + line + sgr(NO_INVISIBLE) + line, + sgr(NO_STRIKETHROUGH) + line ]; const rows = lines.length; await writeSync(page, lines.join('\\r\\n')); @@ -582,20 +593,20 @@ const BG_RGB_GREEN = '48;2;0;255;0'; const BG_RGB_YELLOW = '48;2;255;255;0'; const BG_RESET = '49'; -const INVERSE = '7'; const BOLD = '1'; +const DIM = '2'; +const ITALIC = '3'; const UNDERLINED = '4'; const BLINK = '5'; +const INVERSE = '7'; const INVISIBLE = '8'; +const STRIKETHROUGH = '9'; -const NO_INVERSE = '27'; const NO_BOLD = '22'; +const NO_DIM = '22'; +const NO_ITALIC = '23'; const NO_UNDERLINED = '24'; const NO_BLINK = '25'; +const NO_INVERSE = '27'; const NO_INVISIBLE = '28'; - -const ITALIC = '3'; -const DIM = '2'; - -const NO_ITALIC = '23'; -const NO_DIM = '22'; +const NO_STRIKETHROUGH = '29'; diff --git a/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts index b55ee303..a29dbb28 100644 --- a/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts +++ b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts @@ -7,14 +7,14 @@ import { Terminal, ITerminalAddon } from 'xterm'; declare module 'xterm-addon-serialize' { /** - * An xterm.js addon that enables web links. + * An xterm.js addon that enables serialization of terminal contents. */ export class SerializeAddon implements ITerminalAddon { constructor(); /** - * Activates the addon + * Activates the addon. * @param terminal The terminal the addon is being loaded in. */ public activate(terminal: Terminal): void; @@ -24,15 +24,32 @@ declare module 'xterm-addon-serialize' { * the state. The cursor will also be positioned to the correct cell. When restoring a terminal * it is best to do before `Terminal.open` is called to avoid wasting CPU cycles rendering * incomplete frames. - * @param scrollback The number of rows in scrollback buffer to serialize, starting from the - * bottom of the scrollback buffer. This defaults to the all available rows in the scrollback - * buffer. + * @param options Custom options to allow control over what gets serialized. */ - public serialize(scrollback?: number): string; + public serialize(options?: ISerializeOptions): string; /** * Disposes the addon. */ public dispose(): void; } + + export interface ISerializeOptions { + /** + * The number of rows in the scrollback buffer to serialize, starting from the bottom of the + * scrollback buffer. When not specified, all available rows in the scrollback buffer will be + * serialized. + */ + scrollback?: number; + + /** + * Whether to exclude the terminal modes from the serialization. False by default. + */ + excludeModes?: boolean; + + /** + * Whether to exclude the alt buffer from the serialization. False by default. + */ + excludeAltBuffer?: boolean; + } } diff --git a/addons/xterm-addon-unicode11/package.json b/addons/xterm-addon-unicode11/package.json index 397bf2b9..9fc69416 100644 --- a/addons/xterm-addon-unicode11/package.json +++ b/addons/xterm-addon-unicode11/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-unicode11", - "version": "0.2.0", + "version": "0.3.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-unicode11/test/Unicode11Addon.api.ts b/addons/xterm-addon-unicode11/test/Unicode11Addon.api.ts index ba536e90..4c695b00 100644 --- a/addons/xterm-addon-unicode11/test/Unicode11Addon.api.ts +++ b/addons/xterm-addon-unicode11/test/Unicode11Addon.api.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { openTerminal, getBrowserType } from '../../../out-test/api/TestUtils'; +import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils'; import { Browser, Page } from 'playwright'; const APP = 'http://127.0.0.1:3001/test'; @@ -16,10 +16,7 @@ const height = 600; describe('Unicode11Addon', () => { before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.includes('--headless') - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); }); diff --git a/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts b/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts index 54650f1f..fe44dc31 100644 --- a/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts +++ b/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { openTerminal, pollFor, writeSync, getBrowserType } from '../../../out-test/api/TestUtils'; +import { openTerminal, pollFor, writeSync, launchBrowser } from '../../../out-test/api/TestUtils'; import { Browser, Page } from 'playwright'; const APP = 'http://127.0.0.1:3001/test'; @@ -16,10 +16,7 @@ const height = 600; describe('WebLinksAddon', () => { before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.includes('--headless') - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); }); diff --git a/addons/xterm-addon-webgl/package.json b/addons/xterm-addon-webgl/package.json index 421f6092..226f34ba 100644 --- a/addons/xterm-addon-webgl/package.json +++ b/addons/xterm-addon-webgl/package.json @@ -1,6 +1,6 @@ { "name": "xterm-addon-webgl", - "version": "0.11.1", + "version": "0.11.2", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index ad2393d8..b8bcf5b1 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -8,6 +8,7 @@ import { WebglRenderer } from './WebglRenderer'; import { ICharacterJoinerService, IRenderService } from 'browser/services/Services'; import { IColorSet } from 'browser/Types'; import { EventEmitter } from 'common/EventEmitter'; +import { isSafari } from 'common/Platform'; export class WebglAddon implements ITerminalAddon { private _terminal?: Terminal; @@ -23,6 +24,9 @@ export class WebglAddon implements ITerminalAddon { if (!terminal.element) { throw new Error('Cannot activate WebglAddon before Terminal.open'); } + if (isSafari) { + throw new Error('Webgl is not currently supported on Safari'); + } this._terminal = terminal; const renderService: IRenderService = (terminal as any)._core._renderService; const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService; diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 75b6230c..9d8bde79 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -108,7 +108,7 @@ export class WebglRenderer extends Disposable implements IRenderer { for (const l of this._renderLayers) { l.dispose(); } - this._core.screenElement!.removeChild(this._canvas); + this._canvas.parentElement?.removeChild(this._canvas); super.dispose(); } diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 340e83c9..6128c836 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -402,6 +402,22 @@ export class WebglCharAtlas implements IDisposable { this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight); } + // If this charcater is underscore and beyond the cell bounds, shift it up until it is visible, + // try for a maximum of 5 pixels. + if (chars === '_' && !this._config.allowTransparency) { + let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor); + if (isBeyondCellBounds) { + for (let offset = 1; offset <= 5; offset++) { + this._tmpCtx.clearRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height); + this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset); + isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor); + if (!isBeyondCellBounds) { + break; + } + } + } + } + // Draw underline and strikethrough if (underline || strikethrough) { const lineWidth = Math.max(1, Math.floor(this._config.fontSize / 10)); @@ -443,7 +459,7 @@ export class WebglCharAtlas implements IDisposable { return NULL_RASTERIZED_GLYPH; } - const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, isPowerlineGlyph); + const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, isPowerlineGlyph, drawSuccess); const clippedImageData = this._clipImageData(imageData, this._workBoundingBox); // Check if there is enough room in the current row and go to next if needed @@ -476,7 +492,7 @@ export class WebglCharAtlas implements IDisposable { * @param imageData The image data to read. * @param boundingBox An IBoundingBox to put the clipped bounding box values. */ - private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean): IRasterizedGlyph { + private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean): IRasterizedGlyph { boundingBox.top = 0; const height = restrictedGlyph ? this._config.scaledCharHeight : this._tmpCanvas.height; const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth; @@ -551,8 +567,8 @@ export class WebglCharAtlas implements IDisposable { y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT }, offset: { - x: -boundingBox.left + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING), - y: -boundingBox.top + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING) + x: -boundingBox.left + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING) + (customGlyph ? Math.floor(this._config.letterSpacing / 2) : 0), + y: -boundingBox.top + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING) + (customGlyph ? this._config.lineHeight === 1 ? 0 : Math.round((this._config.scaledCellHeight - this._config.scaledCharHeight) / 2) : 0) } }; } diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index 793929e9..e6942d1c 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -6,7 +6,7 @@ import { assert } from 'chai'; import { Browser, Page } from 'playwright'; import { ITheme } from 'xterm'; -import { getBrowserType, openTerminal, pollFor, writeSync } from '../../../out-test/api/TestUtils'; +import { getBrowserType, launchBrowser, openTerminal, pollFor, writeSync } from '../../../out-test/api/TestUtils'; import { ITerminalOptions } from '../../../src/common/Types'; const APP = 'http://127.0.0.1:3001/test'; @@ -905,10 +905,7 @@ async function getCellPixels(col: number, row: number): Promise { } async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.includes('--headless') - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); diff --git a/bin/test_api.js b/bin/test_api.js index 9fe4659a..f173b417 100644 --- a/bin/test_api.js +++ b/bin/test_api.js @@ -59,6 +59,12 @@ server.stdout.on('data', (data) => { `${script}.cmd` : script)); } + server.kill(); + process.exit(run.status); } }); + +server.stderr.on('data', (data) => { + console.error(data.toString()); +}); diff --git a/demo/client.ts b/demo/client.ts index de1faa37..a5ac8bb5 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -148,6 +148,7 @@ if (document.location.pathname === '/test') { document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); document.getElementById('serialize').addEventListener('click', serializeButtonHandler); document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler); + document.getElementById('load-test').addEventListener('click', loadTest); } function createTerminal(): void { @@ -349,6 +350,9 @@ function initOptions(term: TerminalType): void { } else if (o === 'lineHeight' || o === 'scrollSensitivity') { term.setOption(o, parseFloat(input.value)); updateTerminalSize(); + } else if(o === 'scrollback') { + term.setOption(o, parseInt(input.value)); + setTimeout(() => updateTerminalSize(), 5); } else { term.setOption(o, parseInt(input.value)); } @@ -373,6 +377,9 @@ function initAddons(term: TerminalType): void { if (!addon.canChange) { checkbox.disabled = true; } + if(name === 'unicode11' && checkbox.checked) { + term.unicode.activeVersion = '11'; + } addDomListener(checkbox, 'change', () => { if (checkbox.checked) { addon.instance = new addon.ctor(); @@ -381,10 +388,14 @@ function initAddons(term: TerminalType): void { setTimeout(() => { document.body.appendChild((addon.instance as WebglAddon).textureAtlas); }, 0); + } else if (name === 'unicode11') { + term.unicode.activeVersion = '11'; } } else { if (name === 'webgl') { document.body.removeChild((addon.instance as WebglAddon).textureAtlas); + } else if (name === 'unicode11') { + term.unicode.activeVersion = '6'; } addon.instance!.dispose(); addon.instance = undefined; @@ -478,3 +489,36 @@ function writeCustomGlyphHandler() { term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r'); window.scrollTo(0, 0); } + +function loadTest() { + const isWebglEnabled = !!addons.webgl.instance; + const testData = []; + let byteCount = 0; + for (let i = 0; i < 50; i++) { + const count = 1 + Math.floor(Math.random() * 79); + byteCount += count + 2; + const data = new Uint8Array(count + 2); + data[0] = 0x0A; // \n + for (let i = 1; i < count + 1; i++) { + data[i] = 0x61 + Math.floor(Math.random() * (0x7A - 0x61)); + } + // End each line with \r so the cursor remains constant, this is what ls/tree do and improves + // performance significantly due to the cursor DOM element not needing to change + data[data.length - 1] = 0x0D; // \r + testData.push(data); + } + const start = performance.now(); + for (let i = 0; i < 1024; i++) { + for (const d of testData) { + term.write(d); + } + } + // Wait for all data to be parsed before evaluating time + term.write('', () => { + const time = Math.round(performance.now() - start); + const mbs = ((byteCount / 1024) * (1 / (time / 1000))).toFixed(2); + term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${isWebglEnabled ? 'webgl' : 'canvas'} renderer)`); + // Send ^C to get a new prompt + term._core._onData.fire('\x03'); + }); +} diff --git a/demo/index.html b/demo/index.html index 910397c3..9c86783b 100644 --- a/demo/index.html +++ b/demo/index.html @@ -15,42 +15,91 @@

xterm.js: A terminal for the web

-
-
-

Options

-

These options can be set in the Terminal constructor or by using the Terminal.setOption function.

-
-
-
-

Addons

-

Addons can be loaded and unloaded on a particular terminal to extend its functionality.

-
-

Addons Control

-

SearchAddon

-

- - - - - -

-

SerializeAddon

-

- - -

-

-
-
-

Style

-
- - +
+
+
+
+
+
+ + + + +
+
+

Options

+

These options can be set in the Terminal constructor or by using the Terminal.setOption function.

+
+
+
+

Addons

+

Addons can be loaded and unloaded on a particular terminal to extend its functionality.

+
+

Addons Control

+

SearchAddon

+
+ + + + + +
+

SerializeAddon

+
+ + +
+
+
+
+

Style

+
+ + +
+
+
+

Test

+
+ + + +
+
-
- - - - + + + diff --git a/demo/style.css b/demo/style.css index 9c5fd0bd..cebd08e0 100644 --- a/demo/style.css +++ b/demo/style.css @@ -41,3 +41,52 @@ pre { word-wrap: break-word; white-space: pre-wrap; } + + +#container { + display: flex; + height: 75vh; +} +#grid { + flex: 1; + /* max-height: 80vh; + overflow-y: auto; */ + width: 100%; +} +.tab { + overflow: hidden; + border: 1px solid #ccc; + background-color: #f1f1f1; +} + +/* Style the buttons inside the tab */ +.tab button { + background-color: inherit; + float: left; + border: none; + outline: none; + cursor: pointer; + padding: 14px 16px; + transition: 0.3s; + font-size: 17px; +} + +/* Change background color of buttons on hover */ +.tab button:hover { + background-color: #ddd; +} + +/* Create an active/current tablink class */ +.tab button.active { + background-color: #ccc; + } + +/* Style the tab content */ +.tabContent { + display: none; + padding: 6px 12px; + border: 1px solid #ccc; + border-top: none; + max-height: 100vh; + overflow-y: auto; +} diff --git a/package.json b/package.json index 71a0f308..ad288f49 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "4.13.0", + "version": "4.14.1", "main": "lib/xterm.js", "style": "css/xterm.css", "types": "typings/xterm.d.ts", @@ -23,6 +23,7 @@ "test-api-webkit": "node ./bin/test_api.js --browser=webkit --timeout=20000", "test-unit": "node ./bin/test.js", "test-unit-coverage": "node ./bin/test.js --coverage", + "test-unit-dev": "cross-env NODE_PATH='./out' mocha", "build": "tsc -b ./tsconfig.all.json", "prepare": "npm run setup", "setup": "npm run build", @@ -49,6 +50,7 @@ "@typescript-eslint/eslint-plugin": "^4.23.0", "@typescript-eslint/parser": "^4.23.0", "chai": "^4.3.4", + "cross-env": "^7.0.3", "deep-equal": "^2.0.5", "eslint": "^7.26.0", "express": "^4.17.1", @@ -61,6 +63,7 @@ "nyc": "^15.1.0", "playwright": "^1.11.0", "source-map-loader": "^2.0.1", + "source-map-support": "^0.5.19", "ts-loader": "^9.1.2", "typescript": "^4.2.4", "utf8": "^3.0.0", diff --git a/src/browser/AccessibilityManager.ts b/src/browser/AccessibilityManager.ts index 1be3342d..80092202 100644 --- a/src/browser/AccessibilityManager.ts +++ b/src/browser/AccessibilityManager.ts @@ -53,6 +53,7 @@ export class AccessibilityManager extends Disposable { ) { super(); this._accessibilityTreeRoot = document.createElement('div'); + this._accessibilityTreeRoot.setAttribute('role', 'document'); this._accessibilityTreeRoot.classList.add('xterm-accessibility'); this._rowContainer = document.createElement('div'); diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index 84e6a87e..a102d93f 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -732,80 +732,78 @@ describe('Terminal', () => { }); describe('unicode - surrogates', () => { - it('2 characters per cell', async function (): Promise { - this.timeout(10000); // This is needed because istanbul patches code and slows it down - const high = String.fromCharCode(0xD800); - const cell = new CellData(); - for (let i = 0xDC00; i <= 0xDCFF; ++i) { - await term.writeP(high + String.fromCharCode(i)); - const tchar = term.buffer.lines.get(0)!.loadCell(0, cell); - assert.equal(tchar.getChars(), high + String.fromCharCode(i)); - assert.equal(tchar.getChars().length, 2); - assert.equal(tchar.getWidth(), 1); - assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), ''); - term.reset(); - } - }); - it('2 characters at last cell', async () => { - const high = String.fromCharCode(0xD800); - const cell = new CellData(); - for (let i = 0xDC00; i <= 0xDCFF; ++i) { - term.buffer.x = term.cols - 1; - await term.writeP(high + String.fromCharCode(i)); - assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars(), high + String.fromCharCode(i)); - assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars().length, 2); - assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), ''); - term.reset(); - } - }); - it('2 characters per cell over line end with autowrap', async function (): Promise { - this.timeout(10000); - const high = String.fromCharCode(0xD800); - const cell = new CellData(); - for (let i = 0xDC00; i <= 0xDCFF; ++i) { - term.buffer.x = term.cols - 1; - - await term.writeP('a' + high + String.fromCharCode(i)); - assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), 'a'); - assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), high + String.fromCharCode(i)); - assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars().length, 2); - assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), ''); - term.reset(); - } - }); - it('2 characters per cell over line end without autowrap', async function (): Promise { - this.timeout(10000); - const high = String.fromCharCode(0xD800); - const cell = new CellData(); - for (let i = 0xDC00; i <= 0xDCFF; ++i) { - term.buffer.x = term.cols - 1; - await term.writeP('\x1b[?7l'); // Disable wraparound mode - const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000); - if (width !== 1) { - continue; + for (let i = 0xDC00; i <= 0xDCF0; i += 0x10) { + const range = `0x${i.toString(16).toUpperCase()}-0x${(i + 0xF).toString(16).toUpperCase()}`; + it(`${range}: 2 characters per cell`, async function (): Promise { + const high = String.fromCharCode(0xD800); + const cell = new CellData(); + for (let j = i; j <= i + 0xF; j++) { + await term.writeP(high + String.fromCharCode(j)); + const tchar = term.buffer.lines.get(0)!.loadCell(0, cell); + assert.equal(tchar.getChars(), high + String.fromCharCode(j)); + assert.equal(tchar.getChars().length, 2); + assert.equal(tchar.getWidth(), 1); + assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), ''); + term.reset(); } - await term.writeP('a' + high + String.fromCharCode(i)); - // auto wraparound mode should cut off the rest of the line - assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), high + String.fromCharCode(i)); - assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars().length, 2); - assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), ''); - term.reset(); - } - }); - it('splitted surrogates', async function (): Promise { - this.timeout(10000); - const high = String.fromCharCode(0xD800); - const cell = new CellData(); - for (let i = 0xDC00; i <= 0xDCFF; ++i) { - await term.writeP(high + String.fromCharCode(i)); - const tchar = term.buffer.lines.get(0)!.loadCell(0, cell); - assert.equal(tchar.getChars(), high + String.fromCharCode(i)); - assert.equal(tchar.getChars().length, 2); - assert.equal(tchar.getWidth(), 1); - assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), ''); - term.reset(); - } - }); + }); + it(`${range}: 2 characters at last cell`, async () => { + const high = String.fromCharCode(0xD800); + const cell = new CellData(); + term.buffer.x = term.cols - 1; + for (let j = i; j <= i + 0xF; j++) { + await term.writeP(high + String.fromCharCode(j)); + assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars(), high + String.fromCharCode(j)); + assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars().length, 2); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), ''); + term.reset(); + } + }); + it(`${range}: 2 characters per cell over line end with autowrap`, async function (): Promise { + const high = String.fromCharCode(0xD800); + const cell = new CellData(); + for (let j = i; j <= i + 0xF; j++) { + term.buffer.x = term.cols - 1; + await term.writeP('a' + high + String.fromCharCode(j)); + assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), high + String.fromCharCode(j)); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars().length, 2); + assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), ''); + term.reset(); + } + }); + it(`${range}: 2 characters per cell over line end without autowrap`, async function (): Promise { + const high = String.fromCharCode(0xD800); + const cell = new CellData(); + for (let j = i; j <= i + 0xF; j++) { + term.buffer.x = term.cols - 1; + await term.writeP('\x1b[?7l'); // Disable wraparound mode + const width = wcwidth((0xD800 - 0xD800) * 0x400 + j - 0xDC00 + 0x10000); + if (width !== 1) { + continue; + } + await term.writeP('a' + high + String.fromCharCode(j)); + // auto wraparound mode should cut off the rest of the line + assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), high + String.fromCharCode(j)); + assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars().length, 2); + assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), ''); + term.reset(); + } + }); + it(`${range}: splitted surrogates`, async function (): Promise { + const high = String.fromCharCode(0xD800); + const cell = new CellData(); + for (let j = i; j <= i + 0xF; j++) { + await term.writeP(high + String.fromCharCode(j)); + const tchar = term.buffer.lines.get(0)!.loadCell(0, cell); + assert.equal(tchar.getChars(), high + String.fromCharCode(j)); + assert.equal(tchar.getChars().length, 2); + assert.equal(tchar.getWidth(), 1); + assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), ''); + term.reset(); + } + }); + } }); describe('unicode - combining characters', () => { diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 513fdf62..4afca04c 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -94,6 +94,20 @@ export class Terminal extends CoreTerminal implements ITerminal { */ private _keyDownHandled: boolean = false; + /** + * Records whether the keypress event has already been handled and triggered a data event, if so + * the input event should not trigger a data event but should still print to the textarea so + * screen readers will announce it. + */ + private _keyPressHandled: boolean = false; + + /** + * Records whether there has been a keydown event for a dead key without a corresponding keydown + * event for the composed/alternative character. If we cancel the keydown event for the dead key, + * no events will be emitted for the final character. + */ + private _unprocessedDeadKey: boolean = false; + public linkifier: ILinkifier; public linkifier2: ILinkifier2; public viewport: IViewport | undefined; @@ -384,6 +398,7 @@ export class Terminal extends CoreTerminal implements ITerminal { 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(addDisposableDomListener(this.textarea!, 'input', (ev: InputEvent) => this._inputEvent(ev), true)); this.register(this.onRender(() => this._compositionHelper!.updateCompositionElements())); this.register(this.onRender(e => this._queueLinkification(e.start, e.end))); } @@ -1026,6 +1041,10 @@ export class Terminal extends CoreTerminal implements ITerminal { return false; } + if (event.key === 'Dead' || event.key === 'AltGraph') { + this._unprocessedDeadKey = true; + } + const result = evaluateKeyboardEvent(event, this.coreService.decPrivateModes.applicationCursorKeys, this.browser.isMac, this.options.macOptionIsMeta); this.updateCursorStyle(event); @@ -1053,6 +1072,11 @@ export class Terminal extends CoreTerminal implements ITerminal { return true; } + if (this._unprocessedDeadKey) { + this._unprocessedDeadKey = false; + return true; + } + // If ctrl+c or enter is being sent, clear out the textarea. This is done so that screen readers // will announce deleted characters. This will not work 100% of the time but it should cover // most scenarios. @@ -1075,10 +1099,11 @@ export class Terminal extends CoreTerminal implements ITerminal { this._keyDownHandled = true; } - private _isThirdLevelShift(browser: IBrowser, ev: IKeyboardEvent): boolean { + private _isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean { const thirdLevelKey = (browser.isMac && !this.options.macOptionIsMeta && ev.altKey && !ev.ctrlKey && !ev.metaKey) || - (browser.isWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); + (browser.isWindows && ev.altKey && ev.ctrlKey && !ev.metaKey) || + (browser.isWindows && ev.getModifierState('AltGraph')); if (ev.type === 'keypress') { return thirdLevelKey; @@ -1098,6 +1123,7 @@ export class Terminal extends CoreTerminal implements ITerminal { } this.updateCursorStyle(ev); + this._keyPressHandled = false; } /** @@ -1109,6 +1135,8 @@ export class Terminal extends CoreTerminal implements ITerminal { protected _keyPress(ev: KeyboardEvent): boolean { let key; + this._keyPressHandled = false; + if (this._keyDownHandled) { return false; } @@ -1141,9 +1169,35 @@ export class Terminal extends CoreTerminal implements ITerminal { this._showCursor(); this.coreService.triggerDataEvent(key, true); + this._keyPressHandled = true; + return true; } + /** + * Handle an input event. + * Key Resources: + * - https://developer.mozilla.org/en-US/docs/Web/API/InputEvent + * @param ev The input event to be handled. + */ + protected _inputEvent(ev: InputEvent): boolean { + // Only support emoji IMEs when screen reader mode is disabled as the event must bubble up to + // support reading out character input which can doubling up input characters + if (ev.data && ev.inputType === 'insertText' && !this.optionsService.options.screenReaderMode) { + if (this._keyPressHandled) { + return false; + } + + const text = ev.data; + this.coreService.triggerDataEvent(text, true); + + this.cancel(ev); + return true; + } + + return false; + } + /** * Ring the bell. * Note: We could do sweet things with webaudio here @@ -1239,6 +1293,10 @@ export class Terminal extends CoreTerminal implements ITerminal { this.viewport?.syncScrollArea(); } + public clearTextureAtlas(): void { + this._renderService?.clearTextureAtlas(); + } + private _reportFocus(): void { if (this.element?.classList.contains('focus')) { this.coreService.triggerDataEvent(C0.ESC + '[I'); diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index daa6843c..8fdf458e 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -195,6 +195,9 @@ export class MockTerminal implements ITerminal { public reset(): void { throw new Error('Method not implemented.'); } + public clearTextureAtlas(): void { + throw new Error('Method not implemented.'); + } public refresh(start: number, end: number): void { throw new Error('Method not implemented.'); } @@ -374,6 +377,9 @@ export class MockRenderService implements IRenderService { public refreshRows(start: number, end: number): void { throw new Error('Method not implemented.'); } + public clearTextureAtlas(): void { + throw new Error('Method not implemented.'); + } public resize(cols: number, rows: number): void { throw new Error('Method not implemented.'); } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 0d74b39f..bafeff77 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -79,6 +79,7 @@ export interface IPublicTerminal extends IDisposable { write(data: string | Uint8Array, callback?: () => void): void; paste(data: string): void; refresh(start: number, end: number): void; + clearTextureAtlas(): void; reset(): void; } diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index 162ed174..3c9bea4c 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -8,6 +8,8 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { IColorSet, IViewport } from 'browser/Types'; import { ICharSizeService, IRenderService } from 'browser/services/Services'; import { IBufferService, IOptionsService } from 'common/services/Services'; +import { IBuffer } from 'common/buffer/Types'; +import { IRenderDimensions } from 'browser/renderer/Types'; const FALLBACK_SCROLL_BAR_WIDTH = 15; @@ -18,11 +20,15 @@ const FALLBACK_SCROLL_BAR_WIDTH = 15; export class Viewport extends Disposable implements IViewport { public scrollBarWidth: number = 0; private _currentRowHeight: number = 0; + private _currentScaledCellHeight: number = 0; private _lastRecordedBufferLength: number = 0; private _lastRecordedViewportHeight: number = 0; private _lastRecordedBufferHeight: number = 0; private _lastTouchY: number = 0; private _lastScrollTop: number = 0; + private _lastHadScrollBar: boolean = false; + private _activeBuffer: IBuffer; + private _renderDimensions: IRenderDimensions; // Stores a partial line amount when scrolling, this is used to keep track of how much of a line // is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a @@ -47,8 +53,15 @@ export class Viewport extends Disposable implements IViewport { // Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case, // therefore we account for a standard amount to make it visible this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; + this._lastHadScrollBar = true; this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this))); + // Track properties used in performance critical code manually to avoid using slow getters + this._activeBuffer = this._bufferService.buffer; + this.register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer)); + this._renderDimensions = this._renderService.dimensions; + this.register(this._renderService.onDimensionsChange(e => this._renderDimensions = e)); + // Perform this async to ensure the ICharSizeService is ready. setTimeout(() => this.syncScrollArea(), 0); } @@ -77,6 +90,7 @@ export class Viewport extends Disposable implements IViewport { private _innerRefresh(): void { if (this._charSizeService.height > 0) { this._currentRowHeight = this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio; + this._currentScaledCellHeight = this._renderService.dimensions.scaledCellHeight; this._lastRecordedViewportHeight = this._viewportElement.offsetHeight; const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderService.dimensions.canvasHeight); if (this._lastRecordedBufferHeight !== newBufferHeight) { @@ -94,8 +108,18 @@ export class Viewport extends Disposable implements IViewport { this._viewportElement.scrollTop = scrollTop; } + // Update scroll bar width + if (this._optionsService.options.scrollback === 0) { + this.scrollBarWidth = 0; + } else { + this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH; + } + this._lastHadScrollBar = this.scrollBarWidth > 0; + + this._viewportElement.style.width = (this._renderService.dimensions.actualCellWidth * (this._bufferService.cols) + this.scrollBarWidth).toString() + 'px'; this._refreshAnimationFrame = null; } + /** * Updates dimensions and synchronizes the scroll area if necessary. */ @@ -114,23 +138,21 @@ export class Viewport extends Disposable implements IViewport { } // If the buffer position doesn't match last scroll top - const newScrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight; - if (this._lastScrollTop !== newScrollTop) { - this._refresh(immediate); - return; - } - - // If element's scroll top changed, this can happen when hiding the element - if (this._lastScrollTop !== this._viewportElement.scrollTop) { + if (this._lastScrollTop !== this._activeBuffer.ydisp * this._currentRowHeight) { this._refresh(immediate); return; } // If row height changed - if (this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) { + if (this._renderDimensions.scaledCellHeight !== this._currentScaledCellHeight) { this._refresh(immediate); return; } + + // If the scroll bar visibility changed + if (this._lastHadScrollBar !== (this._optionsService.options.scrollback > 0)) { + this._refresh(immediate); + } } /** diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index a76b1a22..26cf2728 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -222,6 +222,9 @@ export class Terminal implements ITerminalApi { public reset(): void { this._core.reset(); } + public clearTextureAtlas(): void { + this._core.clearTextureAtlas(); + } public loadAddon(addon: ITerminalAddon): void { return this._addonManager.loadAddon(this, addon); } diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 448451d0..68f83a75 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -138,6 +138,10 @@ export abstract class BaseRenderLayer implements IRenderLayer { public abstract reset(): void; + public clearTextureAtlas(): void { + this._charAtlas?.clear(); + } + /** * Fills 1+ cells completely. This uses the existing fillStyle on the context. * @param x The column to start at. diff --git a/src/browser/renderer/Renderer.ts b/src/browser/renderer/Renderer.ts index d5de40db..162a7ed3 100644 --- a/src/browser/renderer/Renderer.ts +++ b/src/browser/renderer/Renderer.ts @@ -149,6 +149,12 @@ export class Renderer extends Disposable implements IRenderer { } } + public clearTextureAtlas(): void { + for (const layer of this._renderLayers) { + layer.clearTextureAtlas(); + } + } + /** * Recalculates the character and canvas dimensions. */ diff --git a/src/browser/renderer/Types.d.ts b/src/browser/renderer/Types.d.ts index fc137bc8..6818a926 100644 --- a/src/browser/renderer/Types.d.ts +++ b/src/browser/renderer/Types.d.ts @@ -52,6 +52,7 @@ export interface IRenderer extends IDisposable { onOptionsChanged(): void; clear(): void; renderRows(start: number, end: number): void; + clearTextureAtlas?(): void; } export interface IRenderLayer extends IDisposable { @@ -100,4 +101,9 @@ export interface IRenderLayer extends IDisposable { * Clear the state of the render layer. */ reset(): void; + + /** + * Clears the texture atlas. + */ + clearTextureAtlas(): void; } diff --git a/src/browser/renderer/atlas/BaseCharAtlas.ts b/src/browser/renderer/atlas/BaseCharAtlas.ts index 4ebaaa47..83c30d2f 100644 --- a/src/browser/renderer/atlas/BaseCharAtlas.ts +++ b/src/browser/renderer/atlas/BaseCharAtlas.ts @@ -28,6 +28,8 @@ export abstract class BaseCharAtlas implements IDisposable { */ private _doWarmUp(): void { } + public clear(): void { } + /** * Called when we start drawing a new frame. * diff --git a/src/browser/renderer/atlas/DynamicCharAtlas.ts b/src/browser/renderer/atlas/DynamicCharAtlas.ts index 883ebe78..666324ad 100644 --- a/src/browser/renderer/atlas/DynamicCharAtlas.ts +++ b/src/browser/renderer/atlas/DynamicCharAtlas.ts @@ -119,6 +119,16 @@ export class DynamicCharAtlas extends BaseCharAtlas { this._drawToCacheCount = 0; } + public clear(): void { + if (this._cacheMap.size > 0) { + const capacity = this._width * this._height; + this._cacheMap = new LRUMap(capacity); + this._cacheMap.prealloc(capacity); + } + this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT); + this._tmpCtx.clearRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight); + } + public draw( ctx: CanvasRenderingContext2D, glyph: IGlyphIdentifier, @@ -266,11 +276,10 @@ export class DynamicCharAtlas extends BaseCharAtlas { } // Draw the character this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight); - this._tmpCtx.restore(); // clear the background from the character to avoid issues with drawing over the previous // character if it extends past it's bounds - const imageData = this._tmpCtx.getImageData( + let imageData = this._tmpCtx.getImageData( 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight ); let isEmpty = false; @@ -278,6 +287,27 @@ export class DynamicCharAtlas extends BaseCharAtlas { isEmpty = clearColor(imageData, backgroundColor); } + // If this charcater is underscore and empty, shift it up until it is visible, try for a maximum + // of 5 pixels. + if (isEmpty && glyph.chars === '_' && !this._config.allowTransparency) { + for (let offset = 1; offset <= 5; offset++) { + // Draw the character + this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight - offset); + + // clear the background from the character to avoid issues with drawing over the previous + // character if it extends past it's bounds + imageData = this._tmpCtx.getImageData( + 0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight + ); + isEmpty = clearColor(imageData, backgroundColor); + if (!isEmpty) { + break; + } + } + } + + this._tmpCtx.restore(); + // copy the data from imageData to _cacheCanvas const x = this._toCoordinateX(index); const y = this._toCoordinateY(index); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts index 2f8d264a..f41e5d44 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts @@ -10,7 +10,7 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { IBufferLine } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; -import { MockOptionsService } from 'common/TestUtils.test'; +import { MockCoreService, MockOptionsService } from 'common/TestUtils.test'; import { css } from 'browser/Color'; import { MockCharacterJoinerService } from 'browser/TestUtils.test'; @@ -21,30 +21,36 @@ describe('DomRendererRowFactory', () => { beforeEach(() => { dom = new jsdom.JSDOM(''); - rowFactory = new DomRendererRowFactory(dom.window.document, { - background: css.toColor('#010101'), - foreground: css.toColor('#020202'), - ansi: [ - // dark: - css.toColor('#2e3436'), - css.toColor('#cc0000'), - css.toColor('#4e9a06'), - css.toColor('#c4a000'), - css.toColor('#3465a4'), - css.toColor('#75507b'), - css.toColor('#06989a'), - css.toColor('#d3d7cf'), - // bright: - css.toColor('#555753'), - css.toColor('#ef2929'), - css.toColor('#8ae234'), - css.toColor('#fce94f'), - css.toColor('#729fcf'), - css.toColor('#ad7fa8'), - css.toColor('#34e2e2'), - css.toColor('#eeeeec') - ] - } as any, new MockCharacterJoinerService(), new MockOptionsService({ drawBoldTextInBrightColors: true })); + rowFactory = new DomRendererRowFactory( + dom.window.document, + { + background: css.toColor('#010101'), + foreground: css.toColor('#020202'), + ansi: [ + // dark: + css.toColor('#2e3436'), + css.toColor('#cc0000'), + css.toColor('#4e9a06'), + css.toColor('#c4a000'), + css.toColor('#3465a4'), + css.toColor('#75507b'), + css.toColor('#06989a'), + css.toColor('#d3d7cf'), + // bright: + css.toColor('#555753'), + css.toColor('#ef2929'), + css.toColor('#8ae234'), + css.toColor('#fce94f'), + css.toColor('#729fcf'), + css.toColor('#ad7fa8'), + css.toColor('#34e2e2'), + css.toColor('#eeeeec') + ] + } as any, + new MockCharacterJoinerService(), + new MockOptionsService({ drawBoldTextInBrightColors: true }), + new MockCoreService() + ); lineData = createEmptyLineData(2); }); diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index a61ebd73..a24f3e46 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -7,7 +7,7 @@ import { IBufferLine } from 'common/Types'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; -import { IOptionsService } from 'common/services/Services'; +import { ICoreService, IOptionsService } from 'common/services/Services'; import { color, rgba } from 'browser/Color'; import { IColorSet, IColor } from 'browser/Types'; import { ICharacterJoinerService } from 'browser/services/Services'; @@ -31,7 +31,8 @@ export class DomRendererRowFactory { private readonly _document: Document, private _colors: IColorSet, @ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService, - @IOptionsService private readonly _optionsService: IOptionsService + @IOptionsService private readonly _optionsService: IOptionsService, + @ICoreService private readonly _coreService: ICoreService ) { } @@ -110,7 +111,7 @@ export class DomRendererRowFactory { } } - if (isCursorRow && x === cursorX) { + if (!this._coreService.isCursorHidden && isCursorRow && x === cursorX) { charElement.classList.add(CURSOR_CLASS); if (cursorBlink) { diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 332e71da..b8283e0e 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -168,6 +168,11 @@ export class RenderService extends Disposable implements IRenderService { } } + public clearTextureAtlas(): void { + this._renderer?.clearTextureAtlas?.(); + this._fullRefresh(); + } + public setColors(colors: IColorSet): void { this._renderer.setColors(colors); this._fullRefresh(); diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 8c8a7bd9..4928fa28 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -53,6 +53,7 @@ export interface IRenderService extends IDisposable { dimensions: IRenderDimensions; refreshRows(start: number, end: number): void; + clearTextureAtlas(): void; resize(cols: number, rows: number): void; changeOptions(): void; setRenderer(renderer: IRenderer): void; diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 0f91a011..9371e5f5 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -20,6 +20,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum } from 'common/services/Services'; import { OscHandler } from 'common/parser/OscParser'; import { DcsHandler } from 'common/parser/DcsParser'; +import { IBuffer } from 'common/buffer/Types'; /** * Map collect to glevel. Used in `selectCharset`. @@ -234,6 +235,8 @@ export class InputHandler extends Disposable implements IInputHandler { private _curAttrData: IAttributeData = DEFAULT_ATTR_DATA.clone(); private _eraseAttrDataInternal: IAttributeData = DEFAULT_ATTR_DATA.clone(); + private _activeBuffer: IBuffer; + private _onRequestBell = new EventEmitter(); public get onRequestBell(): IEvent { return this._onRequestBell.event; } private _onRequestRefreshRows = new EventEmitter(); @@ -284,6 +287,10 @@ export class InputHandler extends Disposable implements IInputHandler { super(); this.register(this._parser); + // Track properties used in performance critical code manually to avoid using slow getters + this._activeBuffer = this._bufferService.buffer; + this.register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer)); + /** * custom fallback handlers */ @@ -510,9 +517,8 @@ export class InputHandler extends Disposable implements IInputHandler { */ public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise { let result: void | Promise; - let buffer = this._bufferService.buffer; - let cursorStartX = buffer.x; - let cursorStartY = buffer.y; + let cursorStartX = this._activeBuffer.x; + let cursorStartY = this._activeBuffer.y; let start = 0; const wasPaused = this._parseStack.paused; @@ -571,8 +577,7 @@ export class InputHandler extends Disposable implements IInputHandler { } } - buffer = this._bufferService.buffer; - if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { + if (this._activeBuffer.x !== cursorStartX || this._activeBuffer.y !== cursorStartY) { this._onCursorMove.fire(); } @@ -583,20 +588,19 @@ export class InputHandler extends Disposable implements IInputHandler { public print(data: Uint32Array, start: number, end: number): void { let code: number; let chWidth: number; - const buffer = this._bufferService.buffer; const charset = this._charsetService.charset; const screenReaderMode = this._optionsService.options.screenReaderMode; const cols = this._bufferService.cols; const wraparoundMode = this._coreService.decPrivateModes.wraparound; const insertMode = this._coreService.modes.insertMode; const curAttr = this._curAttrData; - let bufferRow = buffer.lines.get(buffer.ybase + buffer.y)!; + let bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; - this._dirtyRowService.markDirty(buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); // handle wide chars: reset start_cell-1 if we would overwrite the second cell of a wide char - if (buffer.x && end - start > 0 && bufferRow.getWidth(buffer.x - 1) === 2) { - bufferRow.setCellFromCodePoint(buffer.x - 1, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); + if (this._activeBuffer.x && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x - 1) === 2) { + bufferRow.setCellFromCodePoint(this._activeBuffer.x - 1, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); } for (let pos = start; pos < end; ++pos) { @@ -621,17 +625,17 @@ export class InputHandler extends Disposable implements IInputHandler { } // insert combining char at last cursor position - // buffer.x should never be 0 for a combining char + // this._activeBuffer.x should never be 0 for a combining char // since they always follow a cell consuming char - // therefore we can test for buffer.x to avoid overflow left - if (!chWidth && buffer.x) { - if (!bufferRow.getWidth(buffer.x - 1)) { + // therefore we can test for this._activeBuffer.x to avoid overflow left + if (!chWidth && this._activeBuffer.x) { + if (!bufferRow.getWidth(this._activeBuffer.x - 1)) { // found empty cell after fullwidth, need to go 2 cells back // it is save to step 2 cells back here // since an empty cell is only set by fullwidth chars - bufferRow.addCodepointToCell(buffer.x - 2, code); + bufferRow.addCodepointToCell(this._activeBuffer.x - 2, code); } else { - bufferRow.addCodepointToCell(buffer.x - 1, code); + bufferRow.addCodepointToCell(this._activeBuffer.x - 1, code); } continue; } @@ -639,31 +643,31 @@ export class InputHandler extends Disposable implements IInputHandler { // goto next line if ch would overflow // NOTE: To avoid costly width checks here, // the terminal does not allow a cols < 2. - if (buffer.x + chWidth - 1 >= cols) { + if (this._activeBuffer.x + chWidth - 1 >= cols) { // autowrap - DECAWM // automatically wraps to the beginning of the next line if (wraparoundMode) { // clear left over cells to the right - while (buffer.x < cols) { - bufferRow.setCellFromCodePoint(buffer.x++, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); + while (this._activeBuffer.x < cols) { + bufferRow.setCellFromCodePoint(this._activeBuffer.x++, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); } - buffer.x = 0; - buffer.y++; - if (buffer.y === buffer.scrollBottom + 1) { - buffer.y--; + this._activeBuffer.x = 0; + this._activeBuffer.y++; + if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) { + this._activeBuffer.y--; this._bufferService.scroll(this._eraseAttrData(), true); } else { - if (buffer.y >= this._bufferService.rows) { - buffer.y = this._bufferService.rows - 1; + if (this._activeBuffer.y >= this._bufferService.rows) { + this._activeBuffer.y = this._bufferService.rows - 1; } // The line already exists (eg. the initial viewport), mark it as a // wrapped line - buffer.lines.get(buffer.ybase + buffer.y)!.isWrapped = true; + this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = true; } // row changed, get it again - bufferRow = buffer.lines.get(buffer.ybase + buffer.y)!; + bufferRow = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; } else { - buffer.x = cols - 1; + this._activeBuffer.x = cols - 1; if (chWidth === 2) { // FIXME: check for xterm behavior // What to do here? We got a wide char that does not fit into last cell @@ -675,7 +679,7 @@ export class InputHandler extends Disposable implements IInputHandler { // insert mode: move characters to right if (insertMode) { // right shift cells according to the width - bufferRow.insertCells(buffer.x, chWidth, buffer.getNullCell(curAttr), curAttr); + bufferRow.insertCells(this._activeBuffer.x, chWidth, this._activeBuffer.getNullCell(curAttr), curAttr); // test last cell - since the last cell has only room for // a halfwidth char any fullwidth shifted there is lost // and will be set to empty cell @@ -685,15 +689,15 @@ export class InputHandler extends Disposable implements IInputHandler { } // write current char to buffer and advance cursor - bufferRow.setCellFromCodePoint(buffer.x++, code, chWidth, curAttr.fg, curAttr.bg, curAttr.extended); + bufferRow.setCellFromCodePoint(this._activeBuffer.x++, code, chWidth, curAttr.fg, curAttr.bg, curAttr.extended); // fullwidth char - also set next cell to placeholder stub and advance cursor // for graphemes bigger than fullwidth we can simply loop to zero - // we already made sure above, that buffer.x + chWidth will not overflow right + // we already made sure above, that this._activeBuffer.x + chWidth will not overflow right if (chWidth > 0) { while (--chWidth) { // other than a regular empty cell a cell following a wide char has no width - bufferRow.setCellFromCodePoint(buffer.x++, 0, 0, curAttr.fg, curAttr.bg, curAttr.extended); + bufferRow.setCellFromCodePoint(this._activeBuffer.x++, 0, 0, curAttr.fg, curAttr.bg, curAttr.extended); } } } @@ -702,7 +706,7 @@ export class InputHandler extends Disposable implements IInputHandler { // - fullwidth + surrogates: reset // - combining: only base char gets carried on (bug in xterm?) if (end - start > 0) { - bufferRow.loadCell(buffer.x - 1, this._workCell); + bufferRow.loadCell(this._activeBuffer.x - 1, this._workCell); if (this._workCell.getWidth() === 2 || this._workCell.getCode() > 0xFFFF) { this._parser.precedingCodepoint = 0; } else if (this._workCell.isCombined()) { @@ -713,11 +717,11 @@ export class InputHandler extends Disposable implements IInputHandler { } // handle wide chars: reset cell to the right if it is second cell of a wide char - if (buffer.x < cols && end - start > 0 && bufferRow.getWidth(buffer.x) === 0 && !bufferRow.hasContent(buffer.x)) { - bufferRow.setCellFromCodePoint(buffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); + if (this._activeBuffer.x < cols && end - start > 0 && bufferRow.getWidth(this._activeBuffer.x) === 0 && !bufferRow.hasContent(this._activeBuffer.x)) { + bufferRow.setCellFromCodePoint(this._activeBuffer.x, 0, 1, curAttr.fg, curAttr.bg, curAttr.extended); } - this._dirtyRowService.markDirty(buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); } /** @@ -781,25 +785,22 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y C0 FF "Form Feed" "\f, \x0C" "Treated as LF." */ public lineFeed(): boolean { - // make buffer local for faster access - const buffer = this._bufferService.buffer; - - this._dirtyRowService.markDirty(buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); if (this._optionsService.options.convertEol) { - buffer.x = 0; + this._activeBuffer.x = 0; } - buffer.y++; - if (buffer.y === buffer.scrollBottom + 1) { - buffer.y--; + this._activeBuffer.y++; + if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) { + this._activeBuffer.y--; this._bufferService.scroll(this._eraseAttrData()); - } else if (buffer.y >= this._bufferService.rows) { - buffer.y = this._bufferService.rows - 1; + } else if (this._activeBuffer.y >= this._bufferService.rows) { + this._activeBuffer.y = this._bufferService.rows - 1; } // If the end of the line is hit, prevent this action from wrapping around to the next line. - if (buffer.x >= this._bufferService.cols) { - buffer.x--; + if (this._activeBuffer.x >= this._bufferService.cols) { + this._activeBuffer.x--; } - this._dirtyRowService.markDirty(buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); this._onLineFeed.fire(); return true; @@ -812,7 +813,7 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y C0 CR "Carriage Return" "\r, \x0D" "Move the cursor to the beginning of the row." */ public carriageReturn(): boolean { - this._bufferService.buffer.x = 0; + this._activeBuffer.x = 0; return true; } @@ -828,13 +829,11 @@ export class InputHandler extends Disposable implements IInputHandler { * with the cursor, thus at the home position (top-leftmost cell) this has no effect. */ public backspace(): boolean { - const buffer = this._bufferService.buffer; - // reverse wrap-around is disabled if (!this._coreService.decPrivateModes.reverseWraparound) { this._restrictCursor(); - if (buffer.x > 0) { - buffer.x--; + if (this._activeBuffer.x > 0) { + this._activeBuffer.x--; } return true; } @@ -844,8 +843,8 @@ export class InputHandler extends Disposable implements IInputHandler { // to be at x=cols to be able to address the last cell of a row by BS this._restrictCursor(this._bufferService.cols); - if (buffer.x > 0) { - buffer.x--; + if (this._activeBuffer.x > 0) { + this._activeBuffer.x--; } else { /** * reverse wrap-around handling: @@ -855,21 +854,21 @@ export class InputHandler extends Disposable implements IInputHandler { * - cannot peek into scrollbuffer * - any cursor movement sequence keeps working as expected */ - if (buffer.x === 0 - && buffer.y > buffer.scrollTop - && buffer.y <= buffer.scrollBottom - && buffer.lines.get(buffer.ybase + buffer.y)?.isWrapped) + if (this._activeBuffer.x === 0 + && this._activeBuffer.y > this._activeBuffer.scrollTop + && this._activeBuffer.y <= this._activeBuffer.scrollBottom + && this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)?.isWrapped) { - buffer.lines.get(buffer.ybase + buffer.y)!.isWrapped = false; - buffer.y--; - buffer.x = this._bufferService.cols - 1; + this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false; + this._activeBuffer.y--; + this._activeBuffer.x = this._bufferService.cols - 1; // find last taken cell - last cell can have 3 different states: // - hasContent(true) + hasWidth(1): narrow char - we are done // - hasWidth(0): second part of wide char - we are done // - hasContent(false) + hasWidth(1): empty cell due to early wrapping wide char, go one cell further back - const line = buffer.lines.get(buffer.ybase + buffer.y)!; - if (line.hasWidth(buffer.x) && !line.hasContent(buffer.x)) { - buffer.x--; + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!; + if (line.hasWidth(this._activeBuffer.x) && !line.hasContent(this._activeBuffer.x)) { + this._activeBuffer.x--; // We do this only once, since width=1 + hasContent=false currently happens only once before // early wrapping of a wide char. // This needs to be fixed once we support graphemes taking more than 2 cells. @@ -887,13 +886,13 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y C0 HT "Horizontal Tabulation" "\t, \x09" "Move the cursor to the next character tab stop." */ public tab(): boolean { - if (this._bufferService.buffer.x >= this._bufferService.cols) { + if (this._activeBuffer.x >= this._bufferService.cols) { return true; } - const originalX = this._bufferService.buffer.x; - this._bufferService.buffer.x = this._bufferService.buffer.nextStop(); + const originalX = this._activeBuffer.x; + this._activeBuffer.x = this._activeBuffer.nextStop(); if (this._optionsService.options.screenReaderMode) { - this._onA11yTab.fire(this._bufferService.buffer.x - originalX); + this._onA11yTab.fire(this._activeBuffer.x - originalX); } return true; } @@ -926,27 +925,27 @@ export class InputHandler extends Disposable implements IInputHandler { * Restrict cursor to viewport size / scroll margin (origin mode). */ private _restrictCursor(maxCol: number = this._bufferService.cols - 1): void { - this._bufferService.buffer.x = Math.min(maxCol, Math.max(0, this._bufferService.buffer.x)); - this._bufferService.buffer.y = this._coreService.decPrivateModes.origin - ? Math.min(this._bufferService.buffer.scrollBottom, Math.max(this._bufferService.buffer.scrollTop, this._bufferService.buffer.y)) - : Math.min(this._bufferService.rows - 1, Math.max(0, this._bufferService.buffer.y)); - this._dirtyRowService.markDirty(this._bufferService.buffer.y); + this._activeBuffer.x = Math.min(maxCol, Math.max(0, this._activeBuffer.x)); + this._activeBuffer.y = this._coreService.decPrivateModes.origin + ? Math.min(this._activeBuffer.scrollBottom, Math.max(this._activeBuffer.scrollTop, this._activeBuffer.y)) + : Math.min(this._bufferService.rows - 1, Math.max(0, this._activeBuffer.y)); + this._dirtyRowService.markDirty(this._activeBuffer.y); } /** * Set absolute cursor position. */ private _setCursor(x: number, y: number): void { - this._dirtyRowService.markDirty(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); if (this._coreService.decPrivateModes.origin) { - this._bufferService.buffer.x = x; - this._bufferService.buffer.y = this._bufferService.buffer.scrollTop + y; + this._activeBuffer.x = x; + this._activeBuffer.y = this._activeBuffer.scrollTop + y; } else { - this._bufferService.buffer.x = x; - this._bufferService.buffer.y = y; + this._activeBuffer.x = x; + this._activeBuffer.y = y; } this._restrictCursor(); - this._dirtyRowService.markDirty(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); } /** @@ -956,7 +955,7 @@ export class InputHandler extends Disposable implements IInputHandler { // for relative changes we have to make sure we are within 0 .. cols/rows - 1 // before calculating the new position this._restrictCursor(); - this._setCursor(this._bufferService.buffer.x + x, this._bufferService.buffer.y + y); + this._setCursor(this._activeBuffer.x + x, this._activeBuffer.y + y); } /** @@ -968,7 +967,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public cursorUp(params: IParams): boolean { // stop at scrollTop - const diffToTop = this._bufferService.buffer.y - this._bufferService.buffer.scrollTop; + const diffToTop = this._activeBuffer.y - this._activeBuffer.scrollTop; if (diffToTop >= 0) { this._moveCursor(0, -Math.min(diffToTop, params.params[0] || 1)); } else { @@ -986,7 +985,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public cursorDown(params: IParams): boolean { // stop at scrollBottom - const diffToBottom = this._bufferService.buffer.scrollBottom - this._bufferService.buffer.y; + const diffToBottom = this._activeBuffer.scrollBottom - this._activeBuffer.y; if (diffToBottom >= 0) { this._moveCursor(0, Math.min(diffToBottom, params.params[0] || 1)); } else { @@ -1027,7 +1026,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public cursorNextLine(params: IParams): boolean { this.cursorDown(params); - this._bufferService.buffer.x = 0; + this._activeBuffer.x = 0; return true; } @@ -1041,7 +1040,7 @@ export class InputHandler extends Disposable implements IInputHandler { */ public cursorPrecedingLine(params: IParams): boolean { this.cursorUp(params); - this._bufferService.buffer.x = 0; + this._activeBuffer.x = 0; return true; } @@ -1052,7 +1051,7 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y CSI CHA "Cursor Horizontal Absolute" "CSI Ps G" "Move cursor to `Ps`-th column of the active row (default=1)." */ public cursorCharAbsolute(params: IParams): boolean { - this._setCursor((params.params[0] || 1) - 1, this._bufferService.buffer.y); + this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y); return true; } @@ -1083,7 +1082,7 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y CSI HPA "Horizontal Position Absolute" "CSI Ps ` " "Same as CHA." */ public charPosAbsolute(params: IParams): boolean { - this._setCursor((params.params[0] || 1) - 1, this._bufferService.buffer.y); + this._setCursor((params.params[0] || 1) - 1, this._activeBuffer.y); return true; } @@ -1105,7 +1104,7 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y CSI VPA "Vertical Position Absolute" "CSI Ps d" "Move cursor to `Ps`-th row (default=1)." */ public linePosAbsolute(params: IParams): boolean { - this._setCursor(this._bufferService.buffer.x, (params.params[0] || 1) - 1); + this._setCursor(this._activeBuffer.x, (params.params[0] || 1) - 1); return true; } @@ -1148,9 +1147,9 @@ export class InputHandler extends Disposable implements IInputHandler { public tabClear(params: IParams): boolean { const param = params.params[0]; if (param === 0) { - delete this._bufferService.buffer.tabs[this._bufferService.buffer.x]; + delete this._activeBuffer.tabs[this._activeBuffer.x]; } else if (param === 3) { - this._bufferService.buffer.tabs = {}; + this._activeBuffer.tabs = {}; } return true; } @@ -1162,12 +1161,12 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y CSI CHT "Cursor Horizontal Tabulation" "CSI Ps I" "Move cursor `Ps` times tabs forward (default=1)." */ public cursorForwardTab(params: IParams): boolean { - if (this._bufferService.buffer.x >= this._bufferService.cols) { + if (this._activeBuffer.x >= this._bufferService.cols) { return true; } let param = params.params[0] || 1; while (param--) { - this._bufferService.buffer.x = this._bufferService.buffer.nextStop(); + this._activeBuffer.x = this._activeBuffer.nextStop(); } return true; } @@ -1178,16 +1177,13 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y CSI CBT "Cursor Backward Tabulation" "CSI Ps Z" "Move cursor `Ps` tabs backward (default=1)." */ public cursorBackwardTab(params: IParams): boolean { - if (this._bufferService.buffer.x >= this._bufferService.cols) { + if (this._activeBuffer.x >= this._bufferService.cols) { return true; } let param = params.params[0] || 1; - // make buffer local for faster access - const buffer = this._bufferService.buffer; - while (param--) { - buffer.x = buffer.prevStop(); + this._activeBuffer.x = this._activeBuffer.prevStop(); } return true; } @@ -1201,11 +1197,11 @@ 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._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; line.replaceCells( start, end, - this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData() ); if (clearWrap) { @@ -1219,8 +1215,8 @@ 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)!; - line.fill(this._bufferService.buffer.getNullCell(this._eraseAttrData())); + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.fill(this._activeBuffer.getNullCell(this._eraseAttrData())); line.isWrapped = false; } @@ -1253,22 +1249,22 @@ export class InputHandler extends Disposable implements IInputHandler { let j; switch (params.params[0]) { case 0: - j = this._bufferService.buffer.y; + j = this._activeBuffer.y; this._dirtyRowService.markDirty(j); - this._eraseInBufferLine(j++, this._bufferService.buffer.x, this._bufferService.cols, this._bufferService.buffer.x === 0); + this._eraseInBufferLine(j++, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0); for (; j < this._bufferService.rows; j++) { this._resetBufferLine(j); } this._dirtyRowService.markDirty(j); break; case 1: - j = this._bufferService.buffer.y; + j = this._activeBuffer.y; this._dirtyRowService.markDirty(j); // Deleted front part of line and everything before. This line will no longer be wrapped. - this._eraseInBufferLine(j, 0, this._bufferService.buffer.x + 1, true); - if (this._bufferService.buffer.x + 1 >= this._bufferService.cols) { + this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true); + if (this._activeBuffer.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._activeBuffer.lines.get(j + 1)!.isWrapped = false; } while (j--) { this._resetBufferLine(j); @@ -1285,11 +1281,11 @@ export class InputHandler extends Disposable implements IInputHandler { break; case 3: // Clear scrollback (everything not in viewport) - const scrollBackSize = this._bufferService.buffer.lines.length - this._bufferService.rows; + const scrollBackSize = this._activeBuffer.lines.length - this._bufferService.rows; if (scrollBackSize > 0) { - this._bufferService.buffer.lines.trimStart(scrollBackSize); - this._bufferService.buffer.ybase = Math.max(this._bufferService.buffer.ybase - scrollBackSize, 0); - this._bufferService.buffer.ydisp = Math.max(this._bufferService.buffer.ydisp - scrollBackSize, 0); + this._activeBuffer.lines.trimStart(scrollBackSize); + this._activeBuffer.ybase = Math.max(this._activeBuffer.ybase - scrollBackSize, 0); + this._activeBuffer.ydisp = Math.max(this._activeBuffer.ydisp - scrollBackSize, 0); // Force a scroll event to refresh viewport this._onScroll.fire(0); } @@ -1324,16 +1320,16 @@ export class InputHandler extends Disposable implements IInputHandler { this._restrictCursor(this._bufferService.cols); switch (params.params[0]) { case 0: - this._eraseInBufferLine(this._bufferService.buffer.y, this._bufferService.buffer.x, this._bufferService.cols); + this._eraseInBufferLine(this._activeBuffer.y, this._activeBuffer.x, this._bufferService.cols); break; case 1: - this._eraseInBufferLine(this._bufferService.buffer.y, 0, this._bufferService.buffer.x + 1); + this._eraseInBufferLine(this._activeBuffer.y, 0, this._activeBuffer.x + 1); break; case 2: - this._eraseInBufferLine(this._bufferService.buffer.y, 0, this._bufferService.cols); + this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols); break; } - this._dirtyRowService.markDirty(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); return true; } @@ -1350,26 +1346,23 @@ export class InputHandler extends Disposable implements IInputHandler { this._restrictCursor(); let param = params.params[0] || 1; - // make buffer local for faster access - const buffer = this._bufferService.buffer; - - if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { return true; } - const row: number = buffer.ybase + buffer.y; + const row: number = this._activeBuffer.ybase + this._activeBuffer.y; - const scrollBottomRowsOffset = this._bufferService.rows - 1 - buffer.scrollBottom; - const scrollBottomAbsolute = this._bufferService.rows - 1 + buffer.ybase - scrollBottomRowsOffset + 1; + const scrollBottomRowsOffset = this._bufferService.rows - 1 - this._activeBuffer.scrollBottom; + const scrollBottomAbsolute = this._bufferService.rows - 1 + this._activeBuffer.ybase - scrollBottomRowsOffset + 1; while (param--) { // test: echo -e '\e[44m\e[1L\e[0m' // blankLine(true) - xterm/linux behavior - buffer.lines.splice(scrollBottomAbsolute - 1, 1); - buffer.lines.splice(row, 0, buffer.getBlankLine(this._eraseAttrData())); + this._activeBuffer.lines.splice(scrollBottomAbsolute - 1, 1); + this._activeBuffer.lines.splice(row, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); } - this._dirtyRowService.markRangeDirty(buffer.y, buffer.scrollBottom); - buffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? + this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom); + this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? return true; } @@ -1386,27 +1379,24 @@ export class InputHandler extends Disposable implements IInputHandler { this._restrictCursor(); let param = params.params[0] || 1; - // make buffer local for faster access - const buffer = this._bufferService.buffer; - - if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { return true; } - const row: number = buffer.ybase + buffer.y; + const row: number = this._activeBuffer.ybase + this._activeBuffer.y; let j: number; - j = this._bufferService.rows - 1 - buffer.scrollBottom; - j = this._bufferService.rows - 1 + buffer.ybase - j; + j = this._bufferService.rows - 1 - this._activeBuffer.scrollBottom; + j = this._bufferService.rows - 1 + this._activeBuffer.ybase - j; while (param--) { // test: echo -e '\e[44m\e[1M\e[0m' // blankLine(true) - xterm/linux behavior - buffer.lines.splice(row, 1); - buffer.lines.splice(j, 0, buffer.getBlankLine(this._eraseAttrData())); + this._activeBuffer.lines.splice(row, 1); + this._activeBuffer.lines.splice(j, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); } - this._dirtyRowService.markRangeDirty(buffer.y, buffer.scrollBottom); - buffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? + this._dirtyRowService.markRangeDirty(this._activeBuffer.y, this._activeBuffer.scrollBottom); + this._activeBuffer.x = 0; // see https://vt100.net/docs/vt220-rm/chapter4.html - vt220 only? return true; } @@ -1423,15 +1413,15 @@ export class InputHandler extends Disposable implements IInputHandler { */ public insertChars(params: IParams): boolean { this._restrictCursor(); - const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + this._bufferService.buffer.y); + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y); if (line) { line.insertCells( - this._bufferService.buffer.x, + this._activeBuffer.x, params.params[0] || 1, - this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData() ); - this._dirtyRowService.markDirty(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); } return true; } @@ -1449,15 +1439,15 @@ export class InputHandler extends Disposable implements IInputHandler { */ public deleteChars(params: IParams): boolean { this._restrictCursor(); - const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + this._bufferService.buffer.y); + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y); if (line) { line.deleteCells( - this._bufferService.buffer.x, + this._activeBuffer.x, params.params[0] || 1, - this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData() ); - this._dirtyRowService.markDirty(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); } return true; } @@ -1473,14 +1463,11 @@ export class InputHandler extends Disposable implements IInputHandler { public scrollUp(params: IParams): boolean { let param = params.params[0] || 1; - // make buffer local for faster access - const buffer = this._bufferService.buffer; - while (param--) { - buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(this._eraseAttrData())); + this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 1); + this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 0, this._activeBuffer.getBlankLine(this._eraseAttrData())); } - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1492,14 +1479,11 @@ export class InputHandler extends Disposable implements IInputHandler { public scrollDown(params: IParams): boolean { let param = params.params[0] || 1; - // make buffer local for faster access - const buffer = this._bufferService.buffer; - while (param--) { - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA)); + this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollBottom, 1); + this._activeBuffer.lines.splice(this._activeBuffer.ybase + this._activeBuffer.scrollTop, 0, this._activeBuffer.getBlankLine(DEFAULT_ATTR_DATA)); } - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1522,17 +1506,16 @@ export class InputHandler extends Disposable implements IInputHandler { * SL has no effect outside of the scroll margins. */ public scrollLeft(params: IParams): boolean { - const buffer = this._bufferService.buffer; - if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { return true; } const param = params.params[0] || 1; - for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { - const line = buffer.lines.get(buffer.ybase + y)!; - line.deleteCells(0, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1556,17 +1539,16 @@ export class InputHandler extends Disposable implements IInputHandler { * SL has no effect outside of the scroll margins. */ public scrollRight(params: IParams): boolean { - const buffer = this._bufferService.buffer; - if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { return true; } const param = params.params[0] || 1; - for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { - const line = buffer.lines.get(buffer.ybase + y)!; - line.insertCells(0, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1580,17 +1562,16 @@ export class InputHandler extends Disposable implements IInputHandler { * DECIC has no effect outside the scrolling margins. */ public insertColumns(params: IParams): boolean { - const buffer = this._bufferService.buffer; - if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { return true; } 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)!; - line.insertCells(buffer.x, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1604,17 +1585,16 @@ export class InputHandler extends Disposable implements IInputHandler { * DECDC has no effect outside the scrolling margins. */ public deleteColumns(params: IParams): boolean { - const buffer = this._bufferService.buffer; - if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) { + if (this._activeBuffer.y > this._activeBuffer.scrollBottom || this._activeBuffer.y < this._activeBuffer.scrollTop) { return true; } const param = params.params[0] || 1; - for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) { - const line = buffer.lines.get(buffer.ybase + y)!; - line.deleteCells(buffer.x, param, buffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); + for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) { + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!; + line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData()); line.isWrapped = false; } - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); return true; } @@ -1628,15 +1608,15 @@ export class InputHandler extends Disposable implements IInputHandler { */ public eraseChars(params: IParams): boolean { this._restrictCursor(); - const line = this._bufferService.buffer.lines.get(this._bufferService.buffer.ybase + this._bufferService.buffer.y); + const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y); if (line) { line.replaceCells( - this._bufferService.buffer.x, - this._bufferService.buffer.x + (params.params[0] || 1), - this._bufferService.buffer.getNullCell(this._eraseAttrData()), + this._activeBuffer.x, + this._activeBuffer.x + (params.params[0] || 1), + this._activeBuffer.getNullCell(this._eraseAttrData()), this._eraseAttrData() ); - this._dirtyRowService.markDirty(this._bufferService.buffer.y); + this._dirtyRowService.markDirty(this._activeBuffer.y); } return true; } @@ -2573,8 +2553,8 @@ export class InputHandler extends Disposable implements IInputHandler { break; case 6: // cursor position - const y = this._bufferService.buffer.y + 1; - const x = this._bufferService.buffer.x + 1; + const y = this._activeBuffer.y + 1; + const x = this._activeBuffer.x + 1; this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`); break; } @@ -2588,8 +2568,8 @@ export class InputHandler extends Disposable implements IInputHandler { switch (params.params[0]) { case 6: // cursor position - const y = this._bufferService.buffer.y + 1; - const x = this._bufferService.buffer.x + 1; + const y = this._activeBuffer.y + 1; + const x = this._activeBuffer.x + 1; this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`); break; case 15: @@ -2634,18 +2614,18 @@ export class InputHandler extends Disposable implements IInputHandler { public softReset(params: IParams): boolean { this._coreService.isCursorHidden = false; this._onRequestSyncScrollBar.fire(); - this._bufferService.buffer.scrollTop = 0; - this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1; + this._activeBuffer.scrollTop = 0; + this._activeBuffer.scrollBottom = this._bufferService.rows - 1; this._curAttrData = DEFAULT_ATTR_DATA.clone(); this._coreService.reset(); this._charsetService.reset(); // reset DECSC data - this._bufferService.buffer.savedX = 0; - this._bufferService.buffer.savedY = this._bufferService.buffer.ybase; - this._bufferService.buffer.savedCurAttrData.fg = this._curAttrData.fg; - this._bufferService.buffer.savedCurAttrData.bg = this._curAttrData.bg; - this._bufferService.buffer.savedCharset = this._charsetService.charset; + this._activeBuffer.savedX = 0; + this._activeBuffer.savedY = this._activeBuffer.ybase; + this._activeBuffer.savedCurAttrData.fg = this._curAttrData.fg; + this._activeBuffer.savedCurAttrData.bg = this._curAttrData.bg; + this._activeBuffer.savedCharset = this._charsetService.charset; // reset DECOM this._coreService.decPrivateModes.origin = false; @@ -2708,8 +2688,8 @@ export class InputHandler extends Disposable implements IInputHandler { } if (bottom > top) { - this._bufferService.buffer.scrollTop = top - 1; - this._bufferService.buffer.scrollBottom = bottom - 1; + this._activeBuffer.scrollTop = top - 1; + this._activeBuffer.scrollBottom = bottom - 1; this._setCursor(0, 0); } return true; @@ -2804,11 +2784,11 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y ESC SC "Save Cursor" "ESC 7" "Save cursor position, charmap and text attributes." */ public saveCursor(params?: IParams): boolean { - this._bufferService.buffer.savedX = this._bufferService.buffer.x; - this._bufferService.buffer.savedY = this._bufferService.buffer.ybase + this._bufferService.buffer.y; - this._bufferService.buffer.savedCurAttrData.fg = this._curAttrData.fg; - this._bufferService.buffer.savedCurAttrData.bg = this._curAttrData.bg; - this._bufferService.buffer.savedCharset = this._charsetService.charset; + this._activeBuffer.savedX = this._activeBuffer.x; + this._activeBuffer.savedY = this._activeBuffer.ybase + this._activeBuffer.y; + this._activeBuffer.savedCurAttrData.fg = this._curAttrData.fg; + this._activeBuffer.savedCurAttrData.bg = this._curAttrData.bg; + this._activeBuffer.savedCharset = this._charsetService.charset; return true; } @@ -2822,13 +2802,13 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y ESC RC "Restore Cursor" "ESC 8" "Restore cursor position, charmap and text attributes." */ public restoreCursor(params?: IParams): boolean { - this._bufferService.buffer.x = this._bufferService.buffer.savedX || 0; - this._bufferService.buffer.y = Math.max(this._bufferService.buffer.savedY - this._bufferService.buffer.ybase, 0); - this._curAttrData.fg = this._bufferService.buffer.savedCurAttrData.fg; - this._curAttrData.bg = this._bufferService.buffer.savedCurAttrData.bg; + this._activeBuffer.x = this._activeBuffer.savedX || 0; + this._activeBuffer.y = Math.max(this._activeBuffer.savedY - this._activeBuffer.ybase, 0); + this._curAttrData.fg = this._activeBuffer.savedCurAttrData.fg; + this._curAttrData.bg = this._activeBuffer.savedCurAttrData.bg; this._charsetService.charset = (this as any)._savedCharset; - if (this._bufferService.buffer.savedCharset) { - this._charsetService.charset = this._bufferService.buffer.savedCharset; + if (this._activeBuffer.savedCharset) { + this._charsetService.charset = this._activeBuffer.savedCharset; } this._restrictCursor(); return true; @@ -2910,7 +2890,7 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y ESC NEL "Next Line" "ESC E" "Move the cursor to the beginning of the next row." */ public nextLine(): boolean { - this._bufferService.buffer.x = 0; + this._activeBuffer.x = 0; this.index(); return true; } @@ -2990,13 +2970,12 @@ export class InputHandler extends Disposable implements IInputHandler { */ public index(): boolean { this._restrictCursor(); - const buffer = this._bufferService.buffer; - this._bufferService.buffer.y++; - if (buffer.y === buffer.scrollBottom + 1) { - buffer.y--; + this._activeBuffer.y++; + if (this._activeBuffer.y === this._activeBuffer.scrollBottom + 1) { + this._activeBuffer.y--; this._bufferService.scroll(this._eraseAttrData()); - } else if (buffer.y >= this._bufferService.rows) { - buffer.y = this._bufferService.rows - 1; + } else if (this._activeBuffer.y >= this._bufferService.rows) { + this._activeBuffer.y = this._bufferService.rows - 1; } this._restrictCursor(); return true; @@ -3013,7 +2992,7 @@ export class InputHandler extends Disposable implements IInputHandler { * @vt: #Y ESC HTS "Horizontal Tabulation Set" "ESC H" "Places a tab stop at the current cursor position." */ public tabSet(): boolean { - this._bufferService.buffer.tabs[this._bufferService.buffer.x] = true; + this._activeBuffer.tabs[this._activeBuffer.x] = true; return true; } @@ -3028,17 +3007,16 @@ export class InputHandler extends Disposable implements IInputHandler { */ public reverseIndex(): boolean { this._restrictCursor(); - const buffer = this._bufferService.buffer; - if (buffer.y === buffer.scrollTop) { + if (this._activeBuffer.y === this._activeBuffer.scrollTop) { // possibly move the code below to term.reverseScroll(); // 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.ybase + buffer.y, scrollRegionHeight, 1); - buffer.lines.set(buffer.ybase + buffer.y, buffer.getBlankLine(this._eraseAttrData())); - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + const scrollRegionHeight = this._activeBuffer.scrollBottom - this._activeBuffer.scrollTop; + this._activeBuffer.lines.shiftElements(this._activeBuffer.ybase + this._activeBuffer.y, scrollRegionHeight, 1); + this._activeBuffer.lines.set(this._activeBuffer.ybase + this._activeBuffer.y, this._activeBuffer.getBlankLine(this._eraseAttrData())); + this._dirtyRowService.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom); } else { - buffer.y--; + this._activeBuffer.y--; this._restrictCursor(); // quickfix to not run out of bounds } return true; @@ -3099,12 +3077,11 @@ export class InputHandler extends Disposable implements IInputHandler { cell.fg = this._curAttrData.fg; cell.bg = this._curAttrData.bg; - const buffer = this._bufferService.buffer; this._setCursor(0, 0); for (let yOffset = 0; yOffset < this._bufferService.rows; ++yOffset) { - const row = buffer.ybase + buffer.y + yOffset; - const line = buffer.lines.get(row); + const row = this._activeBuffer.ybase + this._activeBuffer.y + yOffset; + const line = this._activeBuffer.lines.get(row); if (line) { line.fill(cell); line.isWrapped = false; diff --git a/src/common/buffer/BufferSet.ts b/src/common/buffer/BufferSet.ts index b74c4eac..de220e8f 100644 --- a/src/common/buffer/BufferSet.ts +++ b/src/common/buffer/BufferSet.ts @@ -42,6 +42,10 @@ export class BufferSet extends Disposable implements IBufferSet { // See http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-The-Alternate-Screen-Buffer this._alt = new Buffer(false, this._optionsService, this._bufferService); this._activeBuffer = this._normal; + this._onBufferActivate.fire({ + activeBuffer: this._normal, + inactiveBuffer: this._alt + }); this.setupTabStops(); } diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 99594d22..c8c5f273 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -36,8 +36,8 @@ export class BufferService extends Disposable implements IBufferService { @IOptionsService private _optionsService: IOptionsService ) { super(); - this.cols = Math.max(_optionsService.options.cols, MINIMUM_COLS); - this.rows = Math.max(_optionsService.options.rows, MINIMUM_ROWS); + this.cols = Math.max(_optionsService.options.cols || 0, MINIMUM_COLS); + this.rows = Math.max(_optionsService.options.rows || 0, MINIMUM_ROWS); this.buffers = new BufferSet(_optionsService, this); } diff --git a/src/common/services/OptionsService.test.ts b/src/common/services/OptionsService.test.ts index c289b5be..8675b6b9 100644 --- a/src/common/services/OptionsService.test.ts +++ b/src/common/services/OptionsService.test.ts @@ -15,6 +15,16 @@ describe('OptionsService', () => { afterEach(() => { console.error = originalError; }); + it('uses default value if invalid constructor option values passed for cols/rows', () => { + const optionsService = new OptionsService({ cols: undefined, rows: undefined }); + assert.equal(optionsService.getOption('rows'), DEFAULT_OPTIONS.rows); + assert.equal(optionsService.getOption('cols'), DEFAULT_OPTIONS.cols); + }); + it('uses values from constructor option values if correctly passed', () => { + const optionsService = new OptionsService({ cols: 80, rows: 25 }); + assert.equal(optionsService.getOption('rows'), 25); + assert.equal(optionsService.getOption('cols'), 80); + }); it('uses default value if invalid constructor option value passed', () => { assert.equal(new OptionsService({tabStopWidth: 0}).getOption('tabStopWidth'), DEFAULT_OPTIONS.tabStopWidth); }); diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 5add8283..e9dcaa6a 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -149,6 +149,11 @@ export class OptionsService implements IOptionsService { if (value <= 0) { throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`); } + case 'rows': + case 'cols': + if (!value && value !== 0) { + throw new Error(`${key} must be numeric, value: ${value}`); + } break; } return value; diff --git a/test/api/CharWidth.api.ts b/test/api/CharWidth.api.ts index d7cea109..83067149 100644 --- a/test/api/CharWidth.api.ts +++ b/test/api/CharWidth.api.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { pollFor, openTerminal, getBrowserType } from './TestUtils'; +import { pollFor, openTerminal, launchBrowser } from './TestUtils'; import { Browser, Page } from 'playwright'; const APP = 'http://127.0.0.1:3001/test'; @@ -15,10 +15,7 @@ const height = 600; describe('CharWidth Integration Tests', function(): void { before(async function(): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts index a695abdc..54ee6957 100644 --- a/test/api/InputHandler.api.ts +++ b/test/api/InputHandler.api.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { pollFor, openTerminal, getBrowserType } from './TestUtils'; +import { pollFor, openTerminal, getBrowserType, launchBrowser } from './TestUtils'; import { Browser, Page } from 'playwright'; import { IRenderDimensions } from 'browser/renderer/Types'; @@ -21,9 +21,7 @@ describe('InputHandler Integration Tests', function(): void { before(async function(): Promise { const browserType = getBrowserType(); isChromium = browserType.name() === 'chromium'; - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); diff --git a/test/api/MouseTracking.api.ts b/test/api/MouseTracking.api.ts index 3df21a90..ebeb680f 100644 --- a/test/api/MouseTracking.api.ts +++ b/test/api/MouseTracking.api.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { pollFor, writeSync, openTerminal, getBrowserType } from './TestUtils'; +import { pollFor, writeSync, openTerminal, getBrowserType, launchBrowser } from './TestUtils'; import { Browser, Page } from 'playwright'; const APP = 'http://127.0.0.1:3001/test'; @@ -211,9 +211,7 @@ describe('Mouse Tracking Tests', async () => { const itMouse = isChromium ? it : it.skip; before(async function(): Promise { - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); }); diff --git a/test/api/Parser.api.ts b/test/api/Parser.api.ts index 0ef57cf1..ada28adf 100644 --- a/test/api/Parser.api.ts +++ b/test/api/Parser.api.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { writeSync, openTerminal, getBrowserType } from './TestUtils'; +import { writeSync, openTerminal, launchBrowser } from './TestUtils'; import { Browser, Page } from 'playwright'; const APP = 'http://127.0.0.1:3001/test'; @@ -16,10 +16,7 @@ const height = 600; describe('Parser Integration Tests', function (): void { before(async function (): Promise { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index 1599f3a2..00598e47 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { pollFor, timeout, writeSync, openTerminal, getBrowserType } from './TestUtils'; +import { pollFor, timeout, writeSync, openTerminal, launchBrowser } from './TestUtils'; import { Browser, Page } from 'playwright'; const APP = 'http://127.0.0.1:3001/test'; @@ -16,10 +16,7 @@ const height = 600; describe('API Integration Tests', function(): void { before(async () => { - const browserType = getBrowserType(); - browser = await browserType.launch({ - headless: process.argv.indexOf('--headless') !== -1 - }); + browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); }); diff --git a/test/api/TestUtils.ts b/test/api/TestUtils.ts index 2b1e8828..4fa98d43 100644 --- a/test/api/TestUtils.ts +++ b/test/api/TestUtils.ts @@ -67,3 +67,17 @@ export function getBrowserType(): playwright.BrowserType = { + headless: process.argv.includes('--headless'), + } + + const index = process.argv.indexOf('--executablePath'); + if(index > 0 && process.argv.length > index + 1 && typeof process.argv[index + 1] === 'string') { + options.executablePath = process.argv[index + 1]; + } + + return browserType.launch(options); +} diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 13a32126..84b715e8 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -84,7 +84,7 @@ declare module 'xterm-headless' { * line height and letter spacing is used. Note that this doesn't work with the DOM renderer * which renders all characters using the font. The default is true. */ - customGlyphs?: boolean; + customGlyphs?: boolean; /** * Whether input should be disabled. @@ -1085,18 +1085,20 @@ declare module 'xterm-headless' { /** Whether the cell has the bold attribute (CSI 1 m). */ isBold(): number; - /** Whether the cell has the inverse attribute (CSI 3 m). */ + /** Whether the cell has the italic attribute (CSI 3 m). */ isItalic(): number; - /** Whether the cell has the inverse attribute (CSI 2 m). */ + /** Whether the cell has the dim attribute (CSI 2 m). */ isDim(): number; /** Whether the cell has the underline attribute (CSI 4 m). */ isUnderline(): number; - /** Whether the cell has the inverse attribute (CSI 5 m). */ + /** Whether the cell has the blink attribute (CSI 5 m). */ isBlink(): number; /** Whether the cell has the inverse attribute (CSI 7 m). */ isInverse(): number; - /** Whether the cell has the inverse attribute (CSI 8 m). */ + /** Whether the cell has the invisible attribute (CSI 8 m). */ isInvisible(): number; + /** Whether the cell has the strikethrough attribute (CSI 9 m). */ + isStrikethrough(): number; /** Whether the cell is using the RGB foreground color mode. */ isFgRGB(): boolean; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index ba2be988..d7c2effc 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1056,6 +1056,14 @@ declare module 'xterm' { */ refresh(start: number, end: number): void; + /** + * Clears the texture atlas of the canvas renderer if it's active. Doing this will force a + * redraw of all glyphs which can workaround issues causing the texture to become corrupt, for + * example Chromium/Nvidia has an issue where the texture gets messed up when resuming the OS + * from sleep. + */ + clearTextureAtlas(): void; + /** * Perform a full reset (RIS, aka '\x1bc'). */ @@ -1444,18 +1452,20 @@ declare module 'xterm' { /** Whether the cell has the bold attribute (CSI 1 m). */ isBold(): number; - /** Whether the cell has the inverse attribute (CSI 3 m). */ + /** Whether the cell has the italic attribute (CSI 3 m). */ isItalic(): number; - /** Whether the cell has the inverse attribute (CSI 2 m). */ + /** Whether the cell has the dim attribute (CSI 2 m). */ isDim(): number; /** Whether the cell has the underline attribute (CSI 4 m). */ isUnderline(): number; - /** Whether the cell has the inverse attribute (CSI 5 m). */ + /** Whether the cell has the blink attribute (CSI 5 m). */ isBlink(): number; /** Whether the cell has the inverse attribute (CSI 7 m). */ isInverse(): number; - /** Whether the cell has the inverse attribute (CSI 8 m). */ + /** Whether the cell has the invisible attribute (CSI 8 m). */ isInvisible(): number; + /** Whether the cell has the strikethrough attribute (CSI 9 m). */ + isStrikethrough(): number; /** Whether the cell is using the RGB foreground color mode. */ isFgRGB(): boolean; diff --git a/yarn.lock b/yarn.lock index 571f8a90..cf92d8f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1329,7 +1329,14 @@ core-util-is@1.0.2, core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= -cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -3909,7 +3916,7 @@ source-map-loader@^2.0.1: iconv-lite "^0.6.2" source-map-js "^0.6.2" -source-map-support@~0.5.19: +source-map-support@^0.5.19, source-map-support@~0.5.19: version "0.5.19" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==