From 2571a0654f87e4663ac8eb4cbddaa6dd4075d15a Mon Sep 17 00:00:00 2001 From: javacs3 Date: Sun, 7 Jul 2019 22:59:15 +0800 Subject: [PATCH 01/47] add SerializeAddon --- addons/xterm-addon-serialize/.gitignore | 2 + addons/xterm-addon-serialize/.npmignore | 5 + addons/xterm-addon-serialize/package.json | 20 +++ .../src/SerializeAddon.api.ts | 134 ++++++++++++++++++ .../src/SerializeAddon.ts | 42 ++++++ .../xterm-addon-serialize/src/tsconfig.json | 19 +++ .../typings/xterm-addon-serialize.d.ts | 30 ++++ .../xterm-addon-serialize/webpack.config.js | 31 ++++ demo/client.ts | 3 + package.json | 1 + tsconfig.all.json | 3 +- 11 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 addons/xterm-addon-serialize/.gitignore create mode 100644 addons/xterm-addon-serialize/.npmignore create mode 100644 addons/xterm-addon-serialize/package.json create mode 100644 addons/xterm-addon-serialize/src/SerializeAddon.api.ts create mode 100644 addons/xterm-addon-serialize/src/SerializeAddon.ts create mode 100644 addons/xterm-addon-serialize/src/tsconfig.json create mode 100644 addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts create mode 100644 addons/xterm-addon-serialize/webpack.config.js diff --git a/addons/xterm-addon-serialize/.gitignore b/addons/xterm-addon-serialize/.gitignore new file mode 100644 index 00000000..3063f07d --- /dev/null +++ b/addons/xterm-addon-serialize/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules diff --git a/addons/xterm-addon-serialize/.npmignore b/addons/xterm-addon-serialize/.npmignore new file mode 100644 index 00000000..1c794445 --- /dev/null +++ b/addons/xterm-addon-serialize/.npmignore @@ -0,0 +1,5 @@ +**/*.api.js +**/*.api.ts +tsconfig.json +.yarnrc +webpack.config.js diff --git a/addons/xterm-addon-serialize/package.json b/addons/xterm-addon-serialize/package.json new file mode 100644 index 00000000..c8a66c4b --- /dev/null +++ b/addons/xterm-addon-serialize/package.json @@ -0,0 +1,20 @@ +{ + "name": "xterm-addon-serialize", + "version": "0.1.0", + "author": { + "name": "The xterm.js authors", + "url": "https://xtermjs.org/" + }, + "main": "lib/xterm-addon-serialize.js", + "types": "typings/xterm-addon-serialize.d.ts", + "license": "MIT", + "scripts": { + "build": "../../node_modules/.bin/tsc -p src", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package" + }, + "peerDependencies": { + "xterm": "^3.14.0" + } +} diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts new file mode 100644 index 00000000..88e4503e --- /dev/null +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as puppeteer from 'puppeteer'; +import * as util from 'util'; +import { assert } from 'chai'; +import { ITerminalOptions } from 'xterm'; + +const APP = 'http://127.0.0.1:3000/test'; + +let browser: puppeteer.Browser; +let page: puppeteer.Page; +const width = 800; +const height = 600; + +describe('SerializeAddon', () => { + before(async function (): Promise { + this.timeout(20000); + browser = await puppeteer.launch({ + headless: process.argv.indexOf('--headless') !== -1, + slowMo: 80, + args: [`--window-size=${width},${height}`] + }); + page = (await browser.pages())[0]; + await page.setViewport({ width, height }); + }); + + after(async () => { + await browser.close(); + }); + + beforeEach(async function (): Promise { + this.timeout(20000); + await page.goto(APP); + }); + + it('empty content', async function (): Promise { + this.timeout(20000); + const rows = 10; + const cols = 10; + const blankline = ' '.repeat(cols); + const lines = newArray(blankline, rows); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + `) + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('digits content', async function (): Promise { + this.timeout(20000); + const rows = 10; + const cols = 10; + const digitsLine = digitsString(cols); + const lines = newArray(digitsLine, rows); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `) + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize n rows of content', async function (): Promise { + this.timeout(20000); + const rows = 10; + const halfRows = rows >> 1; + const cols = 10; + const lines = newArray((index: number) => digitsString(cols, index), rows); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `) + + assert.equal(await page.evaluate(`serializeAddon.serialize(${halfRows});`), lines.slice(0, halfRows).join('\r\n')); + }); + + it('serialize 0 rows of content', async function (): Promise { + this.timeout(20000); + const rows = 10; + const cols = 10; + const lines = newArray((index: number) => digitsString(cols, index), rows); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `) + + assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), ''); + }); +}); + +async function openTerminal(options: ITerminalOptions = {}): Promise { + await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`); + await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + if (options.rendererType === 'dom') { + await page.waitForSelector('.xterm-rows'); + } else { + await page.waitForSelector('.xterm-text-layer'); + } +} + +function newArray(initial: T | ((index: number) => T), count: number): T[] { + const array: T[] = new Array(count); + for (let i = 0; i < array.length; i++) { + if (typeof initial === 'function') { + array[i] = (<(index: number) => T>initial)(i); + } else { + array[i] = initial; + } + } + return array; +} + +function digitsString(length: number, from: number = 0) { + let s = ''; + for (let i = 0; i < length; i++) { + s += (from++) % 10; + } + return s; +} diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts new file mode 100644 index 00000000..8dd5b194 --- /dev/null +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ITerminalAddon } from 'xterm'; + +export class SerializeAddon implements ITerminalAddon { + private _terminal: Terminal | undefined; + + constructor() { } + + public activate(terminal: Terminal): void { + this._terminal = terminal; + } + + public serialize(rows?: number): string { + if (!this._terminal) { + return ''; + } + const buffer = this._terminal.buffer; + const length = Math.max(0, Math.min((rows === undefined ? buffer.length : rows), buffer.length)); + let data = ''; + + for (let i = 0; i < length; i++) { + const line = buffer.getLine(i); + const last = i === length - 1; + if (line) { + data += line.translateToString(); + } + if (!last) { + data += '\r\n'; + } + } + + return data; + } + + public dispose(): void { + if (this._terminal !== undefined) { } + } +} diff --git a/addons/xterm-addon-serialize/src/tsconfig.json b/addons/xterm-addon-serialize/src/tsconfig.json new file mode 100644 index 00000000..5539aa56 --- /dev/null +++ b/addons/xterm-addon-serialize/src/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "lib": [ + "dom", + "es2015" + ], + "rootDir": ".", + "outDir": "../out", + "sourceMap": true, + "removeComments": true, + "strict": true + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ] +} diff --git a/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts new file mode 100644 index 00000000..e36e724a --- /dev/null +++ b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + + +import { Terminal, ITerminalAddon } from 'xterm'; + +declare module 'xterm-addon-serialize' { + /** + * An xterm.js addon that enables web links. + */ + export class SerializeAddon implements ITerminalAddon { + + constructor(); + + /** + * Activates the addon + * @param terminal The terminal the addon is being loaded in. + */ + public activate(terminal: Terminal): void; + + public serialize(rows?: number): string; + + /** + * Disposes the addon. + */ + public dispose(): void; + } +} diff --git a/addons/xterm-addon-serialize/webpack.config.js b/addons/xterm-addon-serialize/webpack.config.js new file mode 100644 index 00000000..4cabbad9 --- /dev/null +++ b/addons/xterm-addon-serialize/webpack.config.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'SerializeAddon'; +const mainFile = 'xterm-addon-serialize.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd' + }, + mode: 'production' +}; diff --git a/demo/client.ts b/demo/client.ts index e2259841..eb8b1053 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -14,6 +14,7 @@ import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon'; import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon'; import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon'; import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon'; +import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAddon'; // Use webpacked version (yarn package) // import { Terminal } from '../lib/xterm'; @@ -35,6 +36,7 @@ export interface IWindowWithTerminal extends Window { SearchAddon?: typeof SearchAddon; WebLinksAddon?: typeof WebLinksAddon; WebglAddon?: typeof WebglAddon; + SerializeAddon?: typeof SerializeAddon; } declare let window: IWindowWithTerminal; @@ -88,6 +90,7 @@ if (document.location.pathname === '/test') { window.SearchAddon = SearchAddon; window.WebLinksAddon = WebLinksAddon; window.WebglAddon = WebglAddon; + window.SerializeAddon = SerializeAddon; } else { createTerminal(); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); diff --git a/package.json b/package.json index f26c010f..1ee4986d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test": "npm run test-unit", "posttest": "npm run lint", "test-api": "mocha \"**/*.api.js\"", + "test-addons": "mocha \"**/*.test.js\"", "test-unit": "node ./bin/test.js", "build": "tsc -b ./tsconfig.all.json", "prepare": "npm run build", diff --git a/tsconfig.all.json b/tsconfig.all.json index 7ddf33d5..5a7ae05b 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -9,6 +9,7 @@ { "path": "./addons/xterm-addon-fit/src" }, { "path": "./addons/xterm-addon-search/src" }, { "path": "./addons/xterm-addon-web-links/src" }, - { "path": "./addons/xterm-addon-webgl/src" } + { "path": "./addons/xterm-addon-webgl/src" }, + { "path": "./addons/xterm-addon-serialize/src" } ] } From 3ba6ffe5f499aad1c57c78e89530aa6e1fc068f4 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Wed, 10 Jul 2019 21:57:50 +0800 Subject: [PATCH 02/47] fix lint error --- addons/xterm-addon-serialize/src/SerializeAddon.api.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts index 88e4503e..464316c3 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -47,7 +47,7 @@ describe('SerializeAddon', () => { await page.evaluate(` window.serializeAddon = new SerializeAddon(); window.term.loadAddon(window.serializeAddon); - `) + `); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); @@ -64,7 +64,7 @@ describe('SerializeAddon', () => { window.serializeAddon = new SerializeAddon(); window.term.loadAddon(window.serializeAddon); window.term.write(${util.inspect(lines.join('\r\n'))}); - `) + `); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); @@ -81,7 +81,7 @@ describe('SerializeAddon', () => { window.serializeAddon = new SerializeAddon(); window.term.loadAddon(window.serializeAddon); window.term.write(${util.inspect(lines.join('\r\n'))}); - `) + `); assert.equal(await page.evaluate(`serializeAddon.serialize(${halfRows});`), lines.slice(0, halfRows).join('\r\n')); }); @@ -97,7 +97,7 @@ describe('SerializeAddon', () => { window.serializeAddon = new SerializeAddon(); window.term.loadAddon(window.serializeAddon); window.term.write(${util.inspect(lines.join('\r\n'))}); - `) + `); assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), ''); }); @@ -125,7 +125,7 @@ function newArray(initial: T | ((index: number) => T), count: number): T[] { return array; } -function digitsString(length: number, from: number = 0) { +function digitsString(length: number, from: number = 0): string { let s = ''; for (let i = 0; i < length; i++) { s += (from++) % 10; From 57a82556547d5c352a12f39112360c7bfa5f6f66 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Fri, 12 Jul 2019 23:03:49 +0800 Subject: [PATCH 03/47] add test files from addons folder --- bin/test.js | 3 ++- package.json | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/test.js b/bin/test.js index 89f0d61e..2d816097 100644 --- a/bin/test.js +++ b/bin/test.js @@ -12,7 +12,8 @@ env.NODE_PATH = path.resolve(__dirname, '../out'); let testFiles = [ './out/*test.js', - './out/**/*test.js' + './out/**/*test.js', + './addons/**/out/*test.js', ]; // ability to inject particular test files via diff --git a/package.json b/package.json index 1ee4986d..f26c010f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "test": "npm run test-unit", "posttest": "npm run lint", "test-api": "mocha \"**/*.api.js\"", - "test-addons": "mocha \"**/*.test.js\"", "test-unit": "node ./bin/test.js", "build": "tsc -b ./tsconfig.all.json", "prepare": "npm run build", From 31f812cf94c3514f2749b8183c3e2e50ae8e0f41 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Fri, 12 Jul 2019 23:21:48 +0800 Subject: [PATCH 04/47] add missing api docs and refactor serialize implementation by using list join instead and some code clean up --- .../src/SerializeAddon.ts | 19 ++++++------------- .../typings/xterm-addon-serialize.d.ts | 8 ++++++++ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 8dd5b194..720da58b 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -15,28 +15,21 @@ export class SerializeAddon implements ITerminalAddon { } public serialize(rows?: number): string { + // TODO: Add frontground/background color support later if (!this._terminal) { - return ''; + throw new Error('No terminal found!'); } const buffer = this._terminal.buffer; const length = Math.max(0, Math.min((rows === undefined ? buffer.length : rows), buffer.length)); - let data = ''; + const lines: string[] = new Array(length); for (let i = 0; i < length; i++) { const line = buffer.getLine(i); - const last = i === length - 1; - if (line) { - data += line.translateToString(); - } - if (!last) { - data += '\r\n'; - } + lines[i] = line ? line.translateToString() : ''; } - return data; + return lines.join('\r\n'); } - public dispose(): void { - if (this._terminal !== undefined) { } - } + public dispose(): void { } } 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 e36e724a..78b615d7 100644 --- a/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts +++ b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts @@ -20,6 +20,14 @@ declare module 'xterm-addon-serialize' { */ public activate(terminal: Terminal): void; + /** + * Serializes terminal rows into a string that can be written back to the terminal + * to restore 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 rows The number of rows to serialize, starting from the top of the + * terminal. This defaults to the number of rows in the viewport. + */ public serialize(rows?: number): string; /** From fe3520c68587a6e538bdd75ed8f280d628b9d4fc Mon Sep 17 00:00:00 2001 From: javacs3 Date: Sat, 13 Jul 2019 17:25:18 +0800 Subject: [PATCH 05/47] change exception message --- addons/xterm-addon-serialize/src/SerializeAddon.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 720da58b..10d920cd 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -17,7 +17,7 @@ export class SerializeAddon implements ITerminalAddon { public serialize(rows?: number): string { // TODO: Add frontground/background color support later if (!this._terminal) { - throw new Error('No terminal found!'); + throw new Error('Cannot use addon until it has been loaded'); } const buffer = this._terminal.buffer; const length = Math.max(0, Math.min((rows === undefined ? buffer.length : rows), buffer.length)); From bca90245c8ff900353770a38b0155cfcb2dcfdaa Mon Sep 17 00:00:00 2001 From: javacs3 Date: Sun, 14 Jul 2019 23:22:20 +0800 Subject: [PATCH 06/47] serialize from the bottom of the terminal --- .../src/SerializeAddon.api.ts | 2 +- .../src/SerializeAddon.ts | 19 ++++++++++++++----- .../typings/xterm-addon-serialize.d.ts | 2 +- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts index 464316c3..5543696b 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -83,7 +83,7 @@ describe('SerializeAddon', () => { window.term.write(${util.inspect(lines.join('\r\n'))}); `); - assert.equal(await page.evaluate(`serializeAddon.serialize(${halfRows});`), lines.slice(0, halfRows).join('\r\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize(${halfRows});`), lines.slice(halfRows, 2 * halfRows).join('\r\n')); }); it('serialize 0 rows of content', async function (): Promise { diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 10d920cd..5d060949 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -5,6 +5,10 @@ import { Terminal, ITerminalAddon } from 'xterm'; +function crop(value: number, from: number, to: number) { + return Math.max(from, Math.min(value, to)) +} + export class SerializeAddon implements ITerminalAddon { private _terminal: Terminal | undefined; @@ -19,13 +23,18 @@ export class SerializeAddon implements ITerminalAddon { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } - const buffer = this._terminal.buffer; - const length = Math.max(0, Math.min((rows === undefined ? buffer.length : rows), buffer.length)); - const lines: string[] = new Array(length); + const terminalRows = this._terminal.rows; + if (rows === undefined) { + rows = terminalRows; + } + rows = crop(rows, 0, terminalRows); - for (let i = 0; i < length; i++) { + const buffer = this._terminal.buffer; + const lines: string[] = new Array(rows); + + for (let i = terminalRows - rows; i < terminalRows; i++) { const line = buffer.getLine(i); - lines[i] = line ? line.translateToString() : ''; + lines[i - terminalRows + rows] = line ? line.translateToString() : ''; } return lines.join('\r\n'); 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 78b615d7..9e7b500a 100644 --- a/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts +++ b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts @@ -25,7 +25,7 @@ declare module 'xterm-addon-serialize' { * to restore 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 rows The number of rows to serialize, starting from the top of the + * @param rows The number of rows to serialize, starting from the bottom of the * terminal. This defaults to the number of rows in the viewport. */ public serialize(rows?: number): string; From f859bffa749b1dd6ccdd401e89d15964e3bc63ee Mon Sep 17 00:00:00 2001 From: javacs3 Date: Mon, 15 Jul 2019 09:42:56 +0800 Subject: [PATCH 07/47] fix lint error --- addons/xterm-addon-serialize/src/SerializeAddon.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 5d060949..9328e674 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -5,8 +5,8 @@ import { Terminal, ITerminalAddon } from 'xterm'; -function crop(value: number, from: number, to: number) { - return Math.max(from, Math.min(value, to)) +function crop(value: number, from: number, to: number): number { + return Math.max(from, Math.min(value, to)); } export class SerializeAddon implements ITerminalAddon { From a069df7a369512a1e2ea5c850dacee9ebe8396ec Mon Sep 17 00:00:00 2001 From: javacs3 Date: Sun, 4 Aug 2019 23:02:05 +0800 Subject: [PATCH 08/47] refactor SerializeAddons initial value validation --- .../src/SerializeAddon.ts | 21 +++++++++++-------- .../xterm-addon-serialize/src/tsconfig.json | 7 +++++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 9328e674..4a051c33 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -4,9 +4,14 @@ */ import { Terminal, ITerminalAddon } from 'xterm'; +// import { IBufferLine } from 'common/Types'; -function crop(value: number, from: number, to: number): number { - return Math.max(from, Math.min(value, to)); +function crop(value: number | undefined, low: number, high: number, initial: number): number { + if (value === undefined) { + return initial; + } else { + return Math.max(low, Math.min(value, high)); + } } export class SerializeAddon implements ITerminalAddon { @@ -23,18 +28,16 @@ export class SerializeAddon implements ITerminalAddon { if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } - const terminalRows = this._terminal.rows; - if (rows === undefined) { - rows = terminalRows; - } - rows = crop(rows, 0, terminalRows); + + const maxRows = this._terminal.rows; + rows = crop(rows, 0, maxRows, maxRows); const buffer = this._terminal.buffer; const lines: string[] = new Array(rows); - for (let i = terminalRows - rows; i < terminalRows; i++) { + for (let i = maxRows - rows; i < maxRows; i++) { const line = buffer.getLine(i); - lines[i - terminalRows + rows] = line ? line.translateToString() : ''; + lines[i - maxRows + rows] = line ? line.translateToString() : ''; } return lines.join('\r\n'); diff --git a/addons/xterm-addon-serialize/src/tsconfig.json b/addons/xterm-addon-serialize/src/tsconfig.json index 5539aa56..57f3d6ed 100644 --- a/addons/xterm-addon-serialize/src/tsconfig.json +++ b/addons/xterm-addon-serialize/src/tsconfig.json @@ -10,10 +10,17 @@ "outDir": "../out", "sourceMap": true, "removeComments": true, + "baseUrl": ".", + "paths": { + "common/*": [ "../../../src/common/*" ] + }, "strict": true }, "include": [ "./**/*", "../../../typings/xterm.d.ts" + ], + "references": [ + { "path": "../../../src/common" } ] } From f088a9953f1efdfc7f35b5d58d36d11cf466120e Mon Sep 17 00:00:00 2001 From: javacs3 Date: Mon, 5 Aug 2019 21:08:51 +0800 Subject: [PATCH 09/47] refactor SerializeAddon extract SerializeHandler --- .../src/SerializeAddon.ts | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 4a051c33..44438c98 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -5,12 +5,29 @@ import { Terminal, ITerminalAddon } from 'xterm'; // import { IBufferLine } from 'common/Types'; +import { IBuffer } from 'common/buffer/Types'; function crop(value: number | undefined, low: number, high: number, initial: number): number { if (value === undefined) { return initial; - } else { - return Math.max(low, Math.min(value, high)); + } + return Math.max(low, Math.min(value, high)); +} + +class SerializeHandler { + constructor(private _buffer: IBuffer) { } + + serialize(start: number, end: number): string { + const rows = end - start; + const lines: string[] = new Array(rows); + + for (let i = start; i < end; i++) { + const line = this._buffer.lines.get(i); + + lines[i - start] = line ? line.translateToString() : ''; + } + + return lines.join('\r\n'); } } @@ -30,17 +47,11 @@ export class SerializeAddon implements ITerminalAddon { } const maxRows = this._terminal.rows; + const handler = new SerializeHandler((this._terminal)._core.buffer); + rows = crop(rows, 0, maxRows, maxRows); - const buffer = this._terminal.buffer; - const lines: string[] = new Array(rows); - - for (let i = maxRows - rows; i < maxRows; i++) { - const line = buffer.getLine(i); - lines[i - maxRows + rows] = line ? line.translateToString() : ''; - } - - return lines.join('\r\n'); + return handler.serialize(maxRows - rows, maxRows); } public dispose(): void { } From d81b4999dc8381a87d79188758670d1589981f81 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Wed, 7 Aug 2019 22:56:18 +0800 Subject: [PATCH 10/47] add frontground/background color support for SerializeAddon --- .../src/SerializeAddon.api.ts | 307 +++++++++++++++++- .../src/SerializeAddon.ts | 190 ++++++++++- src/common/buffer/Constants.ts | 14 +- 3 files changed, 492 insertions(+), 19 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts index 5543696b..eec5b6fc 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -21,6 +21,7 @@ describe('SerializeAddon', () => { browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, slowMo: 80, + devtools: true, args: [`--window-size=${width},${height}`] }); page = (await browser.pages())[0]; @@ -40,8 +41,7 @@ describe('SerializeAddon', () => { this.timeout(20000); const rows = 10; const cols = 10; - const blankline = ' '.repeat(cols); - const lines = newArray(blankline, rows); + const lines = newArray('', rows); await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); await page.evaluate(` @@ -69,7 +69,7 @@ describe('SerializeAddon', () => { assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); - it('serialize n rows of content', async function (): Promise { + it('serialize half rows of content', async function (): Promise { this.timeout(20000); const rows = 10; const halfRows = rows >> 1; @@ -101,6 +101,254 @@ describe('SerializeAddon', () => { assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), ''); }); + + it('serialize all rows of content with color16', async function (): Promise { + this.timeout(20000); + const rows = 16; + const cols = 10; + const color16 = [ + 30, 31, 32, 33, 34, 35, 36, 37, // Set foreground color + 40, 41, 42, 43, 44, 45, 46, 47 // Set background color + ]; + const lines = newArray( + (index: number) => digitsString(cols, index, `\x1b[${color16[index % color16.length]}m`), + rows + ); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with fg/bg flags', async function (): Promise { + this.timeout(20000); + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + mkSGR(FG_P16_GREEN) + line, // Workaround: If we clear all flags a the end, serialize will use \x1b[0m to clear instead of the sepcific disable sequence + mkSGR(INVERSE) + line, + mkSGR(BOLD) + line, + mkSGR(UNDERLINED) + line, + mkSGR(BLINK) + line, + mkSGR(INVISIBLE) + line, + mkSGR(NO_INVERSE) + line, + mkSGR(NO_BOLD) + line, + mkSGR(NO_UNDERLINED) + line, + mkSGR(NO_BLINK) + line, + mkSGR(NO_INVISIBLE) + line + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with color256', async function (): Promise { + this.timeout(20000); + const rows = 32; + const cols = 10; + const lines = newArray( + (index: number) => digitsString(cols, index, `\x1b[38;5;${index}m`), + rows + ); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with color16 and style separately', async function (): Promise { + this.timeout(20000); + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + mkSGR(FG_P16_RED) + line, // fg Red, + mkSGR(UNDERLINED) + line, // fg Red, Underlined + mkSGR(FG_P16_GREEN) + line, // fg Green, Underlined + mkSGR(INVERSE) + line, // fg Green, Underlined, Inverse + mkSGR(NO_INVERSE) + line, // fg Green, Underlined + mkSGR(INVERSE) + line, // fg Green, Underlined, Inverse + mkSGR(BG_P16_YELLOW) + line, // fg Green, bg Yellow, Underlined, Inverse + mkSGR(FG_RESET) + line, // bg Yellow, Underlined, Inverse + mkSGR(BG_RESET) + line, // Underlined, Inverse + mkSGR(NORMAL) + line // Back to normal + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with color16 and style together', async function (): Promise { + this.timeout(20000); + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + mkSGR(FG_P16_RED) + line, // fg Red + mkSGR(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow + mkSGR(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic + mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow + mkSGR(FG_RESET, ITALIC) + line, // bg Yellow, Italic + mkSGR(BG_RESET) + line, // Italic + mkSGR(NORMAL) + line, // Back to normal + mkSGR(FG_P16_RED) + line, // fg Red + mkSGR(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow + mkSGR(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic + mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow + mkSGR(FG_RESET, ITALIC) + line, // bg Yellow, Italic + mkSGR(BG_RESET) + line // Italic + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with color256 and style separately', async function (): Promise { + this.timeout(20000); + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + mkSGR(FG_P256_RED) + line, // fg Red 256, + mkSGR(UNDERLINED) + line, // fg Red 256, Underlined + mkSGR(FG_P256_GREEN) + line, // fg Green 256, Underlined + mkSGR(INVERSE) + line, // fg Green 256, Underlined, Inverse + mkSGR(NO_INVERSE) + line, // fg Green 256, Underlined + mkSGR(INVERSE) + line, // fg Green 256, Underlined, Inverse + mkSGR(BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256, Underlined, Inverse + mkSGR(FG_RESET) + line, // bg Yellow 256, Underlined, Inverse + mkSGR(BG_RESET) + line, // Underlined, Inverse + mkSGR(NORMAL) + line // Back to normal + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with color256 and style together', async function (): Promise { + this.timeout(20000); + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + mkSGR(FG_P256_RED) + line, // fg Red 256 + mkSGR(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256 + mkSGR(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic + mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256 + mkSGR(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic + mkSGR(BG_RESET) + line, // Italic + mkSGR(NORMAL) + line, // Back to normal + mkSGR(FG_P256_RED) + line, // fg Red 256 + mkSGR(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256 + mkSGR(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic + mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256 + mkSGR(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic + mkSGR(BG_RESET) + line // Italic + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with colorRGB and style separately', async function (): Promise { + this.timeout(20000); + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + mkSGR(FG_RGB_RED) + line, // fg Red RGB, + mkSGR(UNDERLINED) + line, // fg Red RGB, Underlined + mkSGR(FG_RGB_GREEN) + line, // fg Green RGB, Underlined + mkSGR(INVERSE) + line, // fg Green RGB, Underlined, Inverse + mkSGR(NO_INVERSE) + line, // fg Green RGB, Underlined + mkSGR(INVERSE) + line, // fg Green RGB, Underlined, Inverse + mkSGR(BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB, Underlined, Inverse + mkSGR(FG_RESET) + line, // bg Yellow RGB, Underlined, Inverse + mkSGR(BG_RESET) + line, // Underlined, Inverse + mkSGR(NORMAL) + line // Back to normal + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize all rows of content with colorRGB and style together', async function (): Promise { + this.timeout(20000); + const cols = 10; + const line = '+'.repeat(cols); + const lines: string[] = [ + mkSGR(FG_RGB_RED) + line, // fg Red RGB + mkSGR(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB + mkSGR(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic + mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB + mkSGR(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic + mkSGR(BG_RESET) + line, // Italic + mkSGR(NORMAL) + line, // Back to normal + mkSGR(FG_RGB_RED) + line, // fg Red RGB + mkSGR(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB + mkSGR(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic + mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB + mkSGR(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic + mkSGR(BG_RESET) + line // Italic + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { @@ -125,10 +373,57 @@ function newArray(initial: T | ((index: number) => T), count: number): T[] { return array; } -function digitsString(length: number, from: number = 0): string { - let s = ''; +function digitsString(length: number, from: number = 0, sgr: string = ''): string { + let s = sgr; for (let i = 0; i < length; i++) { - s += (from++) % 10; + s += `${(from++) % 10}`; } return s; } + +function mkSGR(...seq: string[]): string { + return `\x1b[${seq.join(';')}m`; +} + +const NORMAL = '0'; + +const FG_P16_RED = '31'; +const FG_P16_GREEN = '32'; +const FG_P16_YELLOW = '33'; +const FG_P256_RED = '38;5;1'; +const FG_P256_GREEN = '38;5;2'; +const FG_P256_YELLOW = '38;5;3'; +const FG_RGB_RED = '38;2;255;0;0'; +const FG_RGB_GREEN = '38;2;0;255;0'; +const FG_RGB_YELLOW = '38;2;255;255;0'; +const FG_RESET = '39'; + + +const BG_P16_RED = '41'; +const BG_P16_GREEN = '42'; +const BG_P16_YELLOW = '43'; +const BG_P256_RED = '48;5;1'; +const BG_P256_GREEN = '48;5;2'; +const BG_P256_YELLOW = '48;5;3'; +const BG_RGB_RED = '48;2;255;0;0'; +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 UNDERLINED = '4'; +const BLINK = '5'; +const INVISIBLE = '8'; + +const NO_INVERSE = '27'; +const NO_BOLD = '22'; +const NO_UNDERLINED = '24'; +const NO_BLINK = '25'; +const NO_INVISIBLE = '28'; + +const ITALIC = '3'; +const DIM = '2'; + +const NO_ITALIC = '23'; +const NO_DIM = '22'; diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 44438c98..88ea852f 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -4,8 +4,10 @@ */ import { Terminal, ITerminalAddon } from 'xterm'; -// import { IBufferLine } from 'common/Types'; +import { ICellData } from 'common/Types'; +import { CellData } from 'common/buffer/CellData'; import { IBuffer } from 'common/buffer/Types'; +import { Attributes, FgFlags, BgFlags } from 'common/buffer/Constants'; function crop(value: number | undefined, low: number, high: number, initial: number): number { if (value === undefined) { @@ -14,20 +16,184 @@ function crop(value: number | undefined, low: number, high: number, initial: num return Math.max(low, Math.min(value, high)); } -class SerializeHandler { +class BaseSerializeHandler { constructor(private _buffer: IBuffer) { } - serialize(start: number, end: number): string { - const rows = end - start; - const lines: string[] = new Array(rows); + serialize(startRow: number, endRow: number): string { + let oldCell = new CellData(); - for (let i = start; i < end; i++) { - const line = this._buffer.lines.get(i); + this._serializeStart(endRow - startRow); - lines[i - start] = line ? line.translateToString() : ''; + for (let row = startRow; row < endRow; row++) { + const line = this._buffer.lines.get(row); + + this._lineStart(row); + + if (line) { + for (let col = 0; col < line.length; col++) { + const cell = new CellData(); + + line.loadCell(col, cell); + + if (oldCell.fg !== cell.fg) { + this._fgChanged(cell, oldCell, row, col); + } + if (oldCell.bg !== cell.bg) { + this._bgChanged(cell, oldCell, row, col); + } + + this._cellChanged(cell, oldCell, row, col); + + oldCell = cell; + } + } + + this._lineEnd(row); } - return lines.join('\r\n'); + this._serializeEnd(); + + return this._serializeFinished(); + } + + protected _cellChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { } + + protected _fgChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { } + + protected _bgChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { } + + protected _lineStart(row: number): void { } + + protected _lineEnd(row: number): void { } + + protected _serializeStart(rows: number): void { } + + protected _serializeEnd(): void { } + + protected _serializeFinished(): string { return ''; } +} + +const FG_FM_MASK = FgFlags.FM_MASK; +const BG_FM_MASK = BgFlags.FM_MASK; +const COLOR_MASK = Attributes.CM_MASK | Attributes.RGB_MASK; + +class StringSerializeHandler extends BaseSerializeHandler { + private _rowIndex: number = 0; + private _allRows: string[] = new Array(); + private _currentRow: string = ''; + private _sgrSeq: string[] = []; + + constructor(buffer: IBuffer) { + super(buffer); + } + + protected _serializeStart(rows: number): void { + this._allRows = new Array(rows); + } + + protected _lineEnd(row: number): void { + this._allRows[this._rowIndex++] = this._currentRow; + this._currentRow = ''; + } + + protected _fgChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { + const fgFlagsChanged = (cell.fg ^ oldCell.fg) & FG_FM_MASK; + const fgColorChanged = (cell.fg ^ oldCell.fg) & COLOR_MASK; + const sgrSeq = this._sgrSeq; + + if ((cell.fg === 0) && (cell.bg === 0)) { + return; + } + + if (fgFlagsChanged) { + if (fgFlagsChanged & FgFlags.INVERSE) { + sgrSeq.push(cell.isInverse() ? '7' : '27'); + } + if (fgFlagsChanged & FgFlags.BOLD) { + sgrSeq.push(cell.isBold() ? '1' : '22'); + } + if (fgFlagsChanged & FgFlags.UNDERLINE) { + sgrSeq.push(cell.isUnderline() ? '4' : '24'); + } + if (fgFlagsChanged & FgFlags.BLINK) { + sgrSeq.push(cell.isBlink() ? '5' : '25'); + } + if (fgFlagsChanged & FgFlags.INVISIBLE) { + sgrSeq.push(cell.isInvisible() ? '8' : '28'); + } + } + + if (fgColorChanged) { + const fgColor = cell.getFgColor(); + + if (cell.isFgDefault()) { + sgrSeq.push('39'); + } else if (cell.isFgPalette()) { + switch (cell.getFgColorMode()) { + case Attributes.CM_P16: sgrSeq.push(`${30 + fgColor}`); break; + case Attributes.CM_P256: sgrSeq.push(`38;5;${fgColor}`); break; + } + } else if (cell.isFgRGB()) { + const [r, g, b] = CellData.toColorRGB(fgColor); + sgrSeq.push(`38;2;${r};${g};${b}`); + } + } + } + + protected _bgChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { + const bgFlagsChanged = (cell.bg ^ oldCell.bg) & BG_FM_MASK; + const bgColorChanged = (cell.bg ^ oldCell.bg) & COLOR_MASK; + const sgrSeq = this._sgrSeq; + + if ((cell.bg === 0) && (cell.fg === 0)) { + return; + } + + if (bgFlagsChanged) { + if (bgFlagsChanged & BgFlags.ITALIC) { + sgrSeq.push(cell.isItalic() ? '3' : '23'); + } + if (bgFlagsChanged & BgFlags.DIM) { + sgrSeq.push(cell.isDim() ? '2' : '22'); + } + } + + if (bgColorChanged) { + const bgColor = cell.getBgColor(); + + if (cell.isBgDefault()) { + sgrSeq.push('49'); + } else if (cell.isBgPalette()) { + switch (cell.getBgColorMode()) { + case Attributes.CM_P16: sgrSeq.push(`${40 + bgColor}`); break; + case Attributes.CM_P256: sgrSeq.push(`48;5;${bgColor}`); break; + } + } else if (cell.isFgRGB()) { + const [r, g, b] = CellData.toColorRGB(bgColor); + sgrSeq.push(`48;2;${r};${g};${b}`); + } + } + } + + protected _cellChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { + const fgChanged = cell.fg !== oldCell.fg; + const bgChanged = cell.bg !== oldCell.bg; + const isfgBgNormal = (cell.fg === 0) && (cell.bg === 0); + + if ((fgChanged || bgChanged) && isfgBgNormal) { + this._currentRow += '\x1b[0m'; + } + + if (this._sgrSeq.length) { + this._currentRow += `\x1b[${this._sgrSeq.join(';')}m`; + this._sgrSeq = []; + } + + this._currentRow += cell.getChars(); + } + + protected _serializeFinished(): string { + return this._allRows.join('\r\n'); } } @@ -41,13 +207,15 @@ export class SerializeAddon implements ITerminalAddon { } public serialize(rows?: number): string { - // TODO: Add frontground/background color support later + // TODO: Add re-position cursor support + // TODO: Add word wrap mode support + // TODO: Add combinedData support if (!this._terminal) { throw new Error('Cannot use addon until it has been loaded'); } const maxRows = this._terminal.rows; - const handler = new SerializeHandler((this._terminal)._core.buffer); + const handler = new StringSerializeHandler((this._terminal)._core.buffer); rows = crop(rows, 0, maxRows, maxRows); diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index 276a5c54..81ac773a 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -116,7 +116,12 @@ export const enum FgFlags { BOLD = 0x8000000, UNDERLINE = 0x10000000, BLINK = 0x20000000, - INVISIBLE = 0x40000000 + INVISIBLE = 0x40000000, + + /** + * bit 27..31 (32th bit unused) + */ + FM_MASK = 0x7C000000 } export const enum BgFlags { @@ -124,5 +129,10 @@ export const enum BgFlags { * bit 27..32 (upper 4 unused) */ ITALIC = 0x4000000, - DIM = 0x8000000 + DIM = 0x8000000, + + /** + * bit 27..32 (upper 4 unused) + */ + FM_MASK = 0xFC000000 } From 92709110efe443cc2445db0e987ec7271b2aa54a Mon Sep 17 00:00:00 2001 From: javacs3 Date: Thu, 8 Aug 2019 23:04:46 +0800 Subject: [PATCH 11/47] add SerializeAddon in demo page --- demo/client.ts | 17 +++++++++++++++++ demo/index.html | 6 ++++++ demo/style.css | 6 ++++++ 3 files changed, 29 insertions(+) diff --git a/demo/client.ts b/demo/client.ts index 1ca35a6a..91c5d33d 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -43,6 +43,7 @@ declare let window: IWindowWithTerminal; let term; let fitAddon: FitAddon; let searchAddon: SearchAddon; +let serializeAddon: SerializeAddon; let protocol; let socketURL; let socket; @@ -96,6 +97,7 @@ if (document.location.pathname === '/test') { createTerminal(); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); document.getElementById('webgl').addEventListener('click', () => term.loadAddon(new WebglAddon())); + document.getElementById('serialize').addEventListener('click', serializeButtonHandler); } function createTerminal(): void { @@ -116,6 +118,8 @@ function createTerminal(): void { typedTerm.loadAddon(searchAddon); fitAddon = new FitAddon(); typedTerm.loadAddon(fitAddon); + serializeAddon = new SerializeAddon(); + typedTerm.loadAddon(serializeAddon); window.term = term; // Expose `term` to window for debugging purposes term.onResize((size: { cols: number, rows: number }) => { @@ -323,3 +327,16 @@ function updateTerminalSize(): void { terminalContainer.style.height = height; fitAddon.fit(); } + +function serializeButtonHandler(): void { + const output = serializeAddon.serialize(); + const outputString = JSON.stringify(output); + console.log('serialize output', outputString); + + document.getElementById('serialize-output').innerText = outputString; + + if ((document.getElementById('write-to-terminal') as HTMLInputElement).checked) { + term.reset(); + term.write(output); + } +} diff --git a/demo/index.html b/demo/index.html index ef8f3891..4a4335ee 100644 --- a/demo/index.html +++ b/demo/index.html @@ -20,6 +20,12 @@

+
+

SerializeAddon

+ + + +

Options

diff --git a/demo/style.css b/demo/style.css index b061dfcb..12728289 100644 --- a/demo/style.css +++ b/demo/style.css @@ -30,3 +30,9 @@ p { padding-left: 20px; vertical-align: top; } + +code { + padding: 2px 4px; + color: #c7254e; + background-color: #f9f2f4; +} From 693f30477efb279d77ed65f25c0d4653c8f4c767 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Fri, 9 Aug 2019 16:34:54 +0800 Subject: [PATCH 12/47] add trim last empty lines support for SerializeAddon --- .../src/SerializeAddon.api.ts | 31 +++++++++++++++++-- .../src/SerializeAddon.ts | 10 +++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts index eec5b6fc..79343fbe 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -41,7 +41,6 @@ describe('SerializeAddon', () => { this.timeout(20000); const rows = 10; const cols = 10; - const lines = newArray('', rows); await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); await page.evaluate(` @@ -49,7 +48,35 @@ describe('SerializeAddon', () => { window.term.loadAddon(window.serializeAddon); `); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), ''); + }); + + it('trim last empty lines', async function (): Promise { + this.timeout(20000); + const cols = 10; + const lines = [ + '', + '', + digitsString(cols), + digitsString(cols), + '', + '', + digitsString(cols), + digitsString(cols), + '', + '', + '' + ]; + const rows = lines.length; + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.slice(0, 8).join('\r\n')); }); it('digits content', async function (): Promise { diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 88ea852f..564bbddc 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -193,7 +193,15 @@ class StringSerializeHandler extends BaseSerializeHandler { } protected _serializeFinished(): string { - return this._allRows.join('\r\n'); + let rowEnd = this._allRows.length; + + for (; rowEnd > 0; rowEnd--) { + if (this._allRows[rowEnd - 1]) { + break; + } + } + + return this._allRows.slice(0, rowEnd).join('\r\n'); } } From f12120b0552ce6f1551f68b91d81f81d577c7b9e Mon Sep 17 00:00:00 2001 From: javacs3 Date: Fri, 9 Aug 2019 19:59:48 +0800 Subject: [PATCH 13/47] fix demo page tag won't display multi space chars --- demo/index.html | 4 ++-- demo/style.css | 10 ++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/demo/index.html b/demo/index.html index 4a4335ee..0fee6f4a 100644 --- a/demo/index.html +++ b/demo/index.html @@ -23,8 +23,8 @@

SerializeAddon

- - + +
diff --git a/demo/style.css b/demo/style.css index 12728289..1ab5dc3c 100644 --- a/demo/style.css +++ b/demo/style.css @@ -31,8 +31,14 @@ p { vertical-align: top; } -code { - padding: 2px 4px; +pre { + display: block; + padding: 9.5px; + font-size: 13px; color: #c7254e; background-color: #f9f2f4; + word-break: break-all; + word-wrap: break-word; + white-space: pre-wrap; + word-wrap: break-word; } From 6710c13cc225cc343e96ccc51b4a2de41d8b7db4 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Fri, 9 Aug 2019 21:58:27 +0800 Subject: [PATCH 14/47] fix cell.getFgColor/getBgColor return color256 code even it's color16 mode leading to serialize return wrong sequence --- .../src/SerializeAddon.api.ts | 6 +++-- .../src/SerializeAddon.ts | 22 +++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts index 79343fbe..6fbf9af2 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -131,12 +131,14 @@ describe('SerializeAddon', () => { it('serialize all rows of content with color16', async function (): Promise { this.timeout(20000); - const rows = 16; const cols = 10; const color16 = [ 30, 31, 32, 33, 34, 35, 36, 37, // Set foreground color - 40, 41, 42, 43, 44, 45, 46, 47 // Set background color + 90, 91, 92, 93, 94, 95, 96, 97, + 40, 41, 42, 43, 44, 45, 46, 47, // Set background color + 100, 101, 103, 104, 105, 106, 107 ]; + const rows = color16.length; const lines = newArray( (index: number) => digitsString(cols, index, `\x1b[${color16[index % color16.length]}m`), rows diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 564bbddc..6143d94f 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -77,6 +77,24 @@ const FG_FM_MASK = FgFlags.FM_MASK; const BG_FM_MASK = BgFlags.FM_MASK; const COLOR_MASK = Attributes.CM_MASK | Attributes.RGB_MASK; +function fgColor256to16(c: number): number { + if (0 <= c && c <= 7) { + return 30 + c; + } else if (8 <= c && c <= 15) { + return 82 + c; + } + return -1; +} + +function bgColor256to16(c: number): number { + if (0 <= c && c <= 7) { + return 40 + c; + } else if (8 <= c && c <= 15) { + return 92 + c; + } + return -1; +} + class StringSerializeHandler extends BaseSerializeHandler { private _rowIndex: number = 0; private _allRows: string[] = new Array(); @@ -130,7 +148,7 @@ class StringSerializeHandler extends BaseSerializeHandler { sgrSeq.push('39'); } else if (cell.isFgPalette()) { switch (cell.getFgColorMode()) { - case Attributes.CM_P16: sgrSeq.push(`${30 + fgColor}`); break; + case Attributes.CM_P16: sgrSeq.push(fgColor256to16(fgColor).toString()); break; case Attributes.CM_P256: sgrSeq.push(`38;5;${fgColor}`); break; } } else if (cell.isFgRGB()) { @@ -165,7 +183,7 @@ class StringSerializeHandler extends BaseSerializeHandler { sgrSeq.push('49'); } else if (cell.isBgPalette()) { switch (cell.getBgColorMode()) { - case Attributes.CM_P16: sgrSeq.push(`${40 + bgColor}`); break; + case Attributes.CM_P16: sgrSeq.push(bgColor256to16(bgColor).toString()); break; case Attributes.CM_P256: sgrSeq.push(`48;5;${bgColor}`); break; } } else if (cell.isFgRGB()) { From 3cba961dad5c94c9328c076668a9e1d6ea99831c Mon Sep 17 00:00:00 2001 From: javacs3 Date: Sun, 11 Aug 2019 00:13:53 +0800 Subject: [PATCH 15/47] refactor SerializeAddon: use public api as much as possible --- .../src/SerializeAddon.ts | 102 +++++++++++------- src/public/Terminal.ts | 35 +++++- typings/xterm.d.ts | 27 +++++ 3 files changed, 123 insertions(+), 41 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 6143d94f..ae72ce4b 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -3,10 +3,7 @@ * @license MIT */ -import { Terminal, ITerminalAddon } from 'xterm'; -import { ICellData } from 'common/Types'; -import { CellData } from 'common/buffer/CellData'; -import { IBuffer } from 'common/buffer/Types'; +import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm'; import { Attributes, FgFlags, BgFlags } from 'common/buffer/Constants'; function crop(value: number | undefined, low: number, high: number, initial: number): number { @@ -16,24 +13,50 @@ function crop(value: number | undefined, low: number, high: number, initial: num return Math.max(low, Math.min(value, high)); } +class NullBufferCell implements IBufferCell { + fg: number = 0; bg: number = 0; + char: string = ''; + width: number = 0; + isInverse: number = 0; + isBold: number = 0; + isUnderline: number = 0; + isBlink: number = 0; + isInvisible: number = 0; + isItalic: number = 0; + isDim: number = 0; + fgColorMode: number = 0; + bgColorMode: number = 0; + isFgRGB: boolean = false; + isBgRGB: boolean = false; + isFgPalette: boolean = false; + isBgPalette: boolean = false; + isFgDefault: boolean = false; + isBgDefault: boolean = false; + fgColor: number = 0; + bgColor: number = 0; +} + class BaseSerializeHandler { constructor(private _buffer: IBuffer) { } serialize(startRow: number, endRow: number): string { - let oldCell = new CellData(); + let oldCell: IBufferCell = new NullBufferCell(); this._serializeStart(endRow - startRow); for (let row = startRow; row < endRow; row++) { - const line = this._buffer.lines.get(row); + const line = this._buffer.getLine(row); this._lineStart(row); if (line) { for (let col = 0; col < line.length; col++) { - const cell = new CellData(); + const cell = line.getCell(col); - line.loadCell(col, cell); + if (!cell) { + console.warn(`Can't get cell at row=${row}, col=${col}`); + continue; + } if (oldCell.fg !== cell.fg) { this._fgChanged(cell, oldCell, row, col); @@ -41,7 +64,6 @@ class BaseSerializeHandler { if (oldCell.bg !== cell.bg) { this._bgChanged(cell, oldCell, row, col); } - this._cellChanged(cell, oldCell, row, col); oldCell = cell; @@ -56,11 +78,11 @@ class BaseSerializeHandler { return this._serializeFinished(); } - protected _cellChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { } + protected _cellChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _fgChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { } + protected _fgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _bgChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { } + protected _bgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } protected _lineStart(row: number): void { } @@ -95,6 +117,14 @@ function bgColor256to16(c: number): number { return -1; } +function hex2rgb(value: number): [number, number, number] { + return [ + value >>> Attributes.RED_SHIFT & 255, + value >>> Attributes.GREEN_SHIFT & 255, + value & 255 + ]; +} + class StringSerializeHandler extends BaseSerializeHandler { private _rowIndex: number = 0; private _allRows: string[] = new Array(); @@ -114,7 +144,7 @@ class StringSerializeHandler extends BaseSerializeHandler { this._currentRow = ''; } - protected _fgChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { + protected _fgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { const fgFlagsChanged = (cell.fg ^ oldCell.fg) & FG_FM_MASK; const fgColorChanged = (cell.fg ^ oldCell.fg) & COLOR_MASK; const sgrSeq = this._sgrSeq; @@ -125,40 +155,40 @@ class StringSerializeHandler extends BaseSerializeHandler { if (fgFlagsChanged) { if (fgFlagsChanged & FgFlags.INVERSE) { - sgrSeq.push(cell.isInverse() ? '7' : '27'); + sgrSeq.push(cell.isInverse ? '7' : '27'); } if (fgFlagsChanged & FgFlags.BOLD) { - sgrSeq.push(cell.isBold() ? '1' : '22'); + sgrSeq.push(cell.isBold ? '1' : '22'); } if (fgFlagsChanged & FgFlags.UNDERLINE) { - sgrSeq.push(cell.isUnderline() ? '4' : '24'); + sgrSeq.push(cell.isUnderline ? '4' : '24'); } if (fgFlagsChanged & FgFlags.BLINK) { - sgrSeq.push(cell.isBlink() ? '5' : '25'); + sgrSeq.push(cell.isBlink ? '5' : '25'); } if (fgFlagsChanged & FgFlags.INVISIBLE) { - sgrSeq.push(cell.isInvisible() ? '8' : '28'); + sgrSeq.push(cell.isInvisible ? '8' : '28'); } } if (fgColorChanged) { - const fgColor = cell.getFgColor(); + const fgColor = cell.fgColor; - if (cell.isFgDefault()) { + if (cell.isFgDefault) { sgrSeq.push('39'); - } else if (cell.isFgPalette()) { - switch (cell.getFgColorMode()) { + } else if (cell.isFgPalette) { + switch (cell.fgColorMode) { case Attributes.CM_P16: sgrSeq.push(fgColor256to16(fgColor).toString()); break; case Attributes.CM_P256: sgrSeq.push(`38;5;${fgColor}`); break; } - } else if (cell.isFgRGB()) { - const [r, g, b] = CellData.toColorRGB(fgColor); + } else if (cell.isFgRGB) { + const [r, g, b] = hex2rgb(fgColor); sgrSeq.push(`38;2;${r};${g};${b}`); } } } - protected _bgChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { + protected _bgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { const bgFlagsChanged = (cell.bg ^ oldCell.bg) & BG_FM_MASK; const bgColorChanged = (cell.bg ^ oldCell.bg) & COLOR_MASK; const sgrSeq = this._sgrSeq; @@ -169,31 +199,31 @@ class StringSerializeHandler extends BaseSerializeHandler { if (bgFlagsChanged) { if (bgFlagsChanged & BgFlags.ITALIC) { - sgrSeq.push(cell.isItalic() ? '3' : '23'); + sgrSeq.push(cell.isItalic ? '3' : '23'); } if (bgFlagsChanged & BgFlags.DIM) { - sgrSeq.push(cell.isDim() ? '2' : '22'); + sgrSeq.push(cell.isDim ? '2' : '22'); } } if (bgColorChanged) { - const bgColor = cell.getBgColor(); + const bgColor = cell.bgColor; - if (cell.isBgDefault()) { + if (cell.isBgDefault) { sgrSeq.push('49'); - } else if (cell.isBgPalette()) { - switch (cell.getBgColorMode()) { + } else if (cell.isBgPalette) { + switch (cell.bgColorMode) { case Attributes.CM_P16: sgrSeq.push(bgColor256to16(bgColor).toString()); break; case Attributes.CM_P256: sgrSeq.push(`48;5;${bgColor}`); break; } - } else if (cell.isFgRGB()) { - const [r, g, b] = CellData.toColorRGB(bgColor); + } else if (cell.isFgRGB) { + const [r, g, b] = hex2rgb(bgColor); sgrSeq.push(`48;2;${r};${g};${b}`); } } } - protected _cellChanged(cell: ICellData, oldCell: ICellData, row: number, col: number): void { + protected _cellChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { const fgChanged = cell.fg !== oldCell.fg; const bgChanged = cell.bg !== oldCell.bg; const isfgBgNormal = (cell.fg === 0) && (cell.bg === 0); @@ -207,7 +237,7 @@ class StringSerializeHandler extends BaseSerializeHandler { this._sgrSeq = []; } - this._currentRow += cell.getChars(); + this._currentRow += cell.char; } protected _serializeFinished(): string { @@ -241,7 +271,7 @@ export class SerializeAddon implements ITerminalAddon { } const maxRows = this._terminal.rows; - const handler = new StringSerializeHandler((this._terminal)._core.buffer); + const handler = new StringSerializeHandler(this._terminal.buffer); rows = crop(rows, 0, maxRows, maxRows); diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 5d270ca7..20ecf7e9 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -5,8 +5,9 @@ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm'; import { ITerminal } from '../Types'; -import { IBufferLine } from 'common/Types'; +import { IBufferLine, ICellData } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; +import { CellData } from 'common/buffer/CellData'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../browser/LocalizableStrings'; import { IEvent } from 'common/EventEmitter'; @@ -204,11 +205,15 @@ class BufferLineApiView implements IBufferLineApi { constructor(private _line: IBufferLine) {} public get isWrapped(): boolean { return this._line.isWrapped; } + public get length(): number { return this._line.length; } public getCell(x: number): IBufferCellApi | undefined { if (x < 0 || x >= this._line.length) { return undefined; } - return new BufferCellApiView(this._line, x); + + const cell = new CellData(); + this._line.loadCell(x, cell); + return new BufferCellApiView(cell); } public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string { return this._line.translateToString(trimRight, startColumn, endColumn); @@ -216,7 +221,27 @@ class BufferLineApiView implements IBufferLineApi { } class BufferCellApiView implements IBufferCellApi { - constructor(private _line: IBufferLine, private _x: number) {} - public get char(): string { return this._line.getString(this._x); } - public get width(): number { return this._line.getWidth(this._x); } + constructor(private _cell: ICellData) {} + + public get fg(): number { return this._cell.fg; } + public get bg(): number { return this._cell.bg; } + public get char(): string { return this._cell.getChars(); } + public get width(): number { return this._cell.getWidth(); } + public get isInverse(): number { return this._cell.isInverse(); } + public get isBold(): number { return this._cell.isBold(); } + public get isUnderline(): number { return this._cell.isUnderline(); } + public get isBlink(): number { return this._cell.isBlink(); } + public get isInvisible(): number { return this._cell.isInvisible(); } + public get isItalic(): number { return this._cell.isItalic(); } + public get isDim(): number { return this._cell.isDim(); } + public get fgColorMode(): number { return this._cell.getFgColorMode(); } + public get bgColorMode(): number { return this._cell.getBgColorMode(); } + public get isFgRGB(): boolean { return this._cell.isFgRGB(); } + public get isBgRGB(): boolean { return this._cell.isBgRGB(); } + public get isFgPalette(): boolean { return this._cell.isFgPalette(); } + public get isBgPalette(): boolean { return this._cell.isBgPalette(); } + public get isFgDefault(): boolean { return this._cell.isFgDefault(); } + public get isBgDefault(): boolean { return this._cell.isBgDefault(); } + public get fgColor(): number { return this._cell.getFgColor(); } + public get bgColor(): number { return this._cell.getBgColor(); } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d9e28b26..1ec96674 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -910,6 +910,7 @@ declare module 'xterm' { * Whether the line is wrapped from the previous line. */ readonly isWrapped: boolean; + readonly length: number; /** * Gets a cell from the line, or undefined if the line index does not exist. @@ -937,6 +938,9 @@ declare module 'xterm' { * Represents a single cell in the terminal's buffer. */ interface IBufferCell { + readonly fg: number; + readonly bg: number; + /** * The character within the cell. */ @@ -950,5 +954,28 @@ declare module 'xterm' { * - This is `0` for cells immediately following cells with a width of `2`. */ readonly width: number; + + // flags + readonly isInverse: number; + readonly isBold: number; + readonly isUnderline: number; + readonly isBlink: number; + readonly isInvisible: number; + readonly isItalic: number; + readonly isDim: number; + + // color modes + readonly fgColorMode: number; + readonly bgColorMode: number; + readonly isFgRGB: boolean; + readonly isBgRGB: boolean; + readonly isFgPalette: boolean; + readonly isBgPalette: boolean; + readonly isFgDefault: boolean; + readonly isBgDefault: boolean; + + // colors + readonly fgColor: number; + readonly bgColor: number; } } From 73f9d45fdc26fd52b712fd181cc3efaf890e03de Mon Sep 17 00:00:00 2001 From: javacs3 Date: Thu, 15 Aug 2019 00:02:01 +0800 Subject: [PATCH 16/47] refactor use a nicer IBufferCell design --- .../src/SerializeAddon.ts | 211 ++++++++---------- src/public/Terminal.ts | 59 +++-- typings/xterm.d.ts | 74 ++++-- 3 files changed, 188 insertions(+), 156 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index ae72ce4b..0332477f 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -3,8 +3,30 @@ * @license MIT */ -import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm'; -import { Attributes, FgFlags, BgFlags } from 'common/buffer/Constants'; +import { Terminal, ITerminalAddon, IBuffer, IBufferCell, Color } from 'xterm'; + +// TODO: Workaround here, will remove this later +// If I use `import { CellStyle } from 'xterm'` instead, demo page will raise bellow error +// +// ERROR in ./addons/xterm-addon-serialize/out/SerializeAddon.js +// Module not found: Error: Can't resolve 'xterm' in '/Users/javacs3/Lab/Playground/xterm.js/addons/xterm-addon-serialize/out' +// @ ./addons/xterm-addon-serialize/out/SerializeAddon.js 16:14-30 +// @ ./demo/client.ts +// +// Looks like typescript generate this line `var xterm_1 = require("xterm");` that leads to the error; +// +enum CellStyle { + default = 0, + // foreground style + inverse = 0x4000000 >>> 24, + bold = 0x8000000 >>> 24, + underline = 0x10000000 >>> 24, + blink = 0x20000000 >>> 24, + invisible = 0x40000000 >>> 24, + // background style + italic = 0x4000000 >>> 16, + dim = 0x8000000 >>> 16 +} function crop(value: number | undefined, low: number, high: number, initial: number): number { if (value === undefined) { @@ -14,29 +36,14 @@ function crop(value: number | undefined, low: number, high: number, initial: num } class NullBufferCell implements IBufferCell { - fg: number = 0; bg: number = 0; char: string = ''; width: number = 0; - isInverse: number = 0; - isBold: number = 0; - isUnderline: number = 0; - isBlink: number = 0; - isInvisible: number = 0; - isItalic: number = 0; - isDim: number = 0; - fgColorMode: number = 0; - bgColorMode: number = 0; - isFgRGB: boolean = false; - isBgRGB: boolean = false; - isFgPalette: boolean = false; - isBgPalette: boolean = false; - isFgDefault: boolean = false; - isBgDefault: boolean = false; - fgColor: number = 0; - bgColor: number = 0; + foregroundColor: Color = { type: 'default', hash: 0 }; + backgroundColor: Color = { type: 'default', hash: 0 } + style: CellStyle = CellStyle.default; } -class BaseSerializeHandler { +abstract class BaseSerializeHandler { constructor(private _buffer: IBuffer) { } serialize(startRow: number, endRow: number): string { @@ -57,14 +64,15 @@ class BaseSerializeHandler { console.warn(`Can't get cell at row=${row}, col=${col}`); continue; } + if ((cell.foregroundColor.hash !== oldCell.foregroundColor.hash) + || (cell.backgroundColor.hash !== oldCell.backgroundColor.hash)) { + this._cellColorChanged(cell, oldCell, row, col); + } + if (cell.style !== oldCell.style) { + this._cellStyleChanged(cell, oldCell, row, col); + } - if (oldCell.fg !== cell.fg) { - this._fgChanged(cell, oldCell, row, col); - } - if (oldCell.bg !== cell.bg) { - this._bgChanged(cell, oldCell, row, col); - } - this._cellChanged(cell, oldCell, row, col); + this._nextCell(cell, oldCell, row, col); oldCell = cell; } @@ -78,11 +86,11 @@ class BaseSerializeHandler { return this._serializeFinished(); } - protected _cellChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } + protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _fgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } + protected _cellStyleChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _bgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } + protected _cellColorChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } protected _lineStart(row: number): void { } @@ -95,10 +103,6 @@ class BaseSerializeHandler { protected _serializeFinished(): string { return ''; } } -const FG_FM_MASK = FgFlags.FM_MASK; -const BG_FM_MASK = BgFlags.FM_MASK; -const COLOR_MASK = Attributes.CM_MASK | Attributes.RGB_MASK; - function fgColor256to16(c: number): number { if (0 <= c && c <= 7) { return 30 + c; @@ -117,12 +121,8 @@ function bgColor256to16(c: number): number { return -1; } -function hex2rgb(value: number): [number, number, number] { - return [ - value >>> Attributes.RED_SHIFT & 255, - value >>> Attributes.GREEN_SHIFT & 255, - value & 255 - ]; +function isDefaultColorStyle(cell: IBufferCell) { + return (cell.foregroundColor.hash === 0) && (cell.backgroundColor.hash === 0) && (cell.style === CellStyle.default); } class StringSerializeHandler extends BaseSerializeHandler { @@ -144,91 +144,76 @@ class StringSerializeHandler extends BaseSerializeHandler { this._currentRow = ''; } - protected _fgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { - const fgFlagsChanged = (cell.fg ^ oldCell.fg) & FG_FM_MASK; - const fgColorChanged = (cell.fg ^ oldCell.fg) & COLOR_MASK; + protected _cellStyleChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + const styleChangedMask = cell.style ^ oldCell.style; + const style = cell.style; const sgrSeq = this._sgrSeq; - if ((cell.fg === 0) && (cell.bg === 0)) { + // skip if it's default color style, we will use \x1b[0m to clear every color style later + if (isDefaultColorStyle(cell)) { return; } - if (fgFlagsChanged) { - if (fgFlagsChanged & FgFlags.INVERSE) { - sgrSeq.push(cell.isInverse ? '7' : '27'); - } - if (fgFlagsChanged & FgFlags.BOLD) { - sgrSeq.push(cell.isBold ? '1' : '22'); - } - if (fgFlagsChanged & FgFlags.UNDERLINE) { - sgrSeq.push(cell.isUnderline ? '4' : '24'); - } - if (fgFlagsChanged & FgFlags.BLINK) { - sgrSeq.push(cell.isBlink ? '5' : '25'); - } - if (fgFlagsChanged & FgFlags.INVISIBLE) { - sgrSeq.push(cell.isInvisible ? '8' : '28'); + if (styleChangedMask & CellStyle.inverse) { + sgrSeq.push((style & CellStyle.inverse) ? '7' : '27'); + } + if (styleChangedMask & CellStyle.bold) { + sgrSeq.push((style & CellStyle.bold) ? '1' : '22'); + } + if (styleChangedMask & CellStyle.underline) { + sgrSeq.push((style & CellStyle.underline) ? '4' : '24'); + } + if (styleChangedMask & CellStyle.blink) { + sgrSeq.push((style & CellStyle.blink) ? '5' : '25'); + } + if (styleChangedMask & CellStyle.invisible) { + sgrSeq.push((style & CellStyle.invisible) ? '8' : '28'); + } + if (styleChangedMask & CellStyle.italic) { + sgrSeq.push((style & CellStyle.italic) ? '3' : '23'); + } + if (styleChangedMask & CellStyle.dim) { + sgrSeq.push((style & CellStyle.dim) ? '2' : '22'); + } + } + + protected _cellColorChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + const foregroundColorChanged = cell.foregroundColor.hash !== oldCell.foregroundColor.hash; + const backgroundColorChanged = cell.backgroundColor.hash !== oldCell.backgroundColor.hash; + const sgrSeq = this._sgrSeq; + + // skip if it's default color style, we will use \x1b[0m to clear every color style later + if (isDefaultColorStyle(cell)) { + return; + } + + if (foregroundColorChanged) { + const foregroundColor = cell.foregroundColor; + switch (foregroundColor.type) { + case 'default': sgrSeq.push('39'); break; + case 'palette16': sgrSeq.push(fgColor256to16(foregroundColor.id).toString()); break; + case 'palette256': sgrSeq.push(`38;5;${foregroundColor.id}`); break; + case 'rgb': const { red, green, blue } = foregroundColor; sgrSeq.push(`38;2;${red};${green};${blue}`); break; } } - if (fgColorChanged) { - const fgColor = cell.fgColor; - - if (cell.isFgDefault) { - sgrSeq.push('39'); - } else if (cell.isFgPalette) { - switch (cell.fgColorMode) { - case Attributes.CM_P16: sgrSeq.push(fgColor256to16(fgColor).toString()); break; - case Attributes.CM_P256: sgrSeq.push(`38;5;${fgColor}`); break; - } - } else if (cell.isFgRGB) { - const [r, g, b] = hex2rgb(fgColor); - sgrSeq.push(`38;2;${r};${g};${b}`); + if (backgroundColorChanged) { + const backgroundColor = cell.backgroundColor; + switch (backgroundColor.type) { + case 'default': sgrSeq.push('49'); break; + case 'palette16': sgrSeq.push(bgColor256to16(backgroundColor.id).toString()); break; + case 'palette256': sgrSeq.push(`48;5;${backgroundColor.id}`); break; + case 'rgb': const { red, green, blue } = backgroundColor; sgrSeq.push(`48;2;${red};${green};${blue}`); break; } } } - protected _bgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { - const bgFlagsChanged = (cell.bg ^ oldCell.bg) & BG_FM_MASK; - const bgColorChanged = (cell.bg ^ oldCell.bg) & COLOR_MASK; - const sgrSeq = this._sgrSeq; + protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + const foregroundColorChanged = cell.foregroundColor.hash !== oldCell.foregroundColor.hash; + const backgroundColorChanged = cell.backgroundColor.hash !== oldCell.backgroundColor.hash; + const styleChanged = cell.style !== oldCell.style; - if ((cell.bg === 0) && (cell.fg === 0)) { - return; - } - - if (bgFlagsChanged) { - if (bgFlagsChanged & BgFlags.ITALIC) { - sgrSeq.push(cell.isItalic ? '3' : '23'); - } - if (bgFlagsChanged & BgFlags.DIM) { - sgrSeq.push(cell.isDim ? '2' : '22'); - } - } - - if (bgColorChanged) { - const bgColor = cell.bgColor; - - if (cell.isBgDefault) { - sgrSeq.push('49'); - } else if (cell.isBgPalette) { - switch (cell.bgColorMode) { - case Attributes.CM_P16: sgrSeq.push(bgColor256to16(bgColor).toString()); break; - case Attributes.CM_P256: sgrSeq.push(`48;5;${bgColor}`); break; - } - } else if (cell.isFgRGB) { - const [r, g, b] = hex2rgb(bgColor); - sgrSeq.push(`48;2;${r};${g};${b}`); - } - } - } - - protected _cellChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { - const fgChanged = cell.fg !== oldCell.fg; - const bgChanged = cell.bg !== oldCell.bg; - const isfgBgNormal = (cell.fg === 0) && (cell.bg === 0); - - if ((fgChanged || bgChanged) && isfgBgNormal) { + if ((foregroundColorChanged || backgroundColorChanged || styleChanged) && isDefaultColorStyle(cell)) { this._currentRow += '\x1b[0m'; } diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 20ecf7e9..377f7acd 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -3,10 +3,11 @@ * @license MIT */ -import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm'; +import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, Color, CellStyle} from 'xterm'; import { ITerminal } from '../Types'; import { IBufferLine, ICellData } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; +import { Attributes, FgFlags, BgFlags } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../browser/LocalizableStrings'; @@ -220,28 +221,46 @@ class BufferLineApiView implements IBufferLineApi { } } +const COLOR_MASK = Attributes.CM_MASK | Attributes.RGB_MASK; + class BufferCellApiView implements IBufferCellApi { constructor(private _cell: ICellData) {} - public get fg(): number { return this._cell.fg; } - public get bg(): number { return this._cell.bg; } public get char(): string { return this._cell.getChars(); } public get width(): number { return this._cell.getWidth(); } - public get isInverse(): number { return this._cell.isInverse(); } - public get isBold(): number { return this._cell.isBold(); } - public get isUnderline(): number { return this._cell.isUnderline(); } - public get isBlink(): number { return this._cell.isBlink(); } - public get isInvisible(): number { return this._cell.isInvisible(); } - public get isItalic(): number { return this._cell.isItalic(); } - public get isDim(): number { return this._cell.isDim(); } - public get fgColorMode(): number { return this._cell.getFgColorMode(); } - public get bgColorMode(): number { return this._cell.getBgColorMode(); } - public get isFgRGB(): boolean { return this._cell.isFgRGB(); } - public get isBgRGB(): boolean { return this._cell.isBgRGB(); } - public get isFgPalette(): boolean { return this._cell.isFgPalette(); } - public get isBgPalette(): boolean { return this._cell.isBgPalette(); } - public get isFgDefault(): boolean { return this._cell.isFgDefault(); } - public get isBgDefault(): boolean { return this._cell.isBgDefault(); } - public get fgColor(): number { return this._cell.getFgColor(); } - public get bgColor(): number { return this._cell.getBgColor(); } + public get foregroundColor(): Color { + const cell = this._cell; + const hash = cell.fg & COLOR_MASK; + if (cell.isFgDefault()) { + return { type: 'default', hash: 0 }; + } else if (cell.isFgPalette()) { + switch (cell.getFgColorMode()) { + case Attributes.CM_P16: return { type: 'palette16', hash, id: cell.getFgColor() }; + case Attributes.CM_P256: return { type: 'palette256', hash, id: cell.getFgColor() }; + } + } else if (cell.isFgRGB()) { + const [red, green, blue] = CellData.toColorRGB(cell.fg); + return { type: 'rgb', hash, red, green, blue }; + } + throw new Error('Invalid foregroundColor'); + } + public get backgroundColor(): Color { + const cell = this._cell; + const hash = cell.bg & COLOR_MASK; + if (cell.isBgDefault()) { + return { type: 'default', hash: 0 }; + } else if (cell.isBgPalette()) { + switch (cell.getBgColorMode()) { + case Attributes.CM_P16: return { type: 'palette16', hash, id: cell.getBgColor() }; + case Attributes.CM_P256: return { type: 'palette256', hash, id: cell.getBgColor() }; + } + } else if (cell.isBgRGB()) { + const [red, green, blue] = CellData.toColorRGB(cell.bg); + return { type: 'rgb', hash, red, green, blue }; + } + throw new Error('Invalid backgroundColor'); + } + public get style(): CellStyle { + return ((this._cell.bg & BgFlags.FM_MASK) >>> 16) | ((this._cell.fg & FgFlags.FM_MASK) >>> 24); + } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 1ec96674..4503e1c1 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -938,9 +938,6 @@ declare module 'xterm' { * Represents a single cell in the terminal's buffer. */ interface IBufferCell { - readonly fg: number; - readonly bg: number; - /** * The character within the cell. */ @@ -955,27 +952,58 @@ declare module 'xterm' { */ readonly width: number; - // flags - readonly isInverse: number; - readonly isBold: number; - readonly isUnderline: number; - readonly isBlink: number; - readonly isInvisible: number; - readonly isItalic: number; - readonly isDim: number; + readonly foregroundColor: Color; + readonly backgroundColor: Color; + readonly style: CellStyle; + } - // color modes - readonly fgColorMode: number; - readonly bgColorMode: number; - readonly isFgRGB: boolean; - readonly isBgRGB: boolean; - readonly isFgPalette: boolean; - readonly isBgPalette: boolean; - readonly isFgDefault: boolean; - readonly isBgDefault: boolean; + export type Color = IDefaultColor | IPalette16Color | IPalette256Color | IRgbColor; + export enum CellStyle { + default = 0, + // foreground style + inverse = 0x4000000 >>> 24, + bold = 0x8000000 >>> 24, + underline = 0x10000000 >>> 24, + blink = 0x20000000 >>> 24, + invisible = 0x40000000 >>> 24, + // background style + italic = 0x4000000 >>> 16, + dim = 0x8000000 >>> 16 + } - // colors - readonly fgColor: number; - readonly bgColor: number; + interface ICellColor { + type: string; + hash: number; + } + + interface IDefaultColor extends ICellColor { + type: 'default'; + hash: 0; + } + + interface IRgbColor extends ICellColor { + type: 'rgb'; + hash: number; + + // 0-255 + red: number; + green: number; + blue: number; + } + + interface IPalette16Color extends ICellColor { + type: 'palette16'; + hash: number; + + // 0-15 + id: number; + } + + interface IPalette256Color extends ICellColor { + type: 'palette256'; + hash: number; + + // 0-255 + id: number; } } From 7a7f3466f3b9b5e905db64b421dfbdf1d23d2d00 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Sat, 24 Aug 2019 00:03:50 +0800 Subject: [PATCH 17/47] refactor ICellColor to CellColor --- .../src/SerializeAddon.ts | 32 +++++----- src/public/Terminal.ts | 62 +++++++++++++------ typings/xterm.d.ts | 45 ++++---------- 3 files changed, 71 insertions(+), 68 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 0332477f..b5d6e08d 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ITerminalAddon, IBuffer, IBufferCell, Color } from 'xterm'; +import { Terminal, ITerminalAddon, IBuffer, IBufferCell, CellColor } from 'xterm'; // TODO: Workaround here, will remove this later // If I use `import { CellStyle } from 'xterm'` instead, demo page will raise bellow error @@ -38,8 +38,8 @@ function crop(value: number | undefined, low: number, high: number, initial: num class NullBufferCell implements IBufferCell { char: string = ''; width: number = 0; - foregroundColor: Color = { type: 'default', hash: 0 }; - backgroundColor: Color = { type: 'default', hash: 0 } + foregroundColor: CellColor = CellColor.getDefault(); + backgroundColor: CellColor = CellColor.getDefault(); style: CellStyle = CellStyle.default; } @@ -64,8 +64,8 @@ abstract class BaseSerializeHandler { console.warn(`Can't get cell at row=${row}, col=${col}`); continue; } - if ((cell.foregroundColor.hash !== oldCell.foregroundColor.hash) - || (cell.backgroundColor.hash !== oldCell.backgroundColor.hash)) { + if (!cell.foregroundColor.equals(oldCell.foregroundColor) + || !cell.backgroundColor.equals(oldCell.backgroundColor)) { this._cellColorChanged(cell, oldCell, row, col); } if (cell.style !== oldCell.style) { @@ -122,7 +122,7 @@ function bgColor256to16(c: number): number { } function isDefaultColorStyle(cell: IBufferCell) { - return (cell.foregroundColor.hash === 0) && (cell.backgroundColor.hash === 0) && (cell.style === CellStyle.default); + return cell.foregroundColor.isDefault() && cell.backgroundColor.isDefault() && (cell.style === CellStyle.default); } class StringSerializeHandler extends BaseSerializeHandler { @@ -178,8 +178,8 @@ class StringSerializeHandler extends BaseSerializeHandler { } protected _cellColorChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { - const foregroundColorChanged = cell.foregroundColor.hash !== oldCell.foregroundColor.hash; - const backgroundColorChanged = cell.backgroundColor.hash !== oldCell.backgroundColor.hash; + const foregroundColorChanged = !cell.foregroundColor.equals(oldCell.foregroundColor); + const backgroundColorChanged = !cell.backgroundColor.equals(oldCell.backgroundColor); const sgrSeq = this._sgrSeq; // skip if it's default color style, we will use \x1b[0m to clear every color style later @@ -191,9 +191,9 @@ class StringSerializeHandler extends BaseSerializeHandler { const foregroundColor = cell.foregroundColor; switch (foregroundColor.type) { case 'default': sgrSeq.push('39'); break; - case 'palette16': sgrSeq.push(fgColor256to16(foregroundColor.id).toString()); break; - case 'palette256': sgrSeq.push(`38;5;${foregroundColor.id}`); break; - case 'rgb': const { red, green, blue } = foregroundColor; sgrSeq.push(`38;2;${red};${green};${blue}`); break; + case 'palette16': sgrSeq.push(fgColor256to16(foregroundColor.paletteId()).toString()); break; + case 'palette256': sgrSeq.push(`38;5;${foregroundColor.paletteId()}`); break; + case 'rgb': const [red, green, blue] = foregroundColor.rgbColor(); sgrSeq.push(`38;2;${red};${green};${blue}`); break; } } @@ -201,16 +201,16 @@ class StringSerializeHandler extends BaseSerializeHandler { const backgroundColor = cell.backgroundColor; switch (backgroundColor.type) { case 'default': sgrSeq.push('49'); break; - case 'palette16': sgrSeq.push(bgColor256to16(backgroundColor.id).toString()); break; - case 'palette256': sgrSeq.push(`48;5;${backgroundColor.id}`); break; - case 'rgb': const { red, green, blue } = backgroundColor; sgrSeq.push(`48;2;${red};${green};${blue}`); break; + case 'palette16': sgrSeq.push(bgColor256to16(backgroundColor.paletteId()).toString()); break; + case 'palette256': sgrSeq.push(`48;5;${backgroundColor.paletteId()}`); break; + case 'rgb': const [red, green, blue] = backgroundColor.rgbColor(); sgrSeq.push(`48;2;${red};${green};${blue}`); break; } } } protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { - const foregroundColorChanged = cell.foregroundColor.hash !== oldCell.foregroundColor.hash; - const backgroundColorChanged = cell.backgroundColor.hash !== oldCell.backgroundColor.hash; + const foregroundColorChanged = !cell.foregroundColor.equals(oldCell.foregroundColor); + const backgroundColorChanged = !cell.backgroundColor.equals(oldCell.backgroundColor); const styleChanged = cell.style !== oldCell.style; if ((foregroundColorChanged || backgroundColorChanged || styleChanged) && isDefaultColorStyle(cell)) { diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 377f7acd..61b06b51 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, Color, CellStyle} from 'xterm'; +import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, CellColor as ICellColorApi, CellStyle } from 'xterm'; import { ITerminal } from '../Types'; import { IBufferLine, ICellData } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; @@ -186,7 +186,7 @@ export class Terminal implements ITerminalApi { } class BufferApiView implements IBufferApi { - constructor(private _buffer: IBuffer) {} + constructor(private _buffer: IBuffer) { } public get cursorY(): number { return this._buffer.y; } public get cursorX(): number { return this._buffer.x; } @@ -203,7 +203,7 @@ class BufferApiView implements IBufferApi { } class BufferLineApiView implements IBufferLineApi { - constructor(private _line: IBufferLine) {} + constructor(private _line: IBufferLine) { } public get isWrapped(): boolean { return this._line.isWrapped; } public get length(): number { return this._line.length; } @@ -223,40 +223,66 @@ class BufferLineApiView implements IBufferLineApi { const COLOR_MASK = Attributes.CM_MASK | Attributes.RGB_MASK; +class CellColorApi implements ICellColorApi { + readonly type: 'default' | 'rgb' | 'palette16' | 'palette256'; + readonly value: number = 0; + + constructor(type: 'default' | 'rgb' | 'palette16' | 'palette256', value: number) { + this.type = type; + this.value = value; + } + public isDefault(): boolean { return this.value === 0; } + public equals(c: ICellColorApi): boolean { return this.value === c.value; } + public paletteId(): number { + switch (this.type) { + case 'default': + case 'palette16': + case 'palette256': return this.value & Attributes.PCOLOR_MASK; + } + return -1; + } + public rgbColor(): [number, number, number] { + if (this.type === 'rgb') { + return CellData.toColorRGB(this.value); + } + return [-1, -1, -1]; + } + + public static getDefault(): ICellColorApi { return new CellColorApi('default', 0); } +} + class BufferCellApiView implements IBufferCellApi { - constructor(private _cell: ICellData) {} + constructor(private _cell: ICellData) { } public get char(): string { return this._cell.getChars(); } public get width(): number { return this._cell.getWidth(); } - public get foregroundColor(): Color { + public get foregroundColor(): ICellColorApi { const cell = this._cell; - const hash = cell.fg & COLOR_MASK; + const value = cell.fg & COLOR_MASK; if (cell.isFgDefault()) { - return { type: 'default', hash: 0 }; + return new CellColorApi('default', 0); } else if (cell.isFgPalette()) { switch (cell.getFgColorMode()) { - case Attributes.CM_P16: return { type: 'palette16', hash, id: cell.getFgColor() }; - case Attributes.CM_P256: return { type: 'palette256', hash, id: cell.getFgColor() }; + case Attributes.CM_P16: return new CellColorApi('palette16', value); + case Attributes.CM_P256: return new CellColorApi('palette256', value); } } else if (cell.isFgRGB()) { - const [red, green, blue] = CellData.toColorRGB(cell.fg); - return { type: 'rgb', hash, red, green, blue }; + return new CellColorApi('rgb', value); } throw new Error('Invalid foregroundColor'); } - public get backgroundColor(): Color { + public get backgroundColor(): ICellColorApi { const cell = this._cell; - const hash = cell.bg & COLOR_MASK; + const value = cell.bg & COLOR_MASK; if (cell.isBgDefault()) { - return { type: 'default', hash: 0 }; + return new CellColorApi('default', 0); } else if (cell.isBgPalette()) { switch (cell.getBgColorMode()) { - case Attributes.CM_P16: return { type: 'palette16', hash, id: cell.getBgColor() }; - case Attributes.CM_P256: return { type: 'palette256', hash, id: cell.getBgColor() }; + case Attributes.CM_P16: return new CellColorApi('palette16', value); + case Attributes.CM_P256: return new CellColorApi('palette256', value); } } else if (cell.isBgRGB()) { - const [red, green, blue] = CellData.toColorRGB(cell.bg); - return { type: 'rgb', hash, red, green, blue }; + return new CellColorApi('rgb', value); } throw new Error('Invalid backgroundColor'); } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 4503e1c1..afdfa3e4 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -952,12 +952,11 @@ declare module 'xterm' { */ readonly width: number; - readonly foregroundColor: Color; - readonly backgroundColor: Color; + readonly foregroundColor: CellColor; + readonly backgroundColor: CellColor; readonly style: CellStyle; } - export type Color = IDefaultColor | IPalette16Color | IPalette256Color | IRgbColor; export enum CellStyle { default = 0, // foreground style @@ -971,39 +970,17 @@ declare module 'xterm' { dim = 0x8000000 >>> 16 } - interface ICellColor { - type: string; - hash: number; - } + export class CellColor { + readonly type: 'default' | 'rgb' | 'palette16' | 'palette256'; + readonly value: number; - interface IDefaultColor extends ICellColor { - type: 'default'; - hash: 0; - } + constructor(type: 'default' | 'rgb' | 'palette16' | 'palette256', value: number); - interface IRgbColor extends ICellColor { - type: 'rgb'; - hash: number; + isDefault(): boolean; + equals(c: CellColor): boolean; + paletteId(): number; + rgbColor(): [number, number, number]; - // 0-255 - red: number; - green: number; - blue: number; - } - - interface IPalette16Color extends ICellColor { - type: 'palette16'; - hash: number; - - // 0-15 - id: number; - } - - interface IPalette256Color extends ICellColor { - type: 'palette256'; - hash: number; - - // 0-255 - id: number; + static getDefault(): CellColor; } } From 22a2496360883447629878ab4ec69869255269af Mon Sep 17 00:00:00 2001 From: javacs3 Date: Sat, 24 Aug 2019 12:26:48 +0800 Subject: [PATCH 18/47] fix 'Can't resolve 'xterm' in xterm.js/addons/xterm-addon-serialize/out' --- demo/start.js | 3 ++- demo/tsconfig.json | 3 ++- src/public/Terminal.ts | 20 ++++++++++---------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/demo/start.js b/demo/start.js index a054e939..64f41805 100644 --- a/demo/start.js +++ b/demo/start.js @@ -51,7 +51,8 @@ const clientConfig = { extensions: [ '.tsx', '.ts', '.js' ], alias: { common: path.resolve('./out/common'), - browser: path.resolve('./out/browser') + browser: path.resolve('./out/browser'), + xterm$: path.resolve('./out/public/Terminal.js') } }, output: { diff --git a/demo/tsconfig.json b/demo/tsconfig.json index 2bf76f67..28ea5262 100644 --- a/demo/tsconfig.json +++ b/demo/tsconfig.json @@ -9,7 +9,8 @@ "xterm-addon-attach": ["../addons/xterm-addon-attach"], "xterm-addon-fit": ["../addons/xterm-addon-fit"], "xterm-addon-search": ["../addons/xterm-addon-search"], - "xterm-addon-web-links": ["../addons/xterm-addon-web-links"] + "xterm-addon-web-links": ["../addons/xterm-addon-web-links"], + "xterm-addon-serialize": ["../addons/xterm-addon-serialize"] } }, "include": [ diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 61b06b51..e1fb8d6d 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -223,7 +223,7 @@ class BufferLineApiView implements IBufferLineApi { const COLOR_MASK = Attributes.CM_MASK | Attributes.RGB_MASK; -class CellColorApi implements ICellColorApi { +export class CellColor implements ICellColorApi { readonly type: 'default' | 'rgb' | 'palette16' | 'palette256'; readonly value: number = 0; @@ -248,7 +248,7 @@ class CellColorApi implements ICellColorApi { return [-1, -1, -1]; } - public static getDefault(): ICellColorApi { return new CellColorApi('default', 0); } + public static getDefault(): ICellColorApi { return new CellColor('default', 0); } } class BufferCellApiView implements IBufferCellApi { @@ -260,14 +260,14 @@ class BufferCellApiView implements IBufferCellApi { const cell = this._cell; const value = cell.fg & COLOR_MASK; if (cell.isFgDefault()) { - return new CellColorApi('default', 0); + return new CellColor('default', 0); } else if (cell.isFgPalette()) { switch (cell.getFgColorMode()) { - case Attributes.CM_P16: return new CellColorApi('palette16', value); - case Attributes.CM_P256: return new CellColorApi('palette256', value); + case Attributes.CM_P16: return new CellColor('palette16', value); + case Attributes.CM_P256: return new CellColor('palette256', value); } } else if (cell.isFgRGB()) { - return new CellColorApi('rgb', value); + return new CellColor('rgb', value); } throw new Error('Invalid foregroundColor'); } @@ -275,14 +275,14 @@ class BufferCellApiView implements IBufferCellApi { const cell = this._cell; const value = cell.bg & COLOR_MASK; if (cell.isBgDefault()) { - return new CellColorApi('default', 0); + return new CellColor('default', 0); } else if (cell.isBgPalette()) { switch (cell.getBgColorMode()) { - case Attributes.CM_P16: return new CellColorApi('palette16', value); - case Attributes.CM_P256: return new CellColorApi('palette256', value); + case Attributes.CM_P16: return new CellColor('palette16', value); + case Attributes.CM_P256: return new CellColor('palette256', value); } } else if (cell.isBgRGB()) { - return new CellColorApi('rgb', value); + return new CellColor('rgb', value); } throw new Error('Invalid backgroundColor'); } From f1a17e3cf5c181da82d96b0c34ba0548f09086d2 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Sun, 25 Aug 2019 22:25:45 +0800 Subject: [PATCH 19/47] refactor CellColor constructor --- src/public/Terminal.ts | 40 +++++++++++----------------------------- typings/xterm.d.ts | 2 +- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index e1fb8d6d..28758642 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -227,9 +227,15 @@ export class CellColor implements ICellColorApi { readonly type: 'default' | 'rgb' | 'palette16' | 'palette256'; readonly value: number = 0; - constructor(type: 'default' | 'rgb' | 'palette16' | 'palette256', value: number) { - this.type = type; + constructor(value: number) { this.value = value; + switch (value & Attributes.CM_MASK) { + case Attributes.CM_P16: this.type = 'palette16'; break; + case Attributes.CM_P256: this.type = 'palette256'; break; + case Attributes.CM_RGB: this.type = 'rgb'; break; + case Attributes.CM_DEFAULT: this.type = 'default'; break; + default: throw new Error('Invalid CellColor value'); + } } public isDefault(): boolean { return this.value === 0; } public equals(c: ICellColorApi): boolean { return this.value === c.value; } @@ -248,7 +254,7 @@ export class CellColor implements ICellColorApi { return [-1, -1, -1]; } - public static getDefault(): ICellColorApi { return new CellColor('default', 0); } + public static getDefault(): ICellColorApi { return new CellColor(0); } } class BufferCellApiView implements IBufferCellApi { @@ -257,34 +263,10 @@ class BufferCellApiView implements IBufferCellApi { public get char(): string { return this._cell.getChars(); } public get width(): number { return this._cell.getWidth(); } public get foregroundColor(): ICellColorApi { - const cell = this._cell; - const value = cell.fg & COLOR_MASK; - if (cell.isFgDefault()) { - return new CellColor('default', 0); - } else if (cell.isFgPalette()) { - switch (cell.getFgColorMode()) { - case Attributes.CM_P16: return new CellColor('palette16', value); - case Attributes.CM_P256: return new CellColor('palette256', value); - } - } else if (cell.isFgRGB()) { - return new CellColor('rgb', value); - } - throw new Error('Invalid foregroundColor'); + return new CellColor(this._cell.fg & COLOR_MASK); } public get backgroundColor(): ICellColorApi { - const cell = this._cell; - const value = cell.bg & COLOR_MASK; - if (cell.isBgDefault()) { - return new CellColor('default', 0); - } else if (cell.isBgPalette()) { - switch (cell.getBgColorMode()) { - case Attributes.CM_P16: return new CellColor('palette16', value); - case Attributes.CM_P256: return new CellColor('palette256', value); - } - } else if (cell.isBgRGB()) { - return new CellColor('rgb', value); - } - throw new Error('Invalid backgroundColor'); + return new CellColor(this._cell.bg & COLOR_MASK); } public get style(): CellStyle { return ((this._cell.bg & BgFlags.FM_MASK) >>> 16) | ((this._cell.fg & FgFlags.FM_MASK) >>> 24); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index afdfa3e4..ae11a8f1 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -974,7 +974,7 @@ declare module 'xterm' { readonly type: 'default' | 'rgb' | 'palette16' | 'palette256'; readonly value: number; - constructor(type: 'default' | 'rgb' | 'palette16' | 'palette256', value: number); + constructor(value: number); isDefault(): boolean; equals(c: CellColor): boolean; From ae70ef0fa2855aa5fa0ba0f6c293460f8c3822f7 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Sun, 25 Aug 2019 22:34:20 +0800 Subject: [PATCH 20/47] resolve SerializeAddon importing workaround --- .../src/SerializeAddon.ts | 25 +------------------ src/public/Terminal.ts | 15 ++++++++++- typings/xterm.d.ts | 16 ++++++------ 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index b5d6e08d..b8b57b6b 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -3,30 +3,7 @@ * @license MIT */ -import { Terminal, ITerminalAddon, IBuffer, IBufferCell, CellColor } from 'xterm'; - -// TODO: Workaround here, will remove this later -// If I use `import { CellStyle } from 'xterm'` instead, demo page will raise bellow error -// -// ERROR in ./addons/xterm-addon-serialize/out/SerializeAddon.js -// Module not found: Error: Can't resolve 'xterm' in '/Users/javacs3/Lab/Playground/xterm.js/addons/xterm-addon-serialize/out' -// @ ./addons/xterm-addon-serialize/out/SerializeAddon.js 16:14-30 -// @ ./demo/client.ts -// -// Looks like typescript generate this line `var xterm_1 = require("xterm");` that leads to the error; -// -enum CellStyle { - default = 0, - // foreground style - inverse = 0x4000000 >>> 24, - bold = 0x8000000 >>> 24, - underline = 0x10000000 >>> 24, - blink = 0x20000000 >>> 24, - invisible = 0x40000000 >>> 24, - // background style - italic = 0x4000000 >>> 16, - dim = 0x8000000 >>> 16 -} +import { Terminal, ITerminalAddon, IBuffer, IBufferCell, CellColor, CellStyle } from 'xterm'; function crop(value: number | undefined, low: number, high: number, initial: number): number { if (value === undefined) { diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 28758642..baae367c 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, CellColor as ICellColorApi, CellStyle } from 'xterm'; +import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, CellColor as ICellColorApi } from 'xterm'; import { ITerminal } from '../Types'; import { IBufferLine, ICellData } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; @@ -221,6 +221,19 @@ class BufferLineApiView implements IBufferLineApi { } } +export enum CellStyle { + default = 0, + // foreground style + inverse = 0x4000000 >>> 24, + bold = 0x8000000 >>> 24, + underline = 0x10000000 >>> 24, + blink = 0x20000000 >>> 24, + invisible = 0x40000000 >>> 24, + // background style + italic = 0x4000000 >>> 16, + dim = 0x8000000 >>> 16 +} + const COLOR_MASK = Attributes.CM_MASK | Attributes.RGB_MASK; export class CellColor implements ICellColorApi { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index ae11a8f1..6fcc1e53 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -958,16 +958,16 @@ declare module 'xterm' { } export enum CellStyle { - default = 0, + default, // foreground style - inverse = 0x4000000 >>> 24, - bold = 0x8000000 >>> 24, - underline = 0x10000000 >>> 24, - blink = 0x20000000 >>> 24, - invisible = 0x40000000 >>> 24, + inverse, + bold, + underline, + blink, + invisible, // background style - italic = 0x4000000 >>> 16, - dim = 0x8000000 >>> 16 + italic, + dim } export class CellColor { From cd5f3e9740291e5ca848400b4a3b1558fa937170 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Thu, 29 Aug 2019 23:17:24 +0800 Subject: [PATCH 21/47] update cell color api design --- .../src/SerializeAddon.ts | 140 ++++++------------ src/public/Terminal.ts | 130 ++++++++-------- typings/xterm.d.ts | 116 +++++++++++---- 3 files changed, 201 insertions(+), 185 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index b8b57b6b..0fa5c3dd 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ITerminalAddon, IBuffer, IBufferCell, CellColor, CellStyle } from 'xterm'; +import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm'; function crop(value: number | undefined, low: number, high: number, initial: number): number { if (value === undefined) { @@ -12,19 +12,14 @@ function crop(value: number | undefined, low: number, high: number, initial: num return Math.max(low, Math.min(value, high)); } -class NullBufferCell implements IBufferCell { - char: string = ''; - width: number = 0; - foregroundColor: CellColor = CellColor.getDefault(); - backgroundColor: CellColor = CellColor.getDefault(); - style: CellStyle = CellStyle.default; -} - abstract class BaseSerializeHandler { constructor(private _buffer: IBuffer) { } serialize(startRow: number, endRow: number): string { - let oldCell: IBufferCell = new NullBufferCell(); + // we need two of them to flip between old and new cell + const cell1 = this._buffer.getNullCell(); + const cell2 = this._buffer.getNullCell(); + let oldCell = cell1; this._serializeStart(endRow - startRow); @@ -35,23 +30,22 @@ abstract class BaseSerializeHandler { if (line) { for (let col = 0; col < line.length; col++) { - const cell = line.getCell(col); + const newCell = line.getCell(col, oldCell === cell1 ? cell2 : cell1); - if (!cell) { + if (!newCell) { console.warn(`Can't get cell at row=${row}, col=${col}`); continue; } - if (!cell.foregroundColor.equals(oldCell.foregroundColor) - || !cell.backgroundColor.equals(oldCell.backgroundColor)) { - this._cellColorChanged(cell, oldCell, row, col); + if (!newCell.equalFg(oldCell) || !newCell.equalBg(oldCell)) { + this._cellFgBgChanged(newCell, oldCell, row, col); } - if (cell.style !== oldCell.style) { - this._cellStyleChanged(cell, oldCell, row, col); + if (!newCell.equalFlags(oldCell)) { + this._cellFlagsChanged(newCell, oldCell, row, col); } - this._nextCell(cell, oldCell, row, col); + this._nextCell(newCell, oldCell, row, col); - oldCell = cell; + oldCell = newCell; } } @@ -65,9 +59,9 @@ abstract class BaseSerializeHandler { protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _cellStyleChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } + protected _cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _cellColorChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } + protected _cellFgBgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } protected _lineStart(row: number): void { } @@ -80,33 +74,11 @@ abstract class BaseSerializeHandler { protected _serializeFinished(): string { return ''; } } -function fgColor256to16(c: number): number { - if (0 <= c && c <= 7) { - return 30 + c; - } else if (8 <= c && c <= 15) { - return 82 + c; - } - return -1; -} - -function bgColor256to16(c: number): number { - if (0 <= c && c <= 7) { - return 40 + c; - } else if (8 <= c && c <= 15) { - return 92 + c; - } - return -1; -} - -function isDefaultColorStyle(cell: IBufferCell) { - return cell.foregroundColor.isDefault() && cell.backgroundColor.isDefault() && (cell.style === CellStyle.default); -} - class StringSerializeHandler extends BaseSerializeHandler { private _rowIndex: number = 0; private _allRows: string[] = new Array(); private _currentRow: string = ''; - private _sgrSeq: string[] = []; + private _sgrSeq: number[] = []; constructor(buffer: IBuffer) { super(buffer); @@ -121,76 +93,54 @@ class StringSerializeHandler extends BaseSerializeHandler { this._currentRow = ''; } - protected _cellStyleChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { - const styleChangedMask = cell.style ^ oldCell.style; - const style = cell.style; + protected _cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { const sgrSeq = this._sgrSeq; // skip if it's default color style, we will use \x1b[0m to clear every color style later - if (isDefaultColorStyle(cell)) { - return; - } + if (cell.isDefaultAttibutes() || cell.equalFlags(oldCell)) { return; } - if (styleChangedMask & CellStyle.inverse) { - sgrSeq.push((style & CellStyle.inverse) ? '7' : '27'); - } - if (styleChangedMask & CellStyle.bold) { - sgrSeq.push((style & CellStyle.bold) ? '1' : '22'); - } - if (styleChangedMask & CellStyle.underline) { - sgrSeq.push((style & CellStyle.underline) ? '4' : '24'); - } - if (styleChangedMask & CellStyle.blink) { - sgrSeq.push((style & CellStyle.blink) ? '5' : '25'); - } - if (styleChangedMask & CellStyle.invisible) { - sgrSeq.push((style & CellStyle.invisible) ? '8' : '28'); - } - if (styleChangedMask & CellStyle.italic) { - sgrSeq.push((style & CellStyle.italic) ? '3' : '23'); - } - if (styleChangedMask & CellStyle.dim) { - sgrSeq.push((style & CellStyle.dim) ? '2' : '22'); - } + if (cell.flags.inverse !== oldCell.flags.inverse) { sgrSeq.push(cell.flags.inverse ? 7 : 27); } + if (cell.flags.bold !== oldCell.flags.bold) { sgrSeq.push(cell.flags.bold ? 1 : 22); } + if (cell.flags.underline !== oldCell.flags.underline) { sgrSeq.push(cell.flags.underline ? 4 : 24); } + if (cell.flags.blink !== oldCell.flags.blink) { sgrSeq.push(cell.flags.blink ? 5 : 25); } + if (cell.flags.invisible !== oldCell.flags.invisible) { sgrSeq.push(cell.flags.invisible ? 8 : 28); } + if (cell.flags.italic !== oldCell.flags.italic) { sgrSeq.push(cell.flags.italic ? 3 : 23); } + if (cell.flags.dim !== oldCell.flags.dim) { sgrSeq.push(cell.flags.dim ? 2 : 22); } } - protected _cellColorChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { - const foregroundColorChanged = !cell.foregroundColor.equals(oldCell.foregroundColor); - const backgroundColorChanged = !cell.backgroundColor.equals(oldCell.backgroundColor); + protected _cellFgBgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { const sgrSeq = this._sgrSeq; // skip if it's default color style, we will use \x1b[0m to clear every color style later - if (isDefaultColorStyle(cell)) { - return; - } + if (cell.isDefaultAttibutes()) { return; } - if (foregroundColorChanged) { - const foregroundColor = cell.foregroundColor; - switch (foregroundColor.type) { - case 'default': sgrSeq.push('39'); break; - case 'palette16': sgrSeq.push(fgColor256to16(foregroundColor.paletteId()).toString()); break; - case 'palette256': sgrSeq.push(`38;5;${foregroundColor.paletteId()}`); break; - case 'rgb': const [red, green, blue] = foregroundColor.rgbColor(); sgrSeq.push(`38;2;${red};${green};${blue}`); break; + if (!cell.equalFg(oldCell)) { + const color = cell.fg.color; + switch (cell.fg.colorMode) { + case 'RGB': sgrSeq.push(38, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); break; + case 'P256': sgrSeq.push(38, 5, color); break; + case 'P16': sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)); break; + default: sgrSeq.push(39); break; } } - if (backgroundColorChanged) { - const backgroundColor = cell.backgroundColor; - switch (backgroundColor.type) { - case 'default': sgrSeq.push('49'); break; - case 'palette16': sgrSeq.push(bgColor256to16(backgroundColor.paletteId()).toString()); break; - case 'palette256': sgrSeq.push(`48;5;${backgroundColor.paletteId()}`); break; - case 'rgb': const [red, green, blue] = backgroundColor.rgbColor(); sgrSeq.push(`48;2;${red};${green};${blue}`); break; + if (!cell.equalBg(oldCell)) { + const color = cell.bg.color; + switch (cell.bg.colorMode) { + case 'RGB': sgrSeq.push(48, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); break; + case 'P256': sgrSeq.push(48, 5, color); break; + case 'P16': sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)); break; + default: sgrSeq.push(49); break; } } } protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { - const foregroundColorChanged = !cell.foregroundColor.equals(oldCell.foregroundColor); - const backgroundColorChanged = !cell.backgroundColor.equals(oldCell.backgroundColor); - const styleChanged = cell.style !== oldCell.style; + const fgChanged = !cell.equalFg(oldCell); + const bgChanged = !cell.equalBg(oldCell); + const flagsChanged = !cell.equalFlags(oldCell); - if ((foregroundColorChanged || backgroundColorChanged || styleChanged) && isDefaultColorStyle(cell)) { + if (cell.isDefaultAttibutes() && (fgChanged || bgChanged || flagsChanged)) { this._currentRow += '\x1b[0m'; } diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index baae367c..e1aef88e 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -3,9 +3,9 @@ * @license MIT */ -import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, CellColor as ICellColorApi } from 'xterm'; +import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IBufferCellColor as IBufferCellColorApi, IBufferCellFlags as IBufferCellFlagsApi } from 'xterm'; import { ITerminal } from '../Types'; -import { IBufferLine, ICellData } from 'common/Types'; +import { IBufferLine } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; import { Attributes, FgFlags, BgFlags } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; @@ -14,6 +14,7 @@ import * as Strings from '../browser/LocalizableStrings'; import { IEvent } from 'common/EventEmitter'; import { AddonManager } from './AddonManager'; import { IParams } from 'common/parser/Types'; +import { AttributeData } from 'common/buffer/AttributeData'; export class Terminal implements ITerminalApi { private _core: ITerminal; @@ -200,6 +201,7 @@ class BufferApiView implements IBufferApi { } return new BufferLineApiView(line); } + public getNullCell(): IBufferCellApi { return new BufferCellApiView(new CellData()); } } class BufferLineApiView implements IBufferLineApi { @@ -207,81 +209,81 @@ class BufferLineApiView implements IBufferLineApi { public get isWrapped(): boolean { return this._line.isWrapped; } public get length(): number { return this._line.length; } - public getCell(x: number): IBufferCellApi | undefined { + public getCell(x: number, cell?: BufferCellApiView): IBufferCellApi | undefined { if (x < 0 || x >= this._line.length) { return undefined; } - const cell = new CellData(); - this._line.loadCell(x, cell); - return new BufferCellApiView(cell); + if (cell) { + this._line.loadCell(x, cell.cell); + return cell; + } + return new BufferCellApiView(this._line.loadCell(x, new CellData())); } public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string { return this._line.translateToString(trimRight, startColumn, endColumn); } } -export enum CellStyle { - default = 0, - // foreground style - inverse = 0x4000000 >>> 24, - bold = 0x8000000 >>> 24, - underline = 0x10000000 >>> 24, - blink = 0x20000000 >>> 24, - invisible = 0x40000000 >>> 24, - // background style - italic = 0x4000000 >>> 16, - dim = 0x8000000 >>> 16 -} - -const COLOR_MASK = Attributes.CM_MASK | Attributes.RGB_MASK; - -export class CellColor implements ICellColorApi { - readonly type: 'default' | 'rgb' | 'palette16' | 'palette256'; - readonly value: number = 0; - - constructor(value: number) { - this.value = value; - switch (value & Attributes.CM_MASK) { - case Attributes.CM_P16: this.type = 'palette16'; break; - case Attributes.CM_P256: this.type = 'palette256'; break; - case Attributes.CM_RGB: this.type = 'rgb'; break; - case Attributes.CM_DEFAULT: this.type = 'default'; break; - default: throw new Error('Invalid CellColor value'); - } - } - public isDefault(): boolean { return this.value === 0; } - public equals(c: ICellColorApi): boolean { return this.value === c.value; } - public paletteId(): number { - switch (this.type) { - case 'default': - case 'palette16': - case 'palette256': return this.value & Attributes.PCOLOR_MASK; - } - return -1; - } - public rgbColor(): [number, number, number] { - if (this.type === 'rgb') { - return CellData.toColorRGB(this.value); - } - return [-1, -1, -1]; - } - - public static getDefault(): ICellColorApi { return new CellColor(0); } -} +const fgFlagMask = FgFlags.BOLD | FgFlags.BLINK | FgFlags.INVERSE | FgFlags.INVISIBLE | FgFlags.UNDERLINE; +const bgFlagMask = BgFlags.DIM | BgFlags.ITALIC; +const colorMask = Attributes.CM_MASK | Attributes.RGB_MASK; class BufferCellApiView implements IBufferCellApi { - constructor(private _cell: ICellData) { } - - public get char(): string { return this._cell.getChars(); } - public get width(): number { return this._cell.getWidth(); } - public get foregroundColor(): ICellColorApi { - return new CellColor(this._cell.fg & COLOR_MASK); + public flags: IBufferCellFlagsApi; + public fg: IBufferCellColorApi; + public bg: IBufferCellColorApi; + constructor(public cell: CellData) { + this.flags = { + get bold(): boolean { return !!(cell.fg & FgFlags.BOLD); }, + get underline(): boolean { return !!(cell.fg & FgFlags.UNDERLINE); }, + get blink(): boolean { return !!(cell.fg & FgFlags.BLINK); }, + get inverse(): boolean { return !!(cell.fg & FgFlags.INVERSE); }, + get invisible(): boolean { return !!(cell.fg & FgFlags.INVISIBLE); }, + get italic(): boolean { return !!(cell.bg & BgFlags.ITALIC); }, + get dim(): boolean { return !!(cell.bg & BgFlags.DIM); } + }; + this.fg = { + get colorMode(): 'RGB' | 'P256' | 'P16' | 'DEFAULT' { + switch (cell.getFgColorMode()) { + case Attributes.CM_RGB: return 'RGB'; + case Attributes.CM_P256: return 'P256'; + case Attributes.CM_P16: return 'P16'; + default: return 'DEFAULT'; + } + }, + get color(): number { return cell.getFgColor(); }, + get rgb(): [number, number, number] { return AttributeData.toColorRGB(cell.getFgColor()); } + }; + this.bg = { + get colorMode(): 'RGB' | 'P256' | 'P16' | 'DEFAULT' { + switch (cell.getFgColorMode()) { + case Attributes.CM_RGB: return 'RGB'; + case Attributes.CM_P256: return 'P256'; + case Attributes.CM_P16: return 'P16'; + default: return 'DEFAULT'; + } + }, + get color(): number { return cell.getBgColor(); }, + get rgb(): [number, number, number] { return AttributeData.toColorRGB(cell.getBgColor()); } + }; } - public get backgroundColor(): ICellColorApi { - return new CellColor(this._cell.bg & COLOR_MASK); + public get char(): string { return this.cell.getChars(); } + public get width(): number { return this.cell.getWidth(); } + public isDefaultAttibutes(): boolean { + return this.cell.fg === 0 && this.cell.bg === 0; } - public get style(): CellStyle { - return ((this._cell.bg & BgFlags.FM_MASK) >>> 16) | ((this._cell.fg & FgFlags.FM_MASK) >>> 24); + public equalAttibutes(other: BufferCellApiView): boolean { + return this.cell.fg === other.cell.fg && this.cell.bg === other.cell.bg; + } + public equalFlags(other: BufferCellApiView): boolean { + return (this.cell.fg & fgFlagMask) === (other.cell.fg & fgFlagMask) + && (this.cell.bg & bgFlagMask) === (other.cell.bg & bgFlagMask); + } + public equalFg(other: BufferCellApiView): boolean { + return (this.cell.fg & colorMask) === (other.cell.fg & colorMask); + } + public equalBg(other: BufferCellApiView): boolean { + return (this.cell.bg & colorMask) === (other.cell.bg & colorMask); } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 6fcc1e53..b0415350 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -900,6 +900,13 @@ declare module 'xterm' { * @param y The line index to get. */ getLine(y: number): IBufferLine | undefined; + + /** + * Creates an empty cell object suitable as a cell reference in + * `line.getCell(x, cell)`. Use this to avoid costly recreation of + * cell objects when dealing with tons of cells. + */ + getNullCell(): IBufferCell; } /** @@ -920,8 +927,9 @@ declare module 'xterm' { * behavior. * * @param x The character index to get. + * @param cell Optional cell object to load data into. */ - getCell(x: number): IBufferCell | undefined; + getCell(x: number, cell?: IBufferCell): IBufferCell | undefined; /** * Gets the line as a string. Note that this is gets only the string for the @@ -934,6 +942,49 @@ declare module 'xterm' { translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string; } + /** + * Represents foreground and background color settings of a cell. + */ + interface IBufferCellColor { + /** + * Color mode of the color setting. + * RGB Color is an RGB color, use `.rgb` to grab the different channels. + * P256 Color is an indexed value of the 256 color palette. + * P16 Color is an indexed value of the 8 color palette (+8 for AIX bright colors). + * DEFAULT No color set, thus default color should be used. + */ + colorMode: 'RGB' | 'P256' | 'P16' | 'DEFAULT'; + + /** + * Color value set in the current color mode. + * Note that the color value can only be interpreted in conjunction + * with the color mode: + * RGB color contains 8 bit channels in RGB32 bitorder, e.g. red << 16 | green << 8 | blue + * P256 color contains indexed value 0..255 + * P16 color contains indexed value 0..15 + * DEFAULT color always contains -1 + */ + color: number; + + /** + * Helper to get RGB channels from color mode RGB. Reports channels as [red, green, blue]. + */ + rgb: [number, number, number]; + } + + /** + * Represents style flags of a cell. + */ + interface IBufferCellFlags { + readonly bold: boolean; + readonly underline: boolean; + readonly blink: boolean; + readonly inverse: boolean; + readonly invisible: boolean; + readonly italic: boolean; + readonly dim: boolean; + } + /** * Represents a single cell in the terminal's buffer. */ @@ -952,35 +1003,48 @@ declare module 'xterm' { */ readonly width: number; - readonly foregroundColor: CellColor; - readonly backgroundColor: CellColor; - readonly style: CellStyle; - } + /** + * Text attribute flags like bold, underline etc. + */ + readonly flags: IBufferCellFlags; - export enum CellStyle { - default, - // foreground style - inverse, - bold, - underline, - blink, - invisible, - // background style - italic, - dim - } + /** + * Foreground color. + */ + readonly fg: IBufferCellColor; - export class CellColor { - readonly type: 'default' | 'rgb' | 'palette16' | 'palette256'; - readonly value: number; + /** + * Background color. + */ + readonly bg: IBufferCellColor; - constructor(value: number); + /** + * Whether cells have default attributes (flags and colors). + */ + isDefaultAttibutes(): boolean; - isDefault(): boolean; - equals(c: CellColor): boolean; - paletteId(): number; - rgbColor(): [number, number, number]; + /** + * Whether cells have the same text attributes (flags and colors). + * @param other Other cell. + */ + equalAttibutes(other: IBufferCell): boolean; - static getDefault(): CellColor; + /** + * Whether cells have the same text attribute flags. + * @param other Other cell. + */ + equalFlags(other: IBufferCell): boolean; + + /** + * Whether cells have the same foreground color. + * @param other Other cell. + */ + equalFg(other: IBufferCell): boolean; + + /** + * Whether cells have the same background color. + * @param other Other cell. + */ + equalBg(other: IBufferCell): boolean; } } From e2df7dc96c6c9ef9afd9d9f6abfd2ac0ac026779 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Fri, 16 Aug 2019 21:22:37 +0800 Subject: [PATCH 22/47] add benchmark for serialize addon --- .../src/SerializeAddon.ts | 2 +- package.json | 9 +- test/benchmark/SerializeAddon.benchmark.ts | 84 +++++++++++++++++++ test/benchmark/tsconfig.json | 6 +- yarn.lock | 29 +++++++ 5 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 test/benchmark/SerializeAddon.benchmark.ts diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 0fa5c3dd..c68abab7 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -182,7 +182,7 @@ export class SerializeAddon implements ITerminalAddon { throw new Error('Cannot use addon until it has been loaded'); } - const maxRows = this._terminal.rows; + const maxRows = this._terminal.buffer.length; const handler = new StringSerializeHandler(this._terminal.buffer); rows = crop(rows, 0, maxRows, maxRows); diff --git a/package.json b/package.json index 68467a89..57af066d 100644 --- a/package.json +++ b/package.json @@ -14,15 +14,15 @@ "lint": "tslint 'src/**/*.ts' 'addons/**/*.ts'", "test": "npm run test-unit", "posttest": "npm run lint", - "test-api": "mocha \"**/*.api.js\"", + "test-api": "mocha \"**/SerializeAddon.api.js\"", "test-unit": "node ./bin/test.js", "build": "tsc -b ./tsconfig.all.json", "prepare": "npm run build", "prepublishOnly": "npm run package", "watch": "tsc -b -w ./tsconfig.all.json --preserveWatchOutput", - "benchmark": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json", - "benchmark-baseline": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/test/benchmark/*benchmark.js", - "benchmark-eval": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/test/benchmark/*benchmark.js", + "benchmark": "NODE_PATH=./out:./out-test/benchmark xterm-benchmark -r 5 -c test/benchmark/benchmark.json", + "benchmark-baseline": "NODE_PATH=./out:./out-test/benchmark xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/test/benchmark/SerializeAddon.benchmark.js", + "benchmark-eval": "NODE_PATH=./out:./out-test/benchmark xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/test/benchmark/SerializeAddon.benchmark.js", "clean": "rm -rf lib out addons/*/lib addons/*/out" }, "devDependencies": { @@ -49,6 +49,7 @@ "tslint-consistent-codestyle": "^1.13.0", "typescript": "3.5", "utf8": "^3.0.0", + "v8-profiler-node8": "^6.1.1", "webpack": "^4.35.3", "webpack-cli": "^3.1.0", "ws": "^7.0.0", diff --git a/test/benchmark/SerializeAddon.benchmark.ts b/test/benchmark/SerializeAddon.benchmark.ts new file mode 100644 index 00000000..9423f400 --- /dev/null +++ b/test/benchmark/SerializeAddon.benchmark.ts @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { perfContext, before, after, ThroughputRuntimeCase } from 'xterm-benchmark'; + +import * as fs from 'fs' +import { spawn } from 'node-pty'; +import * as profiler from 'v8-profiler-node8'; +import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder'; +import { Terminal } from 'public/Terminal'; +import { SerializeAddon } from 'addons/xterm-addon-serialize/src/SerializeAddon'; + +class TestTerminal extends Terminal { + writeSync(data: string): void { + (this)._core.writeBuffer.push(data); + (this)._core._innerWrite(); + } + writeSyncUtf8(data: Uint8Array): void { + (this)._core.writeBufferUtf8.push(data); + (this)._core._innerWriteUtf8(); + } +} + +perfContext('Terminal: sh -c "dd if=/dev/random count=40 bs=1k | hexdump | lolcat -f"', () => { + let content = ''; + let contentUtf8: Uint8Array; + + before(async () => { + const p = spawn('sh', ['-c', 'dd if=/dev/random count=40 bs=1k | hexdump | lolcat -f'], { + name: 'xterm-256color', + cols: 80, + rows: 25, + cwd: process.env.HOME, + env: process.env, + encoding: (null as unknown as string) // needs to be fixed in node-pty + }); + const chunks: Buffer[] = []; + let length = 0; + p.on('data', data => { + chunks.push(data as unknown as Buffer); + length += data.length; + }); + await new Promise(resolve => p.on('exit', () => resolve())); + contentUtf8 = Buffer.concat(chunks, length); + // translate to content string + const buffer = new Uint32Array(contentUtf8.length); + const decoder = new Utf8ToUtf32(); + const codepoints = decoder.decode(contentUtf8, buffer); + for (let i = 0; i < codepoints; ++i) { + content += stringFromCodePoint(buffer[i]); + // peek into content to force flat repr in v8 + if (!(i % 10000000)) { + content[i]; + } + } + }); + + perfContext('serialize', () => { + let terminal: TestTerminal; + let serializeAddon = new SerializeAddon(); + before(() => { + terminal = new TestTerminal({ cols: 80, rows: 25, scrollback: 5000 }); + serializeAddon.activate(terminal); + terminal.writeSync(content); + profiler.startProfiling(); + }); + after(() => { + const p1 = profiler.stopProfiling(); + p1.export((err, res) => { + if (err) { + console.error(err); + return; + } + console.log(p1.getHeader()); + fs.writeFileSync('serialize-profile.cpuprofile', res); + }) + }) + new ThroughputRuntimeCase('', () => { + return { payloadSize: serializeAddon.serialize().length }; + }, { fork: false }).showAverageThroughput(); + }); +}); diff --git a/test/benchmark/tsconfig.json b/test/benchmark/tsconfig.json index 8b93dcb1..51abeba5 100644 --- a/test/benchmark/tsconfig.json +++ b/test/benchmark/tsconfig.json @@ -16,7 +16,9 @@ "paths": { "common/*": [ "../../src/common/*" ], "browser/*": [ "../../src/browser/*" ], - "Terminal": [ "../../src/Terminal" ] + "addons/xterm-addon-serialize/src/*": ["../../addons/xterm-addon-serialize/src/*"], + "public/*": ["../../src/public/*"], + "Terminal": ["../../src/Terminal"] }, }, "include": [ @@ -31,4 +33,4 @@ { "path": "../../src/common" }, { "path": "../../src/browser" }, ] -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index a67ede71..30f2ea79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3143,6 +3143,11 @@ nan@2.10.0, nan@^2.9.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== +nan@^2.14.0: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -3237,6 +3242,22 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" +node-pre-gyp@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz#df9ab7b68dd6498137717838e4f92a33fc9daa42" + integrity sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ== + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + node-pty@0.7.6: version "0.7.6" resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.7.6.tgz#bff6148c9c5836ca7e73c7aaaec067dcbdac2f7b" @@ -4842,6 +4863,14 @@ v8-compile-cache@^2.0.0: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c" integrity sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw== +v8-profiler-node8@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/v8-profiler-node8/-/v8-profiler-node8-6.1.1.tgz#2046dd46d80744784f9e94bce1411717435c1f76" + integrity sha512-mKS7TXRRYi70hvbv5c1tk9AbuqNrtbLc+jFLlsZ2TpaC1l5lWryBlDLZKJ1JP6hjSbMEjW1ucjWLSaKsaPnGXg== + dependencies: + nan "^2.14.0" + node-pre-gyp "^0.13.0" + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" From 3543155953ff08bcb7d09c3a5f1d038b8b281f16 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Sun, 8 Sep 2019 23:09:28 +0800 Subject: [PATCH 23/47] fix typo --- src/public/Terminal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index e1aef88e..9f3aca87 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -257,7 +257,7 @@ class BufferCellApiView implements IBufferCellApi { }; this.bg = { get colorMode(): 'RGB' | 'P256' | 'P16' | 'DEFAULT' { - switch (cell.getFgColorMode()) { + switch (cell.getBgColorMode()) { case Attributes.CM_RGB: return 'RGB'; case Attributes.CM_P256: return 'P256'; case Attributes.CM_P16: return 'P16'; From 7ec99c2469fa96876e4c24c1e358bc4f2af06e0c Mon Sep 17 00:00:00 2001 From: javacs3 Date: Wed, 20 Nov 2019 22:56:44 +0800 Subject: [PATCH 24/47] update IBufferCell API --- .../src/SerializeAddon.ts | 89 +++++++------- package.json | 2 +- src/public/Terminal.ts | 95 +++++++-------- typings/xterm.d.ts | 110 +++++------------- 4 files changed, 114 insertions(+), 182 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index c68abab7..66796ecd 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -12,6 +12,7 @@ function crop(value: number | undefined, low: number, high: number, initial: num return Math.max(low, Math.min(value, high)); } +// TODO: Refine this template class later abstract class BaseSerializeHandler { constructor(private _buffer: IBuffer) { } @@ -21,12 +22,12 @@ abstract class BaseSerializeHandler { const cell2 = this._buffer.getNullCell(); let oldCell = cell1; - this._serializeStart(endRow - startRow); + this.serializeStart(endRow - startRow); for (let row = startRow; row < endRow; row++) { const line = this._buffer.getLine(row); - this._lineStart(row); + this.lineStart(row); if (line) { for (let col = 0; col < line.length; col++) { @@ -37,41 +38,41 @@ abstract class BaseSerializeHandler { continue; } if (!newCell.equalFg(oldCell) || !newCell.equalBg(oldCell)) { - this._cellFgBgChanged(newCell, oldCell, row, col); + this.cellFgBgChanged(newCell, oldCell, row, col); } if (!newCell.equalFlags(oldCell)) { - this._cellFlagsChanged(newCell, oldCell, row, col); + this.cellFlagsChanged(newCell, oldCell, row, col); } - this._nextCell(newCell, oldCell, row, col); + this.nextCell(newCell, oldCell, row, col); oldCell = newCell; } } - this._lineEnd(row); + this.lineEnd(row); } - this._serializeEnd(); + this.serializeEnd(); - return this._serializeFinished(); + return this.serializeFinished(); } - protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } + protected nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } + protected cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _cellFgBgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } + protected cellFgBgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _lineStart(row: number): void { } + protected lineStart(row: number): void { } - protected _lineEnd(row: number): void { } + protected lineEnd(row: number): void { } - protected _serializeStart(rows: number): void { } + protected serializeStart(rows: number): void { } - protected _serializeEnd(): void { } + protected serializeEnd(): void { } - protected _serializeFinished(): string { return ''; } + protected serializeFinished(): string { return ''; } } class StringSerializeHandler extends BaseSerializeHandler { @@ -84,63 +85,59 @@ class StringSerializeHandler extends BaseSerializeHandler { super(buffer); } - protected _serializeStart(rows: number): void { + protected serializeStart(rows: number): void { this._allRows = new Array(rows); } - protected _lineEnd(row: number): void { + protected lineEnd(row: number): void { this._allRows[this._rowIndex++] = this._currentRow; this._currentRow = ''; } - protected _cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + protected cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { const sgrSeq = this._sgrSeq; // skip if it's default color style, we will use \x1b[0m to clear every color style later - if (cell.isDefaultAttibutes() || cell.equalFlags(oldCell)) { return; } + if (cell.isAttributeDefault() || cell.equalFlags(oldCell)) { return; } - if (cell.flags.inverse !== oldCell.flags.inverse) { sgrSeq.push(cell.flags.inverse ? 7 : 27); } - if (cell.flags.bold !== oldCell.flags.bold) { sgrSeq.push(cell.flags.bold ? 1 : 22); } - if (cell.flags.underline !== oldCell.flags.underline) { sgrSeq.push(cell.flags.underline ? 4 : 24); } - if (cell.flags.blink !== oldCell.flags.blink) { sgrSeq.push(cell.flags.blink ? 5 : 25); } - if (cell.flags.invisible !== oldCell.flags.invisible) { sgrSeq.push(cell.flags.invisible ? 8 : 28); } - if (cell.flags.italic !== oldCell.flags.italic) { sgrSeq.push(cell.flags.italic ? 3 : 23); } - if (cell.flags.dim !== oldCell.flags.dim) { sgrSeq.push(cell.flags.dim ? 2 : 22); } + if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); } + if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); } + if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); } + if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); } + 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); } } - protected _cellFgBgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + protected cellFgBgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { const sgrSeq = this._sgrSeq; // skip if it's default color style, we will use \x1b[0m to clear every color style later - if (cell.isDefaultAttibutes()) { return; } + if (cell.isAttributeDefault()) { return; } if (!cell.equalFg(oldCell)) { - const color = cell.fg.color; - switch (cell.fg.colorMode) { - case 'RGB': sgrSeq.push(38, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); break; - case 'P256': sgrSeq.push(38, 5, color); break; - case 'P16': sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)); break; - default: sgrSeq.push(39); break; - } + const color = cell.getFgColor(); + if (cell.isFgRGB()) { sgrSeq.push(38, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); } + else if (cell.isFgPalette256()) { sgrSeq.push(38, 5, color); } + else if (cell.isFgPalette16()) { sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)); } + else { sgrSeq.push(39); } } if (!cell.equalBg(oldCell)) { - const color = cell.bg.color; - switch (cell.bg.colorMode) { - case 'RGB': sgrSeq.push(48, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); break; - case 'P256': sgrSeq.push(48, 5, color); break; - case 'P16': sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)); break; - default: sgrSeq.push(49); break; - } + const color = cell.getBgColor(); + if (cell.isBgRGB()) { sgrSeq.push(48, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); } + else if (cell.isBgPalette256()) { sgrSeq.push(48, 5, color); } + else if (cell.isBgPalette16()) { sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)); } + else { sgrSeq.push(49); } } } - protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + protected nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { const fgChanged = !cell.equalFg(oldCell); const bgChanged = !cell.equalBg(oldCell); const flagsChanged = !cell.equalFlags(oldCell); - if (cell.isDefaultAttibutes() && (fgChanged || bgChanged || flagsChanged)) { + if (cell.isAttributeDefault() && (fgChanged || bgChanged || flagsChanged)) { this._currentRow += '\x1b[0m'; } @@ -152,7 +149,7 @@ class StringSerializeHandler extends BaseSerializeHandler { this._currentRow += cell.char; } - protected _serializeFinished(): string { + protected serializeFinished(): string { let rowEnd = this._allRows.length; for (; rowEnd > 0; rowEnd--) { diff --git a/package.json b/package.json index 57af066d..19ccf0d9 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "lint": "tslint 'src/**/*.ts' 'addons/**/*.ts'", "test": "npm run test-unit", "posttest": "npm run lint", - "test-api": "mocha \"**/SerializeAddon.api.js\"", + "test-api": "mocha \"**/*.api.js\"", "test-unit": "node ./bin/test.js", "build": "tsc -b ./tsconfig.all.json", "prepare": "npm run build", diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 9f3aca87..540ef638 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IBufferCellColor as IBufferCellColorApi, IBufferCellFlags as IBufferCellFlagsApi } from 'xterm'; +import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm'; import { ITerminal } from '../Types'; import { IBufferLine } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; @@ -14,7 +14,6 @@ import * as Strings from '../browser/LocalizableStrings'; import { IEvent } from 'common/EventEmitter'; import { AddonManager } from './AddonManager'; import { IParams } from 'common/parser/Types'; -import { AttributeData } from 'common/buffer/AttributeData'; export class Terminal implements ITerminalApi { private _core: ITerminal; @@ -225,65 +224,55 @@ class BufferLineApiView implements IBufferLineApi { } } -const fgFlagMask = FgFlags.BOLD | FgFlags.BLINK | FgFlags.INVERSE | FgFlags.INVISIBLE | FgFlags.UNDERLINE; -const bgFlagMask = BgFlags.DIM | BgFlags.ITALIC; -const colorMask = Attributes.CM_MASK | Attributes.RGB_MASK; +const FG_FLAG_MASK = FgFlags.BOLD | FgFlags.BLINK | FgFlags.INVERSE | FgFlags.INVISIBLE | FgFlags.UNDERLINE; +const BG_FLAG_MASK = BgFlags.DIM | BgFlags.ITALIC; +const COLOR_MASK = Attributes.CM_MASK | Attributes.RGB_MASK; class BufferCellApiView implements IBufferCellApi { - public flags: IBufferCellFlagsApi; - public fg: IBufferCellColorApi; - public bg: IBufferCellColorApi; - constructor(public cell: CellData) { - this.flags = { - get bold(): boolean { return !!(cell.fg & FgFlags.BOLD); }, - get underline(): boolean { return !!(cell.fg & FgFlags.UNDERLINE); }, - get blink(): boolean { return !!(cell.fg & FgFlags.BLINK); }, - get inverse(): boolean { return !!(cell.fg & FgFlags.INVERSE); }, - get invisible(): boolean { return !!(cell.fg & FgFlags.INVISIBLE); }, - get italic(): boolean { return !!(cell.bg & BgFlags.ITALIC); }, - get dim(): boolean { return !!(cell.bg & BgFlags.DIM); } - }; - this.fg = { - get colorMode(): 'RGB' | 'P256' | 'P16' | 'DEFAULT' { - switch (cell.getFgColorMode()) { - case Attributes.CM_RGB: return 'RGB'; - case Attributes.CM_P256: return 'P256'; - case Attributes.CM_P16: return 'P16'; - default: return 'DEFAULT'; - } - }, - get color(): number { return cell.getFgColor(); }, - get rgb(): [number, number, number] { return AttributeData.toColorRGB(cell.getFgColor()); } - }; - this.bg = { - get colorMode(): 'RGB' | 'P256' | 'P16' | 'DEFAULT' { - switch (cell.getBgColorMode()) { - case Attributes.CM_RGB: return 'RGB'; - case Attributes.CM_P256: return 'P256'; - case Attributes.CM_P16: return 'P16'; - default: return 'DEFAULT'; - } - }, - get color(): number { return cell.getBgColor(); }, - get rgb(): [number, number, number] { return AttributeData.toColorRGB(cell.getBgColor()); } - }; - } + constructor(public cell: CellData) {} + public get char(): string { return this.cell.getChars(); } public get width(): number { return this.cell.getWidth(); } - public isDefaultAttibutes(): boolean { - return this.cell.fg === 0 && this.cell.bg === 0; - } - public equalAttibutes(other: BufferCellApiView): boolean { - return this.cell.fg === other.cell.fg && this.cell.bg === other.cell.bg; - } + + public getWidth(): number { return this.cell.getWidth(); } + public getChars(): string { return this.cell.getChars(); } + public getCode(): number { return this.cell.getCode(); } + + public isInverse(): number { return this.cell.isInverse(); } + public isBold(): number { return this.cell.isBold(); } + public isUnderline(): number { return this.cell.isUnderline(); } + public isBlink(): number { return this.cell.isBlink(); } + public isInvisible(): number { return this.cell.isInvisible(); } + public isItalic(): number { return this.cell.isItalic(); } + public isDim(): number { return this.cell.isDim(); } + + public getFgColorMode(): number { return this.cell.getFgColorMode(); } + public getBgColorMode(): number { return this.cell.getBgColorMode(); } + public isFgRGB(): boolean { return this.cell.isFgRGB(); } + public isBgRGB(): boolean { return this.cell.isBgRGB(); } + public isFgPalette(): boolean { return this.cell.isFgPalette(); } + public isBgPalette(): boolean { return this.cell.isBgPalette(); } + public isFgPalette16(): boolean { return this.cell.getFgColorMode() == Attributes.CM_P16; } + public isBgPalette16(): boolean { return this.cell.getBgColorMode() == Attributes.CM_P16; } + public isFgPalette256(): boolean { return this.cell.getFgColorMode() == Attributes.CM_P256; } + public isBgPalette256(): boolean { return this.cell.getBgColorMode() == Attributes.CM_P256; } + + public isAttributeDefault(): boolean { return this.cell.fg === 0 && this.cell.bg === 0; } + public isFgDefault(): boolean { return this.cell.isFgDefault(); } + public isBgDefault(): boolean { return this.cell.isBgDefault(); } + + public getFgColor(): number { return this.cell.getFgColor(); } + public getBgColor(): number { return this.cell.getBgColor(); } + + public equalFlags(other: BufferCellApiView): boolean { - return (this.cell.fg & fgFlagMask) === (other.cell.fg & fgFlagMask) - && (this.cell.bg & bgFlagMask) === (other.cell.bg & bgFlagMask); + return (this.cell.fg & FG_FLAG_MASK) === (other.cell.fg & FG_FLAG_MASK) + && (this.cell.bg & BG_FLAG_MASK) === (other.cell.bg & BG_FLAG_MASK); } public equalFg(other: BufferCellApiView): boolean { - return (this.cell.fg & colorMask) === (other.cell.fg & colorMask); + return (this.cell.fg & COLOR_MASK) === (other.cell.fg & COLOR_MASK); } public equalBg(other: BufferCellApiView): boolean { - return (this.cell.bg & colorMask) === (other.cell.bg & colorMask); + return (this.cell.bg & COLOR_MASK) === (other.cell.bg & COLOR_MASK); } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index b0415350..5043b867 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -942,49 +942,6 @@ declare module 'xterm' { translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string; } - /** - * Represents foreground and background color settings of a cell. - */ - interface IBufferCellColor { - /** - * Color mode of the color setting. - * RGB Color is an RGB color, use `.rgb` to grab the different channels. - * P256 Color is an indexed value of the 256 color palette. - * P16 Color is an indexed value of the 8 color palette (+8 for AIX bright colors). - * DEFAULT No color set, thus default color should be used. - */ - colorMode: 'RGB' | 'P256' | 'P16' | 'DEFAULT'; - - /** - * Color value set in the current color mode. - * Note that the color value can only be interpreted in conjunction - * with the color mode: - * RGB color contains 8 bit channels in RGB32 bitorder, e.g. red << 16 | green << 8 | blue - * P256 color contains indexed value 0..255 - * P16 color contains indexed value 0..15 - * DEFAULT color always contains -1 - */ - color: number; - - /** - * Helper to get RGB channels from color mode RGB. Reports channels as [red, green, blue]. - */ - rgb: [number, number, number]; - } - - /** - * Represents style flags of a cell. - */ - interface IBufferCellFlags { - readonly bold: boolean; - readonly underline: boolean; - readonly blink: boolean; - readonly inverse: boolean; - readonly invisible: boolean; - readonly italic: boolean; - readonly dim: boolean; - } - /** * Represents a single cell in the terminal's buffer. */ @@ -1003,48 +960,37 @@ declare module 'xterm' { */ readonly width: number; - /** - * Text attribute flags like bold, underline etc. - */ - readonly flags: IBufferCellFlags; + getWidth(): number; + getChars(): string; + getCode(): number; - /** - * Foreground color. - */ - readonly fg: IBufferCellColor; + getFgColorMode(): number; + getBgColorMode(): number; + getFgColor(): number; + getBgColor(): number; - /** - * Background color. - */ - readonly bg: IBufferCellColor; + isInverse(): number; + isBold(): number; + isUnderline(): number; + isBlink(): number; + isInvisible(): number; + isItalic(): number; + isDim(): number; - /** - * Whether cells have default attributes (flags and colors). - */ - isDefaultAttibutes(): boolean; + isFgRGB(): boolean; + isBgRGB(): boolean; + isFgPalette(): boolean; + isBgPalette(): boolean; + isFgPalette16(): boolean; + isBgPalette16(): boolean; + isFgPalette256(): boolean; + isBgPalette256(): boolean; + isAttributeDefault(): boolean; + isFgDefault(): boolean; + isBgDefault(): boolean; - /** - * Whether cells have the same text attributes (flags and colors). - * @param other Other cell. - */ - equalAttibutes(other: IBufferCell): boolean; - - /** - * Whether cells have the same text attribute flags. - * @param other Other cell. - */ - equalFlags(other: IBufferCell): boolean; - - /** - * Whether cells have the same foreground color. - * @param other Other cell. - */ - equalFg(other: IBufferCell): boolean; - - /** - * Whether cells have the same background color. - * @param other Other cell. - */ - equalBg(other: IBufferCell): boolean; + equalFg(cell: IBufferCell): boolean; + equalBg(cell: IBufferCell): boolean; + equalFlags(cell: IBufferCell): boolean; } } From 1093b7c3d3d4a27183fe2a6e115c9f6e5a14f00b Mon Sep 17 00:00:00 2001 From: javacs3 Date: Thu, 21 Nov 2019 20:53:53 +0800 Subject: [PATCH 25/47] fix lint error --- .../src/SerializeAddon.ts | 44 +++++++++---------- src/public/Terminal.ts | 8 ++-- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 66796ecd..b0071865 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -22,12 +22,12 @@ abstract class BaseSerializeHandler { const cell2 = this._buffer.getNullCell(); let oldCell = cell1; - this.serializeStart(endRow - startRow); + this._serializeStart(endRow - startRow); for (let row = startRow; row < endRow; row++) { const line = this._buffer.getLine(row); - this.lineStart(row); + this._lineStart(row); if (line) { for (let col = 0; col < line.length; col++) { @@ -38,41 +38,41 @@ abstract class BaseSerializeHandler { continue; } if (!newCell.equalFg(oldCell) || !newCell.equalBg(oldCell)) { - this.cellFgBgChanged(newCell, oldCell, row, col); + this._cellFgBgChanged(newCell, oldCell, row, col); } if (!newCell.equalFlags(oldCell)) { - this.cellFlagsChanged(newCell, oldCell, row, col); + this._cellFlagsChanged(newCell, oldCell, row, col); } - this.nextCell(newCell, oldCell, row, col); + this._nextCell(newCell, oldCell, row, col); oldCell = newCell; } } - this.lineEnd(row); + this._lineEnd(row); } - this.serializeEnd(); + this._serializeEnd(); - return this.serializeFinished(); + return this._serializeFinished(); } - protected nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } + protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } + protected _cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected cellFgBgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } + protected _cellFgBgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected lineStart(row: number): void { } + protected _lineStart(row: number): void { } - protected lineEnd(row: number): void { } + protected _lineEnd(row: number): void { } - protected serializeStart(rows: number): void { } + protected _serializeStart(rows: number): void { } - protected serializeEnd(): void { } + protected _serializeEnd(): void { } - protected serializeFinished(): string { return ''; } + protected _serializeFinished(): string { return ''; } } class StringSerializeHandler extends BaseSerializeHandler { @@ -85,16 +85,16 @@ class StringSerializeHandler extends BaseSerializeHandler { super(buffer); } - protected serializeStart(rows: number): void { + protected _serializeStart(rows: number): void { this._allRows = new Array(rows); } - protected lineEnd(row: number): void { + protected _lineEnd(row: number): void { this._allRows[this._rowIndex++] = this._currentRow; this._currentRow = ''; } - protected cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + protected _cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { const sgrSeq = this._sgrSeq; // skip if it's default color style, we will use \x1b[0m to clear every color style later @@ -109,7 +109,7 @@ class StringSerializeHandler extends BaseSerializeHandler { if (cell.isDim() !== oldCell.isDim()) { sgrSeq.push(cell.isDim() ? 2 : 22); } } - protected cellFgBgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + protected _cellFgBgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { const sgrSeq = this._sgrSeq; // skip if it's default color style, we will use \x1b[0m to clear every color style later @@ -132,7 +132,7 @@ class StringSerializeHandler extends BaseSerializeHandler { } } - protected nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { const fgChanged = !cell.equalFg(oldCell); const bgChanged = !cell.equalBg(oldCell); const flagsChanged = !cell.equalFlags(oldCell); @@ -149,7 +149,7 @@ class StringSerializeHandler extends BaseSerializeHandler { this._currentRow += cell.char; } - protected serializeFinished(): string { + protected _serializeFinished(): string { let rowEnd = this._allRows.length; for (; rowEnd > 0; rowEnd--) { diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 540ef638..0cfd0560 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -252,10 +252,10 @@ class BufferCellApiView implements IBufferCellApi { public isBgRGB(): boolean { return this.cell.isBgRGB(); } public isFgPalette(): boolean { return this.cell.isFgPalette(); } public isBgPalette(): boolean { return this.cell.isBgPalette(); } - public isFgPalette16(): boolean { return this.cell.getFgColorMode() == Attributes.CM_P16; } - public isBgPalette16(): boolean { return this.cell.getBgColorMode() == Attributes.CM_P16; } - public isFgPalette256(): boolean { return this.cell.getFgColorMode() == Attributes.CM_P256; } - public isBgPalette256(): boolean { return this.cell.getBgColorMode() == Attributes.CM_P256; } + public isFgPalette16(): boolean { return this.cell.getFgColorMode() === Attributes.CM_P16; } + public isBgPalette16(): boolean { return this.cell.getBgColorMode() === Attributes.CM_P16; } + public isFgPalette256(): boolean { return this.cell.getFgColorMode() === Attributes.CM_P256; } + public isBgPalette256(): boolean { return this.cell.getBgColorMode() === Attributes.CM_P256; } public isAttributeDefault(): boolean { return this.cell.fg === 0 && this.cell.bg === 0; } public isFgDefault(): boolean { return this.cell.isFgDefault(); } From 709214a4145fb919bd90f343059568124e16e52f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 24 Nov 2019 10:06:41 -0800 Subject: [PATCH 26/47] Add webpacked version to client --- demo/client.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/client.ts b/demo/client.ts index e045dc43..d607cb07 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -21,6 +21,7 @@ import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon'; // import { AttachAddon } from 'xterm-addon-attach'; // import { FitAddon } from 'xterm-addon-fit'; // import { SearchAddon, ISearchOptions } from 'xterm-addon-search'; +// import { SerializeAddon } from 'xterm-addon-serialize'; // import { WebLinksAddon } from 'xterm-addon-web-links'; // import { WebglAddon } from 'xterm-addon-webgl'; From 45af02590eec350bd73e6edf643000ac4387ed51 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 24 Nov 2019 10:15:31 -0800 Subject: [PATCH 27/47] Make serialize addon api tests fast --- .../src/SerializeAddon.api.ts | 185 ++++-------------- 1 file changed, 41 insertions(+), 144 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts index 6fbf9af2..a316f34e 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -4,7 +4,6 @@ */ import * as puppeteer from 'puppeteer'; -import * as util from 'util'; import { assert } from 'chai'; import { ITerminalOptions } from 'xterm'; @@ -17,42 +16,30 @@ const height = 600; describe('SerializeAddon', () => { before(async function (): Promise { - this.timeout(20000); browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, - slowMo: 80, - devtools: true, args: [`--window-size=${width},${height}`] }); page = (await browser.pages())[0]; await page.setViewport({ width, height }); - }); - - after(async () => { - await browser.close(); - }); - - beforeEach(async function (): Promise { - this.timeout(20000); await page.goto(APP); - }); - - it('empty content', async function (): Promise { - this.timeout(20000); - const rows = 10; - const cols = 10; - - await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await openTerminal({ rows: 10, cols: 10, rendererType: 'dom' }); await page.evaluate(` window.serializeAddon = new SerializeAddon(); window.term.loadAddon(window.serializeAddon); `); + }); + after(async () => await browser.close()); + beforeEach(async () => await page.evaluate(`window.term.reset()`)); + + it('empty content', async function (): Promise { + const rows = 10; + const cols = 10; assert.equal(await page.evaluate(`serializeAddon.serialize();`), ''); }); it('trim last empty lines', async function (): Promise { - this.timeout(20000); const cols = 10; const lines = [ '', @@ -67,70 +54,37 @@ describe('SerializeAddon', () => { '', '' ]; - const rows = lines.length; - - await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); - await page.evaluate(` - window.serializeAddon = new SerializeAddon(); - window.term.loadAddon(window.serializeAddon); - window.term.write(${util.inspect(lines.join('\r\n'))}); - `); - + await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.slice(0, 8).join('\r\n')); }); it('digits content', async function (): Promise { - this.timeout(20000); const rows = 10; const cols = 10; const digitsLine = digitsString(cols); const lines = newArray(digitsLine, rows); - - await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); - await page.evaluate(` - window.serializeAddon = new SerializeAddon(); - window.term.loadAddon(window.serializeAddon); - window.term.write(${util.inspect(lines.join('\r\n'))}); - `); - + await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); it('serialize half rows of content', async function (): Promise { - this.timeout(20000); const rows = 10; const halfRows = rows >> 1; const cols = 10; const lines = newArray((index: number) => digitsString(cols, index), rows); - - await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); - await page.evaluate(` - window.serializeAddon = new SerializeAddon(); - window.term.loadAddon(window.serializeAddon); - window.term.write(${util.inspect(lines.join('\r\n'))}); - `); - + await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize(${halfRows});`), lines.slice(halfRows, 2 * halfRows).join('\r\n')); }); it('serialize 0 rows of content', async function (): Promise { - this.timeout(20000); const rows = 10; const cols = 10; const lines = newArray((index: number) => digitsString(cols, index), rows); - - await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); - await page.evaluate(` - window.serializeAddon = new SerializeAddon(); - window.term.loadAddon(window.serializeAddon); - window.term.write(${util.inspect(lines.join('\r\n'))}); - `); - + await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), ''); }); it('serialize all rows of content with color16', async function (): Promise { - this.timeout(20000); const cols = 10; const color16 = [ 30, 31, 32, 33, 34, 35, 36, 37, // Set foreground color @@ -143,19 +97,11 @@ describe('SerializeAddon', () => { (index: number) => digitsString(cols, index, `\x1b[${color16[index % color16.length]}m`), rows ); - - await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); - await page.evaluate(` - window.serializeAddon = new SerializeAddon(); - window.term.loadAddon(window.serializeAddon); - window.term.write(${util.inspect(lines.join('\r\n'))}); - `); - + await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); it('serialize all rows of content with fg/bg flags', async function (): Promise { - this.timeout(20000); const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -172,38 +118,22 @@ describe('SerializeAddon', () => { mkSGR(NO_INVISIBLE) + line ]; const rows = lines.length; - - await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); - await page.evaluate(` - window.serializeAddon = new SerializeAddon(); - window.term.loadAddon(window.serializeAddon); - window.term.write(${util.inspect(lines.join('\r\n'))}); - `); - + await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); it('serialize all rows of content with color256', async function (): Promise { - this.timeout(20000); const rows = 32; const cols = 10; const lines = newArray( (index: number) => digitsString(cols, index, `\x1b[38;5;${index}m`), rows ); - - await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); - await page.evaluate(` - window.serializeAddon = new SerializeAddon(); - window.term.loadAddon(window.serializeAddon); - window.term.write(${util.inspect(lines.join('\r\n'))}); - `); - + await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); it('serialize all rows of content with color16 and style separately', async function (): Promise { - this.timeout(20000); const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -218,20 +148,11 @@ describe('SerializeAddon', () => { mkSGR(BG_RESET) + line, // Underlined, Inverse mkSGR(NORMAL) + line // Back to normal ]; - const rows = lines.length; - - await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); - await page.evaluate(` - window.serializeAddon = new SerializeAddon(); - window.term.loadAddon(window.serializeAddon); - window.term.write(${util.inspect(lines.join('\r\n'))}); - `); - + await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); it('serialize all rows of content with color16 and style together', async function (): Promise { - this.timeout(20000); const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -249,20 +170,11 @@ describe('SerializeAddon', () => { mkSGR(FG_RESET, ITALIC) + line, // bg Yellow, Italic mkSGR(BG_RESET) + line // Italic ]; - const rows = lines.length; - - await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); - await page.evaluate(` - window.serializeAddon = new SerializeAddon(); - window.term.loadAddon(window.serializeAddon); - window.term.write(${util.inspect(lines.join('\r\n'))}); - `); - + await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); it('serialize all rows of content with color256 and style separately', async function (): Promise { - this.timeout(20000); const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -277,20 +189,11 @@ describe('SerializeAddon', () => { mkSGR(BG_RESET) + line, // Underlined, Inverse mkSGR(NORMAL) + line // Back to normal ]; - const rows = lines.length; - - await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); - await page.evaluate(` - window.serializeAddon = new SerializeAddon(); - window.term.loadAddon(window.serializeAddon); - window.term.write(${util.inspect(lines.join('\r\n'))}); - `); - + await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); it('serialize all rows of content with color256 and style together', async function (): Promise { - this.timeout(20000); const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -308,20 +211,11 @@ describe('SerializeAddon', () => { mkSGR(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic mkSGR(BG_RESET) + line // Italic ]; - const rows = lines.length; - - await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); - await page.evaluate(` - window.serializeAddon = new SerializeAddon(); - window.term.loadAddon(window.serializeAddon); - window.term.write(${util.inspect(lines.join('\r\n'))}); - `); - + await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); it('serialize all rows of content with colorRGB and style separately', async function (): Promise { - this.timeout(20000); const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -336,20 +230,11 @@ describe('SerializeAddon', () => { mkSGR(BG_RESET) + line, // Underlined, Inverse mkSGR(NORMAL) + line // Back to normal ]; - const rows = lines.length; - - await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); - await page.evaluate(` - window.serializeAddon = new SerializeAddon(); - window.term.loadAddon(window.serializeAddon); - window.term.write(${util.inspect(lines.join('\r\n'))}); - `); - + await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); it('serialize all rows of content with colorRGB and style together', async function (): Promise { - this.timeout(20000); const cols = 10; const line = '+'.repeat(cols); const lines: string[] = [ @@ -367,15 +252,7 @@ describe('SerializeAddon', () => { mkSGR(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic mkSGR(BG_RESET) + line // Italic ]; - const rows = lines.length; - - await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); - await page.evaluate(` - window.serializeAddon = new SerializeAddon(); - window.term.loadAddon(window.serializeAddon); - window.term.write(${util.inspect(lines.join('\r\n'))}); - `); - + await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); }); @@ -456,3 +333,23 @@ const DIM = '2'; const NO_ITALIC = '23'; const NO_DIM = '22'; + +async function writeSync(page: puppeteer.Page, data: string): Promise { + await page.evaluate(` + window.ready = false; + window.term.write('${data}', () => window.ready = true); + `); + await pollFor(page, 'window.ready', true); +} + +async function pollFor(page: puppeteer.Page, fn: string, val: any, preFn?: () => Promise): Promise { + if (preFn) { + await preFn(); + } + const result = await page.evaluate(fn); + if (result !== val) { + return new Promise(r => { + setTimeout(() => r(pollFor(page, fn, val, preFn)), 10); + }); + } +} From 3969eb93826d89359d1b538fb6654b7808d3a64d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 24 Nov 2019 10:20:19 -0800 Subject: [PATCH 28/47] Add failing tab test --- addons/xterm-addon-serialize/src/SerializeAddon.api.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts index a316f34e..fdef10a2 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -255,6 +255,15 @@ describe('SerializeAddon', () => { await writeSync(page, lines.join('\\r\\n')); assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); + + it('serialize tabs correctly', async () => { + const lines = [ + 'a\tb', + 'foo\tbar\tbaz' + ]; + await writeSync(page, lines.join('\\r\\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); }); async function openTerminal(options: ITerminalOptions = {}): Promise { From 903d75e6d3b4500bcfa2839199047d47a1aae8be Mon Sep 17 00:00:00 2001 From: javacs3 Date: Thu, 28 Nov 2019 22:17:53 +0800 Subject: [PATCH 29/47] Refactor 1. add 8 secs timeout for SerializeAddon API test 2. move demo page's addons control panel into a single div 3. remove unnecessary config in demo page 4. remove v8-profiler-node8 dependency 5. remove duplicate css option --- .../src/SerializeAddon.api.ts | 1 + demo/client.ts | 2 -- demo/index.html | 31 +++++++++---------- demo/start.js | 3 +- demo/style.css | 1 - package.json | 1 - test/benchmark/SerializeAddon.benchmark.ts | 25 ++------------- 7 files changed, 20 insertions(+), 44 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts index fdef10a2..a165411a 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -16,6 +16,7 @@ const height = 600; describe('SerializeAddon', () => { before(async function (): Promise { + this.timeout(8 * 1000); browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, args: [`--window-size=${width},${height}`] diff --git a/demo/client.ts b/demo/client.ts index d607cb07..a4f79e77 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -391,10 +391,8 @@ function updateTerminalSize(): void { function serializeButtonHandler(): void { const output = addons.serialize.instance.serialize(); const outputString = JSON.stringify(output); - console.log('serialize output', outputString); document.getElementById('serialize-output').innerText = outputString; - if ((document.getElementById('write-to-terminal') as HTMLInputElement).checked) { term.reset(); term.write(output); diff --git a/demo/index.html b/demo/index.html index c55a818d..4d5149f3 100644 --- a/demo/index.html +++ b/demo/index.html @@ -11,22 +11,6 @@

xterm.js: A terminal for the web

-
-

Actions

-

- - - - - -

-
-

SerializeAddon

- - -
-
-

Options

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

@@ -36,6 +20,21 @@

Addons

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

+

Addons Control

+

SearchAddon

+

+ + + + + +

+

SerializeAddon

+

+ + +

+

Style

diff --git a/demo/start.js b/demo/start.js index 8ffedb42..a14627c1 100644 --- a/demo/start.js +++ b/demo/start.js @@ -48,8 +48,7 @@ const clientConfig = { extensions: [ '.tsx', '.ts', '.js' ], alias: { common: path.resolve('./out/common'), - browser: path.resolve('./out/browser'), - xterm$: path.resolve('./out/public/Terminal.js') + browser: path.resolve('./out/browser') } }, output: { diff --git a/demo/style.css b/demo/style.css index 1ab5dc3c..9c5fd0bd 100644 --- a/demo/style.css +++ b/demo/style.css @@ -40,5 +40,4 @@ pre { word-break: break-all; word-wrap: break-word; white-space: pre-wrap; - word-wrap: break-word; } diff --git a/package.json b/package.json index fe98b468..b740e045 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "tslint-consistent-codestyle": "^1.13.0", "typescript": "3.7", "utf8": "^3.0.0", - "v8-profiler-node8": "^6.1.1", "webpack": "^4.35.3", "webpack-cli": "^3.1.0", "ws": "^7.0.0", diff --git a/test/benchmark/SerializeAddon.benchmark.ts b/test/benchmark/SerializeAddon.benchmark.ts index 9423f400..3b8c4724 100644 --- a/test/benchmark/SerializeAddon.benchmark.ts +++ b/test/benchmark/SerializeAddon.benchmark.ts @@ -3,23 +3,16 @@ * @license MIT */ -import { perfContext, before, after, ThroughputRuntimeCase } from 'xterm-benchmark'; +import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark'; -import * as fs from 'fs' import { spawn } from 'node-pty'; -import * as profiler from 'v8-profiler-node8'; import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder'; import { Terminal } from 'public/Terminal'; import { SerializeAddon } from 'addons/xterm-addon-serialize/src/SerializeAddon'; class TestTerminal extends Terminal { writeSync(data: string): void { - (this)._core.writeBuffer.push(data); - (this)._core._innerWrite(); - } - writeSyncUtf8(data: Uint8Array): void { - (this)._core.writeBufferUtf8.push(data); - (this)._core._innerWriteUtf8(); + (this)._core.writeSync(data); } } @@ -59,24 +52,12 @@ perfContext('Terminal: sh -c "dd if=/dev/random count=40 bs=1k | hexdump | lolca perfContext('serialize', () => { let terminal: TestTerminal; - let serializeAddon = new SerializeAddon(); + const serializeAddon = new SerializeAddon(); before(() => { terminal = new TestTerminal({ cols: 80, rows: 25, scrollback: 5000 }); serializeAddon.activate(terminal); terminal.writeSync(content); - profiler.startProfiling(); }); - after(() => { - const p1 = profiler.stopProfiling(); - p1.export((err, res) => { - if (err) { - console.error(err); - return; - } - console.log(p1.getHeader()); - fs.writeFileSync('serialize-profile.cpuprofile', res); - }) - }) new ThroughputRuntimeCase('', () => { return { payloadSize: serializeAddon.serialize().length }; }, { fork: false }).showAverageThroughput(); From ca12cf58020259da76e78427258e508721d1647f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 7 Dec 2019 10:24:22 -0800 Subject: [PATCH 30/47] Support serializing tabs --- .../xterm-addon-serialize/src/SerializeAddon.api.ts | 10 ++++++++-- addons/xterm-addon-serialize/src/SerializeAddon.ts | 11 +++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts index a165411a..27843714 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -260,10 +260,16 @@ describe('SerializeAddon', () => { it('serialize tabs correctly', async () => { const lines = [ 'a\tb', - 'foo\tbar\tbaz' + 'aa\tc', + 'aaa\td' + ]; + const expected = [ + 'a\x1b[7Cb', + 'aa\x1b[6Cc', + 'aaa\x1b[5Cd' ]; await writeSync(page, lines.join('\\r\\n')); - assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n')); }); }); diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index b0071865..1364b7d2 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -79,6 +79,7 @@ class StringSerializeHandler extends BaseSerializeHandler { private _rowIndex: number = 0; private _allRows: string[] = new Array(); private _currentRow: string = ''; + private _nullCellCount: number = 0; private _sgrSeq: number[] = []; constructor(buffer: IBuffer) { @@ -92,6 +93,7 @@ class StringSerializeHandler extends BaseSerializeHandler { protected _lineEnd(row: number): void { this._allRows[this._rowIndex++] = this._currentRow; this._currentRow = ''; + this._nullCellCount = 0; } protected _cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { @@ -133,6 +135,15 @@ class StringSerializeHandler extends BaseSerializeHandler { } protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + // Count number of null cells encountered after the last non-null cell and move the cursor + // if a non-null cell is found (eg. \t or cursor move) + if (cell.char === '') { + this._nullCellCount++; + } else if (this._nullCellCount > 0) { + this._currentRow += `\x1b[${this._nullCellCount}C`; + this._nullCellCount = 0; + } + const fgChanged = !cell.equalFg(oldCell); const bgChanged = !cell.equalBg(oldCell); const flagsChanged = !cell.equalFlags(oldCell); From 9e1f607bd93d2d2904c9bbad382ccba5100a00a6 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Mon, 16 Dec 2019 21:49:12 +0800 Subject: [PATCH 31/47] fix: serialize-addon, remove unused packages in yarn.lock --- yarn.lock | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2388aed9..171b66e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3431,7 +3431,7 @@ nan@2.10.0: resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== -nan@^2.12.1, nan@^2.14.0: +nan@^2.12.1: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== @@ -3530,22 +3530,6 @@ node-pre-gyp@^0.12.0: semver "^5.3.0" tar "^4" -node-pre-gyp@^0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz#df9ab7b68dd6498137717838e4f92a33fc9daa42" - integrity sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ== - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" - node-pty@0.7.6: version "0.7.6" resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.7.6.tgz#bff6148c9c5836ca7e73c7aaaec067dcbdac2f7b" @@ -5363,14 +5347,6 @@ v8-compile-cache@^2.0.0: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz#a428b28bb26790734c4fc8bc9fa106fccebf6a6c" integrity sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw== -v8-profiler-node8@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/v8-profiler-node8/-/v8-profiler-node8-6.1.1.tgz#2046dd46d80744784f9e94bce1411717435c1f76" - integrity sha512-mKS7TXRRYi70hvbv5c1tk9AbuqNrtbLc+jFLlsZ2TpaC1l5lWryBlDLZKJ1JP6hjSbMEjW1ucjWLSaKsaPnGXg== - dependencies: - nan "^2.14.0" - node-pre-gyp "^0.13.0" - validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" From a43cc23d6717a06796ef7a62f3272ba1c59720da Mon Sep 17 00:00:00 2001 From: javacs3 Date: Tue, 17 Dec 2019 21:21:12 +0800 Subject: [PATCH 32/47] refactor: serialize-addon, remove _cellFgBgChanged(), _cellFlagsChanged(), _lineStart() functions --- .../src/SerializeAddon.ts | 111 +++++++----------- 1 file changed, 42 insertions(+), 69 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 1364b7d2..91ea1d26 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -27,8 +27,6 @@ abstract class BaseSerializeHandler { for (let row = startRow; row < endRow; row++) { const line = this._buffer.getLine(row); - this._lineStart(row); - if (line) { for (let col = 0; col < line.length; col++) { const newCell = line.getCell(col, oldCell === cell1 ? cell2 : cell1); @@ -37,12 +35,6 @@ abstract class BaseSerializeHandler { console.warn(`Can't get cell at row=${row}, col=${col}`); continue; } - if (!newCell.equalFg(oldCell) || !newCell.equalBg(oldCell)) { - this._cellFgBgChanged(newCell, oldCell, row, col); - } - if (!newCell.equalFlags(oldCell)) { - this._cellFlagsChanged(newCell, oldCell, row, col); - } this._nextCell(newCell, oldCell, row, col); @@ -50,7 +42,7 @@ abstract class BaseSerializeHandler { } } - this._lineEnd(row); + this._nextRow(row); } this._serializeEnd(); @@ -60,13 +52,7 @@ abstract class BaseSerializeHandler { protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - - protected _cellFgBgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - - protected _lineStart(row: number): void { } - - protected _lineEnd(row: number): void { } + protected _nextRow(row: number): void { } protected _serializeStart(rows: number): void { } @@ -80,7 +66,6 @@ class StringSerializeHandler extends BaseSerializeHandler { private _allRows: string[] = new Array(); private _currentRow: string = ''; private _nullCellCount: number = 0; - private _sgrSeq: number[] = []; constructor(buffer: IBuffer) { super(buffer); @@ -90,51 +75,52 @@ class StringSerializeHandler extends BaseSerializeHandler { this._allRows = new Array(rows); } - protected _lineEnd(row: number): void { + protected _nextRow(row: number): void { this._allRows[this._rowIndex++] = this._currentRow; this._currentRow = ''; this._nullCellCount = 0; } - protected _cellFlagsChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { - const sgrSeq = this._sgrSeq; - - // skip if it's default color style, we will use \x1b[0m to clear every color style later - if (cell.isAttributeDefault() || cell.equalFlags(oldCell)) { return; } - - if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); } - if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); } - if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); } - if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); } - 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); } - } - - protected _cellFgBgChanged(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { - const sgrSeq = this._sgrSeq; - - // skip if it's default color style, we will use \x1b[0m to clear every color style later - if (cell.isAttributeDefault()) { return; } - - if (!cell.equalFg(oldCell)) { - const color = cell.getFgColor(); - if (cell.isFgRGB()) { sgrSeq.push(38, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); } - else if (cell.isFgPalette256()) { sgrSeq.push(38, 5, color); } - else if (cell.isFgPalette16()) { sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)); } - else { sgrSeq.push(39); } - } - - if (!cell.equalBg(oldCell)) { - const color = cell.getBgColor(); - if (cell.isBgRGB()) { sgrSeq.push(48, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); } - else if (cell.isBgPalette256()) { sgrSeq.push(48, 5, color); } - else if (cell.isBgPalette16()) { sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)); } - else { sgrSeq.push(49); } - } - } - protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + const sgrSeq: number[] = []; + const fgChanged = !cell.equalFg(oldCell); + const bgChanged = !cell.equalBg(oldCell); + const flagsChanged = !cell.equalFlags(oldCell); + + if (fgChanged || bgChanged || flagsChanged) { + if (cell.isAttributeDefault()) { + this._currentRow += '\x1b[0m'; + } else { + if (fgChanged) { + const color = cell.getFgColor(); + if (cell.isFgRGB()) { sgrSeq.push(38, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); } + else if (cell.isFgPalette256()) { sgrSeq.push(38, 5, color); } + else if (cell.isFgPalette16()) { sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)); } + else { sgrSeq.push(39); } + } + if (bgChanged) { + const color = cell.getBgColor(); + if (cell.isBgRGB()) { sgrSeq.push(48, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); } + else if (cell.isBgPalette256()) { sgrSeq.push(48, 5, color); } + else if (cell.isBgPalette16()) { sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)); } + else { sgrSeq.push(49); } + } + if (flagsChanged) { + if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); } + if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); } + if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); } + if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); } + 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 (sgrSeq.length) { + this._currentRow += `\x1b[${sgrSeq.join(';')}m`; + } + // Count number of null cells encountered after the last non-null cell and move the cursor // if a non-null cell is found (eg. \t or cursor move) if (cell.char === '') { @@ -144,19 +130,6 @@ class StringSerializeHandler extends BaseSerializeHandler { this._nullCellCount = 0; } - const fgChanged = !cell.equalFg(oldCell); - const bgChanged = !cell.equalBg(oldCell); - const flagsChanged = !cell.equalFlags(oldCell); - - if (cell.isAttributeDefault() && (fgChanged || bgChanged || flagsChanged)) { - this._currentRow += '\x1b[0m'; - } - - if (this._sgrSeq.length) { - this._currentRow += `\x1b[${this._sgrSeq.join(';')}m`; - this._sgrSeq = []; - } - this._currentRow += cell.char; } From b80b5a94f99e1cdd2cbe020b14184a8ffb21fcef Mon Sep 17 00:00:00 2001 From: javacs3 Date: Tue, 17 Dec 2019 22:42:09 +0800 Subject: [PATCH 33/47] refactor: serialize-addon, rename _nextRow() to _rowEnd() --- addons/xterm-addon-serialize/src/SerializeAddon.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 91ea1d26..468b9655 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -42,7 +42,7 @@ abstract class BaseSerializeHandler { } } - this._nextRow(row); + this._rowEnd(row); } this._serializeEnd(); @@ -52,7 +52,7 @@ abstract class BaseSerializeHandler { protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _nextRow(row: number): void { } + protected _rowEnd(row: number): void { } protected _serializeStart(rows: number): void { } @@ -75,7 +75,7 @@ class StringSerializeHandler extends BaseSerializeHandler { this._allRows = new Array(rows); } - protected _nextRow(row: number): void { + protected _rowEnd(row: number): void { this._allRows[this._rowIndex++] = this._currentRow; this._currentRow = ''; this._nullCellCount = 0; From 84f5f20edbe5d3cc45035dc6cb4dc03a03abd111 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Wed, 18 Dec 2019 16:09:57 +0800 Subject: [PATCH 34/47] refactor: serialize-addon, remove equalFg(), equalBg(), equalFlags() api from xterm.d.ts --- .../src/SerializeAddon.ts | 26 ++++++++++++++++--- src/public/Terminal.ts | 18 +------------ typings/xterm.d.ts | 6 +---- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 468b9655..160f6459 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -61,6 +61,26 @@ abstract class BaseSerializeHandler { protected _serializeFinished(): string { return ''; } } +function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean { + return cell1.getFgColorMode() === cell2.getFgColorMode() + && cell1.getFgColor() === cell2.getFgColor(); +} + +function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean { + return cell1.getBgColorMode() === cell2.getBgColorMode() + && cell1.getBgColor() === cell2.getBgColor(); +} + +function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean { + return cell1.isInverse() === cell2.isInverse() + && cell1.isBold() === cell2.isBold() + && cell1.isUnderline() === cell2.isUnderline() + && cell1.isBlink() === cell2.isBlink() + && cell1.isInvisible() === cell2.isInvisible() + && cell1.isItalic() === cell2.isItalic() + && cell1.isDim() === cell2.isDim(); +} + class StringSerializeHandler extends BaseSerializeHandler { private _rowIndex: number = 0; private _allRows: string[] = new Array(); @@ -83,9 +103,9 @@ class StringSerializeHandler extends BaseSerializeHandler { protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { const sgrSeq: number[] = []; - const fgChanged = !cell.equalFg(oldCell); - const bgChanged = !cell.equalBg(oldCell); - const flagsChanged = !cell.equalFlags(oldCell); + const fgChanged = !equalFg(cell, oldCell); + const bgChanged = !equalBg(cell, oldCell); + const flagsChanged = !equalFlags(cell, oldCell); if (fgChanged || bgChanged || flagsChanged) { if (cell.isAttributeDefault()) { diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 22776d0a..5854cef6 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -7,7 +7,7 @@ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILink import { ITerminal } from '../Types'; import { IBufferLine } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; -import { Attributes, FgFlags, BgFlags } from 'common/buffer/Constants'; +import { Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../browser/LocalizableStrings'; @@ -227,10 +227,6 @@ class BufferLineApiView implements IBufferLineApi { } } -const FG_FLAG_MASK = FgFlags.BOLD | FgFlags.BLINK | FgFlags.INVERSE | FgFlags.INVISIBLE | FgFlags.UNDERLINE; -const BG_FLAG_MASK = BgFlags.DIM | BgFlags.ITALIC; -const COLOR_MASK = Attributes.CM_MASK | Attributes.RGB_MASK; - class BufferCellApiView implements IBufferCellApi { constructor(public cell: CellData) {} @@ -266,18 +262,6 @@ class BufferCellApiView implements IBufferCellApi { public getFgColor(): number { return this.cell.getFgColor(); } public getBgColor(): number { return this.cell.getBgColor(); } - - - public equalFlags(other: BufferCellApiView): boolean { - return (this.cell.fg & FG_FLAG_MASK) === (other.cell.fg & FG_FLAG_MASK) - && (this.cell.bg & BG_FLAG_MASK) === (other.cell.bg & BG_FLAG_MASK); - } - public equalFg(other: BufferCellApiView): boolean { - return (this.cell.fg & COLOR_MASK) === (other.cell.fg & COLOR_MASK); - } - public equalBg(other: BufferCellApiView): boolean { - return (this.cell.bg & COLOR_MASK) === (other.cell.bg & COLOR_MASK); - } } class ParserApi implements IParser { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 6bf2aebf..243266d0 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -439,7 +439,7 @@ declare module 'xterm' { * Currently this is only used for a certain type of mouse reports that * happen to be not UTF-8 compatible. * The event value is a JS string, pass it to the underlying pty as - * binary data, e.g. `pty.write(Buffer.from(data, 'binary'))`. + * binary data, e.g. `pty.write(Buffer.from(data, 'binary'))`. * @returns an `IDisposable` to stop listening. */ onBinary: IEvent; @@ -1049,10 +1049,6 @@ declare module 'xterm' { isAttributeDefault(): boolean; isFgDefault(): boolean; isBgDefault(): boolean; - - equalFg(cell: IBufferCell): boolean; - equalBg(cell: IBufferCell): boolean; - equalFlags(cell: IBufferCell): boolean; } /** From 7a8c3a42cd51c4961bed3ae8e8e0b11e602952ce Mon Sep 17 00:00:00 2001 From: javacs3 Date: Wed, 18 Dec 2019 16:38:02 +0800 Subject: [PATCH 35/47] refactor: serialize-addon, rename some APIs in SerializeAddon --- .../src/SerializeAddon.ts | 44 +++++++------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 160f6459..b9c5ed4f 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -1,14 +1,13 @@ /** * Copyright (c) 2019 The xterm.js authors. All rights reserved. * @license MIT + * + * (EXPERIMENTAL) This Addon is still under development */ import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm'; -function crop(value: number | undefined, low: number, high: number, initial: number): number { - if (value === undefined) { - return initial; - } +function constrain(value: number, low: number, high: number): number { return Math.max(low, Math.min(value, high)); } @@ -22,43 +21,34 @@ abstract class BaseSerializeHandler { const cell2 = this._buffer.getNullCell(); let oldCell = cell1; - this._serializeStart(endRow - startRow); + this._beforeSerialize(endRow - startRow); for (let row = startRow; row < endRow; row++) { const line = this._buffer.getLine(row); - if (line) { for (let col = 0; col < line.length; col++) { - const newCell = line.getCell(col, oldCell === cell1 ? cell2 : cell1); - - if (!newCell) { + const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1); + if (!c) { console.warn(`Can't get cell at row=${row}, col=${col}`); continue; } - - this._nextCell(newCell, oldCell, row, col); - - oldCell = newCell; + this._nextCell(c, oldCell, row, col); + oldCell = c; } } - this._rowEnd(row); } - this._serializeEnd(); + this._afterSerialize(); - return this._serializeFinished(); + return this._serializeString(); } protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } - protected _rowEnd(row: number): void { } - - protected _serializeStart(rows: number): void { } - - protected _serializeEnd(): void { } - - protected _serializeFinished(): string { return ''; } + protected _beforeSerialize(rows: number): void { } + protected _afterSerialize(): void { } + protected _serializeString(): string { return ''; } } function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean { @@ -91,7 +81,7 @@ class StringSerializeHandler extends BaseSerializeHandler { super(buffer); } - protected _serializeStart(rows: number): void { + protected _beforeSerialize(rows: number): void { this._allRows = new Array(rows); } @@ -153,15 +143,13 @@ class StringSerializeHandler extends BaseSerializeHandler { this._currentRow += cell.char; } - protected _serializeFinished(): string { + protected _serializeString(): string { let rowEnd = this._allRows.length; - for (; rowEnd > 0; rowEnd--) { if (this._allRows[rowEnd - 1]) { break; } } - return this._allRows.slice(0, rowEnd).join('\r\n'); } } @@ -186,7 +174,7 @@ export class SerializeAddon implements ITerminalAddon { const maxRows = this._terminal.buffer.length; const handler = new StringSerializeHandler(this._terminal.buffer); - rows = crop(rows, 0, maxRows, maxRows); + rows = (rows === undefined) ? maxRows : constrain(rows, 0, maxRows); return handler.serialize(maxRows - rows, maxRows); } From 34bf2f9a0f6049fb0eef27e54f22648042609293 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Thu, 19 Dec 2019 22:17:32 +0800 Subject: [PATCH 36/47] refactor: serialize-addon, remove timeout in api test --- addons/xterm-addon-serialize/src/SerializeAddon.api.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts index 27843714..c2a3c46e 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -16,7 +16,6 @@ const height = 600; describe('SerializeAddon', () => { before(async function (): Promise { - this.timeout(8 * 1000); browser = await puppeteer.launch({ headless: process.argv.indexOf('--headless') !== -1, args: [`--window-size=${width},${height}`] From a4efefbf5eb45d48551ee5dab600a40a70e0e274 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Tue, 31 Dec 2019 22:06:19 +0800 Subject: [PATCH 37/47] refactor: serialize-addon, move SerializeAddon.benchmark.ts into addons/xterm-addon-serialize and add README.md --- addons/xterm-addon-serialize/.gitignore | 1 + addons/xterm-addon-serialize/README.md | 42 +++++++++++++++++++ .../benchmark/SerializeAddon.benchmark.ts | 2 +- .../benchmark/benchmark.json | 19 +++++++++ .../benchmark/tsconfig.json | 25 +++++++++++ addons/xterm-addon-serialize/package.json | 5 ++- package.json | 6 +-- tsconfig.all.json | 3 +- 8 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 addons/xterm-addon-serialize/README.md rename {test => addons/xterm-addon-serialize}/benchmark/SerializeAddon.benchmark.ts (96%) create mode 100644 addons/xterm-addon-serialize/benchmark/benchmark.json create mode 100644 addons/xterm-addon-serialize/benchmark/tsconfig.json diff --git a/addons/xterm-addon-serialize/.gitignore b/addons/xterm-addon-serialize/.gitignore index 3063f07d..03c051b3 100644 --- a/addons/xterm-addon-serialize/.gitignore +++ b/addons/xterm-addon-serialize/.gitignore @@ -1,2 +1,3 @@ lib node_modules +out-benchmark diff --git a/addons/xterm-addon-serialize/README.md b/addons/xterm-addon-serialize/README.md new file mode 100644 index 00000000..12e0ac87 --- /dev/null +++ b/addons/xterm-addon-serialize/README.md @@ -0,0 +1,42 @@ +## xterm-addon-serialize + +An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables xterm.js to serialize a terminal framebuffer into string or html. This addon requires xterm.js v4+. + +⚠️ This is an experimental addon that is still under construction ⚠️ + +### Install + +```bash +npm install --save xterm-addon-serialize +``` + +### Usage + +```ts +import { Terminal } from "xterm"; +import { SerializeAddon } from "xterm-addon-serialize"; + +const terminal = new Terminal(); +const serializeAddon = new SerializeAddon(); +terminal.loadAddon(serializeAddon); + +terminal.write("something...", () => { + console.log(serializeAddon.serialize()); +}); +``` + +See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts) for more advanced usage. + +### Benchmark + +⚠️ Ensure you have `lolcat`, `hexdump` programs installed in your computer + +```shell +$ git clone https://github.com/xtermjs/xterm.js.git +$ cd xterm.js +$ yarn +$ cd addons/xterm-addon-serialize +$ yarn benchmark && yarn benchmark-baseline +$ # change some code in `xterm-addon-serialize` +$ yarn benchmark-eval +``` diff --git a/test/benchmark/SerializeAddon.benchmark.ts b/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts similarity index 96% rename from test/benchmark/SerializeAddon.benchmark.ts rename to addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts index 3b8c4724..025e7194 100644 --- a/test/benchmark/SerializeAddon.benchmark.ts +++ b/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts @@ -8,7 +8,7 @@ import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark'; import { spawn } from 'node-pty'; import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder'; import { Terminal } from 'public/Terminal'; -import { SerializeAddon } from 'addons/xterm-addon-serialize/src/SerializeAddon'; +import { SerializeAddon } from 'SerializeAddon'; class TestTerminal extends Terminal { writeSync(data: string): void { diff --git a/addons/xterm-addon-serialize/benchmark/benchmark.json b/addons/xterm-addon-serialize/benchmark/benchmark.json new file mode 100644 index 00000000..f8b99b55 --- /dev/null +++ b/addons/xterm-addon-serialize/benchmark/benchmark.json @@ -0,0 +1,19 @@ +{ + "APP_PATH": ".benchmark", + "evalConfig": { + "tolerance": { + "*": [0.75, 1.5], + "*.dev": [0.01, 1.5], + "*.cv": [0.01, 1.5], + "EscapeSequenceParser.benchmark.js.*.averageThroughput.mean": [0.9, 5] + }, + "skip": [ + "*.median", + "*.runs", + "*.dev", + "*.cv", + "EscapeSequenceParser.benchmark.js.*.averageRuntime", + "Terminal.benchmark.js.*.averageRuntime" + ] + } +} diff --git a/addons/xterm-addon-serialize/benchmark/tsconfig.json b/addons/xterm-addon-serialize/benchmark/tsconfig.json new file mode 100644 index 00000000..4e62ed59 --- /dev/null +++ b/addons/xterm-addon-serialize/benchmark/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "lib": ["dom", "es6"], + "outDir": "../out-benchmark", + "types": ["../../../node_modules/@types/node"], + "moduleResolution": "node", + "strict": false, + "target": "es2015", + "module": "commonjs", + "baseUrl": ".", + "paths": { + "common/*": ["../../../src/common/*"], + "browser/*": ["../../../src/browser/*"], + "public/*": ["../../../src/public/*"], + "Terminal": ["../../../src/Terminal"], + "SerializeAddon": ["../src/SerializeAddon"] + } + }, + "include": ["../**/*", "../../../typings/xterm.d.ts", "../../../out/**/*"], + "exclude": ["../../../**/*test.ts", "../../**/*api.ts"], + "references": [ + { "path": "../../../src/common" }, + { "path": "../../../src/browser" } + ] +} diff --git a/addons/xterm-addon-serialize/package.json b/addons/xterm-addon-serialize/package.json index c8a66c4b..787462f9 100644 --- a/addons/xterm-addon-serialize/package.json +++ b/addons/xterm-addon-serialize/package.json @@ -12,7 +12,10 @@ "build": "../../node_modules/.bin/tsc -p src", "prepackage": "npm run build", "package": "../../node_modules/.bin/webpack", - "prepublishOnly": "npm run package" + "prepublishOnly": "npm run package", + "benchmark": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json", + "benchmark-baseline": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/addons/xterm-addon-serialize/benchmark/*benchmark.js", + "benchmark-eval": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/addons/xterm-addon-serialize/benchmark/*benchmark.js" }, "peerDependencies": { "xterm": "^3.14.0" diff --git a/package.json b/package.json index 1acf11f5..e04a1ff8 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,9 @@ "presetup": "node ./bin/install-addons.js", "prepublishOnly": "npm run package", "watch": "tsc -b -w ./tsconfig.all.json --preserveWatchOutput", - "benchmark": "NODE_PATH=./out:./out-test/benchmark xterm-benchmark -r 5 -c test/benchmark/benchmark.json", - "benchmark-baseline": "NODE_PATH=./out:./out-test/benchmark xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/test/benchmark/SerializeAddon.benchmark.js", - "benchmark-eval": "NODE_PATH=./out:./out-test/benchmark xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/test/benchmark/SerializeAddon.benchmark.js", + "benchmark": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json", + "benchmark-baseline": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/test/benchmark/*benchmark.js", + "benchmark-eval": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/test/benchmark/*benchmark.js", "clean": "rm -rf lib out addons/*/lib addons/*/out" }, "devDependencies": { diff --git a/tsconfig.all.json b/tsconfig.all.json index 5a7ae05b..60cb9164 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -10,6 +10,7 @@ { "path": "./addons/xterm-addon-search/src" }, { "path": "./addons/xterm-addon-web-links/src" }, { "path": "./addons/xterm-addon-webgl/src" }, - { "path": "./addons/xterm-addon-serialize/src" } + { "path": "./addons/xterm-addon-serialize/src" }, + { "path": "./addons/xterm-addon-serialize/benchmark" } ] } From 56f4c3d6aefc29370ec0c71593744eb48046dfda Mon Sep 17 00:00:00 2001 From: javacs3 Date: Thu, 9 Jan 2020 21:15:30 +0800 Subject: [PATCH 38/47] remove BufferCellApiView --- addons/xterm-addon-search/src/SearchAddon.ts | 4 +- .../src/SerializeAddon.api.ts | 16 +++---- .../src/SerializeAddon.ts | 16 ++++--- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 2 +- src/common/Types.d.ts | 1 + src/common/buffer/AttributeData.ts | 1 + src/common/buffer/Constants.ts | 14 +----- src/public/Terminal.ts | 48 ++----------------- typings/xterm.d.ts | 18 ------- 9 files changed, 30 insertions(+), 90 deletions(-) diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index db2968e6..183687ac 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -320,13 +320,13 @@ export class SearchAddon implements ITerminalAddon { break; } // Adjust the searchIndex to normalize emoji into single chars - const char = cell.char; + const char = cell.getChars(); if (char.length > 1) { resultIndex -= char.length - 1; } // Adjust the searchIndex for empty characters following wide unicode // chars (eg. CJK) - const charWidth = cell.width; + const charWidth = cell.getWidth(); if (charWidth === 0) { resultIndex++; } diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts index c2a3c46e..a34aa0bd 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -126,7 +126,7 @@ describe('SerializeAddon', () => { const rows = 32; const cols = 10; const lines = newArray( - (index: number) => digitsString(cols, index, `\x1b[38;5;${index}m`), + (index: number) => digitsString(cols, index, `\x1b[38;5;${16 + index}m`), rows ); await writeSync(page, lines.join('\\r\\n')); @@ -256,7 +256,7 @@ describe('SerializeAddon', () => { assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); }); - it('serialize tabs correctly', async () => { + it('serialize tabs correctly', async () => { const lines = [ 'a\tb', 'aa\tc', @@ -311,9 +311,9 @@ const NORMAL = '0'; const FG_P16_RED = '31'; const FG_P16_GREEN = '32'; const FG_P16_YELLOW = '33'; -const FG_P256_RED = '38;5;1'; -const FG_P256_GREEN = '38;5;2'; -const FG_P256_YELLOW = '38;5;3'; +const FG_P256_RED = '38;5;196'; +const FG_P256_GREEN = '38;5;46'; +const FG_P256_YELLOW = '38;5;226'; const FG_RGB_RED = '38;2;255;0;0'; const FG_RGB_GREEN = '38;2;0;255;0'; const FG_RGB_YELLOW = '38;2;255;255;0'; @@ -323,9 +323,9 @@ const FG_RESET = '39'; const BG_P16_RED = '41'; const BG_P16_GREEN = '42'; const BG_P16_YELLOW = '43'; -const BG_P256_RED = '48;5;1'; -const BG_P256_GREEN = '48;5;2'; -const BG_P256_YELLOW = '48;5;3'; +const BG_P256_RED = '48;5;196'; +const BG_P256_GREEN = '48;5;46'; +const BG_P256_YELLOW = '48;5;226'; const BG_RGB_RED = '48;2;255;0;0'; const BG_RGB_GREEN = '48;2;0;255;0'; const BG_RGB_YELLOW = '48;2;255;255;0'; diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index b9c5ed4f..1d011726 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -104,15 +104,19 @@ class StringSerializeHandler extends BaseSerializeHandler { if (fgChanged) { const color = cell.getFgColor(); if (cell.isFgRGB()) { sgrSeq.push(38, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); } - else if (cell.isFgPalette256()) { sgrSeq.push(38, 5, color); } - else if (cell.isFgPalette16()) { sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)); } + else if (cell.isFgPalette()) { + if (color >= 16) { sgrSeq.push(38, 5, color); } + else { sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)); } + } else { sgrSeq.push(39); } } if (bgChanged) { const color = cell.getBgColor(); if (cell.isBgRGB()) { sgrSeq.push(48, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); } - else if (cell.isBgPalette256()) { sgrSeq.push(48, 5, color); } - else if (cell.isBgPalette16()) { sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)); } + else if (cell.isBgPalette()) { + if (color >= 16) { sgrSeq.push(48, 5, color); } + else { sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)); } + } else { sgrSeq.push(49); } } if (flagsChanged) { @@ -133,14 +137,14 @@ class StringSerializeHandler extends BaseSerializeHandler { // Count number of null cells encountered after the last non-null cell and move the cursor // if a non-null cell is found (eg. \t or cursor move) - if (cell.char === '') { + if (cell.getChars() === '') { this._nullCellCount++; } else if (this._nullCellCount > 0) { this._currentRow += `\x1b[${this._nullCellCount}C`; this._nullCellCount = 0; } - this._currentRow += cell.char; + this._currentRow += cell.getChars(); } protected _serializeString(): string { diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 7b35949d..bc46a085 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -258,7 +258,7 @@ export class GlyphRenderer { if (!line) { line = terminal.buffer.getLine(row); } - const chars = line!.getCell(x)!.char; + const chars = line!.getCell(x)!.getChars(); this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET], chars); } else { this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET]); diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 2dcc704b..33cc67d7 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -93,6 +93,7 @@ export interface IAttributeData { isBgPalette(): boolean; isFgDefault(): boolean; isBgDefault(): boolean; + isAttributeDefault(): boolean; // colors getFgColor(): number; diff --git a/src/common/buffer/AttributeData.ts b/src/common/buffer/AttributeData.ts index 0e7e2705..0b2679ee 100644 --- a/src/common/buffer/AttributeData.ts +++ b/src/common/buffer/AttributeData.ts @@ -47,6 +47,7 @@ export class AttributeData implements IAttributeData { public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; } public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; } public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; } + public isAttributeDefault(): boolean { return this.fg === 0 && this.bg === 0; } // colors public getFgColor(): number { diff --git a/src/common/buffer/Constants.ts b/src/common/buffer/Constants.ts index 81ac773a..276a5c54 100644 --- a/src/common/buffer/Constants.ts +++ b/src/common/buffer/Constants.ts @@ -116,12 +116,7 @@ export const enum FgFlags { BOLD = 0x8000000, UNDERLINE = 0x10000000, BLINK = 0x20000000, - INVISIBLE = 0x40000000, - - /** - * bit 27..31 (32th bit unused) - */ - FM_MASK = 0x7C000000 + INVISIBLE = 0x40000000 } export const enum BgFlags { @@ -129,10 +124,5 @@ export const enum BgFlags { * bit 27..32 (upper 4 unused) */ ITALIC = 0x4000000, - DIM = 0x8000000, - - /** - * bit 27..32 (upper 4 unused) - */ - FM_MASK = 0xFC000000 + DIM = 0x8000000 } diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 5854cef6..fd61aaf2 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -5,9 +5,8 @@ import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier } from 'xterm'; import { ITerminal } from '../Types'; -import { IBufferLine } from 'common/Types'; +import { IBufferLine, ICellData } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; -import { Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../browser/LocalizableStrings'; @@ -203,7 +202,7 @@ class BufferApiView implements IBufferApi { } return new BufferLineApiView(line); } - public getNullCell(): IBufferCellApi { return new BufferCellApiView(new CellData()); } + public getNullCell(): IBufferCellApi { return new CellData(); } } class BufferLineApiView implements IBufferLineApi { @@ -211,59 +210,22 @@ class BufferLineApiView implements IBufferLineApi { public get isWrapped(): boolean { return this._line.isWrapped; } public get length(): number { return this._line.length; } - public getCell(x: number, cell?: BufferCellApiView): IBufferCellApi | undefined { + public getCell(x: number, cell?: IBufferCellApi): IBufferCellApi | undefined { if (x < 0 || x >= this._line.length) { return undefined; } if (cell) { - this._line.loadCell(x, cell.cell); + this._line.loadCell(x, cell); return cell; } - return new BufferCellApiView(this._line.loadCell(x, new CellData())); + return this._line.loadCell(x, new CellData()); } public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string { return this._line.translateToString(trimRight, startColumn, endColumn); } } -class BufferCellApiView implements IBufferCellApi { - constructor(public cell: CellData) {} - - public get char(): string { return this.cell.getChars(); } - public get width(): number { return this.cell.getWidth(); } - - public getWidth(): number { return this.cell.getWidth(); } - public getChars(): string { return this.cell.getChars(); } - public getCode(): number { return this.cell.getCode(); } - - public isInverse(): number { return this.cell.isInverse(); } - public isBold(): number { return this.cell.isBold(); } - public isUnderline(): number { return this.cell.isUnderline(); } - public isBlink(): number { return this.cell.isBlink(); } - public isInvisible(): number { return this.cell.isInvisible(); } - public isItalic(): number { return this.cell.isItalic(); } - public isDim(): number { return this.cell.isDim(); } - - public getFgColorMode(): number { return this.cell.getFgColorMode(); } - public getBgColorMode(): number { return this.cell.getBgColorMode(); } - public isFgRGB(): boolean { return this.cell.isFgRGB(); } - public isBgRGB(): boolean { return this.cell.isBgRGB(); } - public isFgPalette(): boolean { return this.cell.isFgPalette(); } - public isBgPalette(): boolean { return this.cell.isBgPalette(); } - public isFgPalette16(): boolean { return this.cell.getFgColorMode() === Attributes.CM_P16; } - public isBgPalette16(): boolean { return this.cell.getBgColorMode() === Attributes.CM_P16; } - public isFgPalette256(): boolean { return this.cell.getFgColorMode() === Attributes.CM_P256; } - public isBgPalette256(): boolean { return this.cell.getBgColorMode() === Attributes.CM_P256; } - - public isAttributeDefault(): boolean { return this.cell.fg === 0 && this.cell.bg === 0; } - public isFgDefault(): boolean { return this.cell.isFgDefault(); } - public isBgDefault(): boolean { return this.cell.isBgDefault(); } - - public getFgColor(): number { return this.cell.getFgColor(); } - public getBgColor(): number { return this.cell.getBgColor(); } -} - class ParserApi implements IParser { constructor(private _core: ITerminal) {} diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 243266d0..c687b783 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1007,20 +1007,6 @@ declare module 'xterm' { * Represents a single cell in the terminal's buffer. */ interface IBufferCell { - /** - * The character within the cell. - */ - readonly char: string; - - /** - * The width of the character. Some examples: - * - * - This is `1` for most cells. - * - This is `2` for wide character like CJK glyphs. - * - This is `0` for cells immediately following cells with a width of `2`. - */ - readonly width: number; - getWidth(): number; getChars(): string; getCode(): number; @@ -1042,10 +1028,6 @@ declare module 'xterm' { isBgRGB(): boolean; isFgPalette(): boolean; isBgPalette(): boolean; - isFgPalette16(): boolean; - isBgPalette16(): boolean; - isFgPalette256(): boolean; - isBgPalette256(): boolean; isAttributeDefault(): boolean; isFgDefault(): boolean; isBgDefault(): boolean; From c512e48c5261441698a3da011d85154e813a7818 Mon Sep 17 00:00:00 2001 From: javacs3 Date: Tue, 4 Feb 2020 14:46:03 +0800 Subject: [PATCH 39/47] fix: api test fail due to IBufferCell api changes --- test/api/CharWidth.api.ts | 4 ++-- test/api/Terminal.api.ts | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/api/CharWidth.api.ts b/test/api/CharWidth.api.ts index 39f0bfe7..a2352f40 100644 --- a/test/api/CharWidth.api.ts +++ b/test/api/CharWidth.api.ts @@ -98,8 +98,8 @@ async function sumWidths(start: number, end: number, sentinel: string): Promise< if (!cell) { break; } - window.result += cell.width; - if (cell.char === '${sentinel}') { + window.result += cell.getWidth(); + if (cell.getChars() === '${sentinel}') { return; } } diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index 40deecf2..cdfab61f 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -498,15 +498,15 @@ describe('API Integration Tests', function(): void { await openTerminal({ cols: 5 }); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(-1)`), undefined); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(5)`), undefined); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).char`), ''); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).width`), 1); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).getChars()`), ''); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).getWidth()`), 1); await writeSync(page, 'a文'); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).char`), 'a'); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).width`), 1); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(1).char`), '文'); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(1).width`), 2); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(2).char`), ''); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(2).width`), 0); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).getChars()`), 'a'); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).getWidth()`), 1); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(1).getChars()`), '文'); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(1).getWidth()`), 2); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(2).getChars()`), ''); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(2).getWidth()`), 0); }); }); }); From 1b4a3475e86193bd4b4172099161c4443fb30708 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 4 Feb 2020 07:07:34 -0800 Subject: [PATCH 40/47] Fix demo after bad merge --- demo/client.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/client.ts b/demo/client.ts index b7951423..95dfcd1e 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -152,6 +152,7 @@ function createTerminal(): void { addons.serialize.instance = new SerializeAddon(); addons.fit.instance = new FitAddon(); addons.unicode11.instance = new Unicode11Addon(); + addons['web-links'].instance = new WebLinksAddon(); typedTerm.loadAddon(addons.fit.instance); typedTerm.loadAddon(addons.search.instance); typedTerm.loadAddon(addons.serialize.instance); From b50b6ccb8b6208694495456bd0bc3f9d870ec071 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 4 Feb 2020 07:41:04 -0800 Subject: [PATCH 41/47] Fix fg used by selection in webgl again See #2650 --- addons/xterm-addon-webgl/src/GlyphRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 5c5d5970..6c67febb 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -284,7 +284,7 @@ export class GlyphRenderer { line = terminal.buffer.getLine(row); } const chars = line!.getCell(x)!.getChars(); - this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET], chars); + this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg, chars); } else { this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg); } From 51a564eb9c22e4d765f98f153c9352e81ce6bd75 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 4 Feb 2020 07:45:12 -0800 Subject: [PATCH 42/47] Remove links to serialize from test/benchmark --- test/benchmark/tsconfig.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/benchmark/tsconfig.json b/test/benchmark/tsconfig.json index 51abeba5..cac99d90 100644 --- a/test/benchmark/tsconfig.json +++ b/test/benchmark/tsconfig.json @@ -16,8 +16,6 @@ "paths": { "common/*": [ "../../src/common/*" ], "browser/*": [ "../../src/browser/*" ], - "addons/xterm-addon-serialize/src/*": ["../../addons/xterm-addon-serialize/src/*"], - "public/*": ["../../src/public/*"], "Terminal": ["../../src/Terminal"] }, }, From 89370a1f74a7962ec3d65ce464b6e94e933ed86c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 4 Feb 2020 07:54:11 -0800 Subject: [PATCH 43/47] Explain why reusing IBufferCell is useful --- typings/xterm.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 2af7e87e..58f818ee 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -982,7 +982,9 @@ declare module 'xterm' { * behavior. * * @param x The character index to get. - * @param cell Optional cell object to load data into. + * @param cell Optional cell object to load data into for performance + * reasons. This is mainly useful when every cell in the buffer is being + * looped over to avoid creating new objects for every cell. */ getCell(x: number, cell?: IBufferCell): IBufferCell | undefined; From e5246f3993825559f8a8e987c815e56c7d3d3451 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 4 Feb 2020 07:55:26 -0800 Subject: [PATCH 44/47] jsdoc IBufferLine.length --- typings/xterm.d.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 58f818ee..067ffc31 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -972,6 +972,11 @@ declare module 'xterm' { * Whether the line is wrapped from the previous line. */ readonly isWrapped: boolean; + + /** + * The length of the line, all call to getCell beyond the length will result + * in `undefined`. + */ readonly length: number; /** From fac025367b161ce072b29d62ac9883b6317d4cad Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 4 Feb 2020 08:15:21 -0800 Subject: [PATCH 45/47] Document IBufferCell members --- typings/xterm.d.ts | 82 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 067ffc31..5f97cf10 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1008,30 +1008,102 @@ declare module 'xterm' { * Represents a single cell in the terminal's buffer. */ interface IBufferCell { + /** + * The width of the character. Some examples: + * + * - `1` for most cells. + * - `2` for wide character like CJK glyphs. + * - `0` for cells immediately following cells with a width of `2`. + */ getWidth(): number; + + /** + * The character(s) within the cell. Examples of what this can contain: + * + * - A normal width character + * - A wide character (eg. CJK) + * - An emoji + */ getChars(): string; + + /** + * Gets the UTF32 codepoint of single characters, if content is a combined + * string it returns the codepoint of the last character in the string. + */ getCode(): number; + /** + * Gets the number representation of the foreground color mode, this can be + * used to perform quick comparisons of 2 cells to see if they're the same. + * Use `isFgRGB`, `isFgPalette` and `isFgDefault` to check what color mode + * a cell is. + */ getFgColorMode(): number; + + /** + * Gets the number representation of the background color mode, this can be + * used to perform quick comparisons of 2 cells to see if they're the same. + * Use `isBgRGB`, `isBgPalette` and `isBgDefault` to check what color mode + * a cell is. + */ getBgColorMode(): number; + + /** + * Gets a cell's foreground color number, this differs depending on what the + * color mode of the cell is: + * + * - Default: This should be 0, representing the default foreground color + * (CSI 39 m). + * - Palette: This is a number from 0 to 255 of ANSI colors (CSI 3(0-7) m, + * CSI 9(0-7) m, CSI 38 ; 5 ; 0-255 m). + * - RGB: A hex value representing a 'true color': 0xRRGGBB. + * (CSI 3 8 ; 2 ; Pi ; Pr ; Pg ; Pb) + */ getFgColor(): number; + + /** + * Gets a cell's background color number, this differs depending on what the + * color mode of the cell is: + * + * - Default: This should be 0, representing the default background color + * (CSI 49 m). + * - Palette: This is a number from 0 to 255 of ANSI colors + * (CSI 4(0-7) m, CSI 10(0-7) m, CSI 48 ; 5 ; 0-255 m). + * - RGB: A hex value representing a 'true color': 0xRRGGBB + * (CSI 4 8 ; 2 ; Pi ; Pr ; Pg ; Pb) + */ getBgColor(): number; - isInverse(): number; + /** Whether the cell has the bold attribute (CSI 1 m). */ isBold(): number; - isUnderline(): number; - isBlink(): number; - isInvisible(): number; + /** Whether the cell has the inverse attribute (CSI 3 m). */ isItalic(): number; + /** Whether the cell has the inverse 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). */ + isBlink(): number; + /** Whether the cell has the inverse attribute (CSI 7 m). */ + isInverse(): number; + /** Whether the cell has the inverse attribute (CSI 8 m). */ + isInvisible(): number; + /** Whether the cell is using the RGB foreground color mode. */ isFgRGB(): boolean; + /** Whether the cell is using the RGB background color mode. */ isBgRGB(): boolean; + /** Whether the cell is using the palette foreground color mode. */ isFgPalette(): boolean; + /** Whether the cell is using the palette background color mode. */ isBgPalette(): boolean; - isAttributeDefault(): boolean; + /** Whether the cell is using the default foreground color mode. */ isFgDefault(): boolean; + /** Whether the cell is using the default background color mode. */ isBgDefault(): boolean; + + /** Whether the cell has the default attribute (no color or style). */ + isAttributeDefault(): boolean; } /** From 0b6a5b763095f516dd88453844a5743fffe0cfbc Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 4 Feb 2020 08:31:06 -0800 Subject: [PATCH 46/47] Remove getNullCell Replace with fetching cell (0,0) and using that as the initial null cell. We can reconsider adding this back if it's needed --- .../src/SerializeAddon.ts | 20 ++++++++++++++++--- src/public/Terminal.ts | 1 - typings/xterm.d.ts | 7 ------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 1d011726..94a3b7ce 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -16,9 +16,7 @@ abstract class BaseSerializeHandler { constructor(private _buffer: IBuffer) { } serialize(startRow: number, endRow: number): string { - // we need two of them to flip between old and new cell - const cell1 = this._buffer.getNullCell(); - const cell2 = this._buffer.getNullCell(); + const { cell1, cell2 } = this._getWorkCells(); let oldCell = cell1; this._beforeSerialize(endRow - startRow); @@ -44,6 +42,22 @@ abstract class BaseSerializeHandler { return this._serializeString(); } + private _getWorkCells(): { cell1: IBufferCell, cell2: IBufferCell } { + const line = this._buffer.getLine(0); + if (!line) { + throw new Error('Could not fetch first line for serialization'); + } + const cell1 = line.getCell(0); + if (!cell1) { + throw new Error('Could not fetch first cell for serialization'); + } + const cell2 = line.getCell(0); + if (!cell2) { + throw new Error('Could not fetch first cell for serialization'); + } + return { cell1, cell2 }; + } + protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } protected _rowEnd(row: number): void { } protected _beforeSerialize(rows: number): void { } diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 065dfb64..2b180a4a 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -204,7 +204,6 @@ class BufferApiView implements IBufferApi { } return new BufferLineApiView(line); } - public getNullCell(): IBufferCellApi { return new CellData(); } } class BufferLineApiView implements IBufferLineApi { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 5f97cf10..2fcce231 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -955,13 +955,6 @@ declare module 'xterm' { * @param y The line index to get. */ getLine(y: number): IBufferLine | undefined; - - /** - * Creates an empty cell object suitable as a cell reference in - * `line.getCell(x, cell)`. Use this to avoid costly recreation of - * cell objects when dealing with tons of cells. - */ - getNullCell(): IBufferCell; } /** From 9c8deb14705435377aa243ae6db041e608aa540e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 4 Feb 2020 08:36:57 -0800 Subject: [PATCH 47/47] Revert "Remove getNullCell" This reverts commit 0b6a5b763095f516dd88453844a5743fffe0cfbc. --- .../src/SerializeAddon.ts | 20 +++---------------- src/public/Terminal.ts | 1 + typings/xterm.d.ts | 7 +++++++ 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index 94a3b7ce..1d011726 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -16,7 +16,9 @@ abstract class BaseSerializeHandler { constructor(private _buffer: IBuffer) { } serialize(startRow: number, endRow: number): string { - const { cell1, cell2 } = this._getWorkCells(); + // we need two of them to flip between old and new cell + const cell1 = this._buffer.getNullCell(); + const cell2 = this._buffer.getNullCell(); let oldCell = cell1; this._beforeSerialize(endRow - startRow); @@ -42,22 +44,6 @@ abstract class BaseSerializeHandler { return this._serializeString(); } - private _getWorkCells(): { cell1: IBufferCell, cell2: IBufferCell } { - const line = this._buffer.getLine(0); - if (!line) { - throw new Error('Could not fetch first line for serialization'); - } - const cell1 = line.getCell(0); - if (!cell1) { - throw new Error('Could not fetch first cell for serialization'); - } - const cell2 = line.getCell(0); - if (!cell2) { - throw new Error('Could not fetch first cell for serialization'); - } - return { cell1, cell2 }; - } - protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } protected _rowEnd(row: number): void { } protected _beforeSerialize(rows: number): void { } diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 2b180a4a..065dfb64 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -204,6 +204,7 @@ class BufferApiView implements IBufferApi { } return new BufferLineApiView(line); } + public getNullCell(): IBufferCellApi { return new CellData(); } } class BufferLineApiView implements IBufferLineApi { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 2685d55c..7b430722 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1117,6 +1117,13 @@ declare module 'xterm' { * @param y The line index to get. */ getLine(y: number): IBufferLine | undefined; + + /** + * Creates an empty cell object suitable as a cell reference in + * `line.getCell(x, cell)`. Use this to avoid costly recreation of + * cell objects when dealing with tons of cells. + */ + getNullCell(): IBufferCell; } /**