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/.gitignore b/addons/xterm-addon-serialize/.gitignore new file mode 100644 index 00000000..03c051b3 --- /dev/null +++ b/addons/xterm-addon-serialize/.gitignore @@ -0,0 +1,3 @@ +lib +node_modules +out-benchmark 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/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/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts b/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts new file mode 100644 index 00000000..025e7194 --- /dev/null +++ b/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +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 'SerializeAddon'; + +class TestTerminal extends Terminal { + writeSync(data: string): void { + (this)._core.writeSync(data); + } +} + +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; + const serializeAddon = new SerializeAddon(); + before(() => { + terminal = new TestTerminal({ cols: 80, rows: 25, scrollback: 5000 }); + serializeAddon.activate(terminal); + terminal.writeSync(content); + }); + new ThroughputRuntimeCase('', () => { + return { payloadSize: serializeAddon.serialize().length }; + }, { fork: false }).showAverageThroughput(); + }); +}); 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 new file mode 100644 index 00000000..787462f9 --- /dev/null +++ b/addons/xterm-addon-serialize/package.json @@ -0,0 +1,23 @@ +{ + "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", + "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/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts new file mode 100644 index 00000000..a34aa0bd --- /dev/null +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -0,0 +1,370 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as puppeteer from 'puppeteer'; +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 { + browser = await puppeteer.launch({ + headless: process.argv.indexOf('--headless') !== -1, + args: [`--window-size=${width},${height}`] + }); + page = (await browser.pages())[0]; + await page.setViewport({ width, height }); + await page.goto(APP); + 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 { + const cols = 10; + const lines = [ + '', + '', + digitsString(cols), + digitsString(cols), + '', + '', + digitsString(cols), + digitsString(cols), + '', + '', + '' + ]; + 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 { + const rows = 10; + const cols = 10; + const digitsLine = digitsString(cols); + const lines = newArray(digitsLine, rows); + 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 { + const rows = 10; + const halfRows = rows >> 1; + const cols = 10; + const lines = newArray((index: number) => digitsString(cols, index), rows); + await writeSync(page, lines.join('\\r\\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize(${halfRows});`), lines.slice(halfRows, 2 * halfRows).join('\r\n')); + }); + + it('serialize 0 rows of content', async function (): Promise { + const rows = 10; + const cols = 10; + const lines = newArray((index: number) => digitsString(cols, index), rows); + await writeSync(page, lines.join('\\r\\n')); + assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), ''); + }); + + it('serialize all rows of content with color16', async function (): Promise { + const cols = 10; + const color16 = [ + 30, 31, 32, 33, 34, 35, 36, 37, // Set foreground 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 + ); + 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 { + 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 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 { + const rows = 32; + const cols = 10; + const lines = newArray( + (index: number) => digitsString(cols, index, `\x1b[38;5;${16 + index}m`), + rows + ); + 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 { + 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 + ]; + 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 { + 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 + ]; + 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 { + 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 + ]; + 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 { + 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 + ]; + 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 { + 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 + ]; + 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 { + 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 + ]; + 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', + '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();`), expected.join('\r\n')); + }); +}); + +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, sgr: string = ''): string { + let s = sgr; + for (let i = 0; i < length; i++) { + 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;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'; +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;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'; +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'; + +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); + }); + } +} diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts new file mode 100644 index 00000000..1d011726 --- /dev/null +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -0,0 +1,187 @@ +/** + * 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 constrain(value: number, low: number, high: number): number { + return Math.max(low, Math.min(value, high)); +} + +// TODO: Refine this template class later +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(); + let oldCell = cell1; + + 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 c = line.getCell(col, oldCell === cell1 ? cell2 : cell1); + if (!c) { + console.warn(`Can't get cell at row=${row}, col=${col}`); + continue; + } + this._nextCell(c, oldCell, row, col); + oldCell = c; + } + } + this._rowEnd(row); + } + + this._afterSerialize(); + + return this._serializeString(); + } + + protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } + protected _rowEnd(row: number): void { } + protected _beforeSerialize(rows: number): void { } + protected _afterSerialize(): void { } + protected _serializeString(): 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(); + private _currentRow: string = ''; + private _nullCellCount: number = 0; + + constructor(buffer: IBuffer) { + super(buffer); + } + + protected _beforeSerialize(rows: number): void { + this._allRows = new Array(rows); + } + + protected _rowEnd(row: number): void { + this._allRows[this._rowIndex++] = this._currentRow; + this._currentRow = ''; + this._nullCellCount = 0; + } + + protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { + const sgrSeq: number[] = []; + const fgChanged = !equalFg(cell, oldCell); + const bgChanged = !equalBg(cell, oldCell); + const flagsChanged = !equalFlags(cell, 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.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.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) { + 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.getChars() === '') { + this._nullCellCount++; + } else if (this._nullCellCount > 0) { + this._currentRow += `\x1b[${this._nullCellCount}C`; + this._nullCellCount = 0; + } + + this._currentRow += cell.getChars(); + } + + 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'); + } +} + +export class SerializeAddon implements ITerminalAddon { + private _terminal: Terminal | undefined; + + constructor() { } + + public activate(terminal: Terminal): void { + this._terminal = terminal; + } + + public serialize(rows?: number): string { + // 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.buffer.length; + const handler = new StringSerializeHandler(this._terminal.buffer); + + rows = (rows === undefined) ? maxRows : constrain(rows, 0, maxRows); + + return handler.serialize(maxRows - rows, maxRows); + } + + public dispose(): void { } +} diff --git a/addons/xterm-addon-serialize/src/tsconfig.json b/addons/xterm-addon-serialize/src/tsconfig.json new file mode 100644 index 00000000..57f3d6ed --- /dev/null +++ b/addons/xterm-addon-serialize/src/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "lib": [ + "dom", + "es2015" + ], + "rootDir": ".", + "outDir": "../out", + "sourceMap": true, + "removeComments": true, + "baseUrl": ".", + "paths": { + "common/*": [ "../../../src/common/*" ] + }, + "strict": true + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ], + "references": [ + { "path": "../../../src/common" } + ] +} 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..9e7b500a --- /dev/null +++ b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts @@ -0,0 +1,38 @@ +/** + * 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; + + /** + * 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 bottom of the + * terminal. This defaults to the number of rows in the viewport. + */ + 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/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts index 332abeb8..6c67febb 100644 --- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts +++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts @@ -283,7 +283,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, fg, chars); } else { this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg); diff --git a/bin/test.js b/bin/test.js index bd1784fb..1380a512 100644 --- a/bin/test.js +++ b/bin/test.js @@ -14,7 +14,8 @@ env.NODE_PATH = path.resolve(__dirname, '../out'); let testFiles = [ './out/*test.js', - './out/**/*test.js' + './out/**/*test.js', + './addons/**/out/*test.js', ]; let flagArgs = []; diff --git a/demo/client.ts b/demo/client.ts index eb4e92a9..6174216c 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -12,6 +12,7 @@ import { Terminal } from '../out/public/Terminal'; import { AttachAddon } from '../addons/xterm-addon-attach/out/AttachAddon'; import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon'; import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon'; +import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAddon'; import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon'; import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon'; import { Unicode11Addon } from '../addons/xterm-addon-unicode11/out/Unicode11Addon'; @@ -21,6 +22,7 @@ import { Unicode11Addon } from '../addons/xterm-addon-unicode11/out/Unicode11Add // 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'; // import { Unicode11Addon } from 'xterm-addon-unicode11'; @@ -35,6 +37,7 @@ export interface IWindowWithTerminal extends Window { AttachAddon?: typeof AttachAddon; FitAddon?: typeof FitAddon; SearchAddon?: typeof SearchAddon; + SerializeAddon?: typeof SerializeAddon; WebLinksAddon?: typeof WebLinksAddon; WebglAddon?: typeof WebglAddon; Unicode11Addon?: typeof Unicode11Addon; @@ -47,7 +50,7 @@ let socketURL; let socket; let pid; -type AddonType = 'attach' | 'fit' | 'search' | 'web-links' | 'webgl' | 'unicode11'; +type AddonType = 'attach' | 'fit' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl'; interface IDemoAddon { name: T; @@ -56,6 +59,7 @@ interface IDemoAddon { T extends 'attach' ? typeof AttachAddon : T extends 'fit' ? typeof FitAddon : T extends 'search' ? typeof SearchAddon : + T extends 'serialize' ? typeof SerializeAddon : T extends 'web-links' ? typeof WebLinksAddon : T extends 'unicode11' ? typeof Unicode11Addon : typeof WebglAddon; @@ -63,6 +67,7 @@ interface IDemoAddon { T extends 'attach' ? AttachAddon : T extends 'fit' ? FitAddon : T extends 'search' ? SearchAddon : + T extends 'serialize' ? SerializeAddon : T extends 'web-links' ? WebLinksAddon : T extends 'webgl' ? WebglAddon : T extends 'unicode11' ? typeof Unicode11Addon : @@ -73,6 +78,7 @@ const addons: { [T in AddonType]: IDemoAddon} = { attach: { name: 'attach', ctor: AttachAddon, canChange: false }, fit: { name: 'fit', ctor: FitAddon, canChange: false }, search: { name: 'search', ctor: SearchAddon, canChange: true }, + serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true }, 'web-links': { name: 'web-links', ctor: WebLinksAddon, canChange: true }, webgl: { name: 'webgl', ctor: WebglAddon, canChange: true }, unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true } @@ -119,12 +125,14 @@ if (document.location.pathname === '/test') { window.AttachAddon = AttachAddon; window.FitAddon = FitAddon; window.SearchAddon = SearchAddon; + window.SerializeAddon = SerializeAddon; + window.Unicode11Addon = Unicode11Addon; window.WebLinksAddon = WebLinksAddon; window.WebglAddon = WebglAddon; - window.Unicode11Addon = Unicode11Addon; } else { createTerminal(); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); + document.getElementById('serialize').addEventListener('click', serializeButtonHandler); } function createTerminal(): void { @@ -140,14 +148,16 @@ function createTerminal(): void { // Load addons const typedTerm = term as TerminalType; - addons['web-links'].instance = new WebLinksAddon(); addons.search.instance = new SearchAddon(); + addons.serialize.instance = new SerializeAddon(); addons.fit.instance = new FitAddon(); addons.unicode11.instance = new Unicode11Addon(); - typedTerm.loadAddon(addons['web-links'].instance); - typedTerm.loadAddon(addons.search.instance); + addons['web-links'].instance = new WebLinksAddon(); typedTerm.loadAddon(addons.fit.instance); + typedTerm.loadAddon(addons.search.instance); + typedTerm.loadAddon(addons.serialize.instance); typedTerm.loadAddon(addons.unicode11.instance); + typedTerm.loadAddon(addons['web-links'].instance); window.term = term; // Expose `term` to window for debugging purposes term.onResize((size: { cols: number, rows: number }) => { @@ -392,3 +402,14 @@ function updateTerminalSize(): void { terminalContainer.style.height = height; addons.fit.instance.fit(); } + +function serializeButtonHandler(): void { + const output = addons.serialize.instance.serialize(); + const outputString = JSON.stringify(output); + + 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 88c6bc4c..4d5149f3 100644 --- a/demo/index.html +++ b/demo/index.html @@ -11,16 +11,6 @@

xterm.js: A terminal for the web

-
-

Actions

-

- - - - - -

-

Options

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

@@ -30,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/style.css b/demo/style.css index b061dfcb..9c5fd0bd 100644 --- a/demo/style.css +++ b/demo/style.css @@ -30,3 +30,14 @@ p { padding-left: 20px; vertical-align: top; } + +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; +} diff --git a/demo/tsconfig.json b/demo/tsconfig.json index 7a53302d..4e1b5035 100644 --- a/demo/tsconfig.json +++ b/demo/tsconfig.json @@ -9,6 +9,7 @@ "xterm-addon-attach": ["../addons/xterm-addon-attach"], "xterm-addon-fit": ["../addons/xterm-addon-fit"], "xterm-addon-search": ["../addons/xterm-addon-search"], + "xterm-addon-serialize": ["../addons/xterm-addon-serialize"], "xterm-addon-web-links": ["../addons/xterm-addon-web-links"], "xterm-addon-webgl": ["../addons/xterm-addon-webgl"] } diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 8aaa06f2..b250b45e 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/public/Terminal.ts b/src/public/Terminal.ts index c96af698..065dfb64 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, IParser, IFunctionIdentifier, IUnicodeHandling, IUnicodeVersionProvider } 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'; @@ -189,7 +190,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,29 +204,30 @@ class BufferApiView implements IBufferApi { } return new BufferLineApiView(line); } + public getNullCell(): IBufferCellApi { return new CellData(); } } class BufferLineApiView implements IBufferLineApi { - constructor(private _line: IBufferLine) {} + constructor(private _line: IBufferLine) { } public get isWrapped(): boolean { return this._line.isWrapped; } - public getCell(x: number): IBufferCellApi | undefined { + public get length(): number { return this._line.length; } + public getCell(x: number, cell?: IBufferCellApi): IBufferCellApi | undefined { if (x < 0 || x >= this._line.length) { return undefined; } - return new BufferCellApiView(this._line, x); + + if (cell) { + this._line.loadCell(x, cell); + return cell; + } + 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(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); } -} - class ParserApi implements IParser { constructor(private _core: ITerminal) {} 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); }); }); }); diff --git a/test/benchmark/tsconfig.json b/test/benchmark/tsconfig.json index 8b93dcb1..cac99d90 100644 --- a/test/benchmark/tsconfig.json +++ b/test/benchmark/tsconfig.json @@ -16,7 +16,7 @@ "paths": { "common/*": [ "../../src/common/*" ], "browser/*": [ "../../src/browser/*" ], - "Terminal": [ "../../src/Terminal" ] + "Terminal": ["../../src/Terminal"] }, }, "include": [ @@ -31,4 +31,4 @@ { "path": "../../src/common" }, { "path": "../../src/browser" }, ] -} \ No newline at end of file +} diff --git a/tsconfig.all.json b/tsconfig.all.json index 8a26c302..8143ea91 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -10,6 +10,8 @@ { "path": "./addons/xterm-addon-search/src" }, { "path": "./addons/xterm-addon-unicode11/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" }, + { "path": "./addons/xterm-addon-serialize/benchmark" } ] } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index fc2549bc..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; } /** @@ -1128,6 +1135,12 @@ declare module 'xterm' { */ readonly isWrapped: boolean; + /** + * The length of the line, all call to getCell beyond the length will result + * in `undefined`. + */ + readonly length: number; + /** * Gets a cell from the line, or undefined if the line index does not exist. * @@ -1136,8 +1149,11 @@ declare module 'xterm' { * behavior. * * @param x The character index to get. + * @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): 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 @@ -1154,19 +1170,102 @@ 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`. + * - `1` for most cells. + * - `2` for wide character like CJK glyphs. + * - `0` for cells immediately following cells with a width of `2`. */ - readonly width: number; + 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; + + /** Whether the cell has the bold attribute (CSI 1 m). */ + isBold(): 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; + /** 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; } /**