Merge remote-tracking branch 'upstream/master' into pr/tisilent/4847
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
lib
|
||||
node_modules
|
||||
out-benchmark
|
||||
@@ -0,0 +1,29 @@
|
||||
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
|
||||
*
|
||||
!*/
|
||||
|
||||
# Whitelist - lib/
|
||||
!lib/**/*.d.ts
|
||||
|
||||
!lib/**/*.js
|
||||
!lib/**/*.js.map
|
||||
|
||||
!lib/**/*.css
|
||||
|
||||
# Whitelist - src/
|
||||
!src/**/*.ts
|
||||
!src/**/*.d.ts
|
||||
|
||||
!src/**/*.js
|
||||
!src/**/*.js.map
|
||||
|
||||
!src/**/*.css
|
||||
|
||||
# Blacklist - src/ test files
|
||||
src/**/*.test.ts
|
||||
src/**/*.test.d.ts
|
||||
src/**/*.test.js
|
||||
src/**/*.test.js.map
|
||||
|
||||
# Whitelist - typings/
|
||||
!typings/*.d.ts
|
||||
@@ -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/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/addon-serialize/typings/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/addon-serialize
|
||||
$ yarn benchmark && yarn benchmark-baseline
|
||||
$ # change some code in `@xterm/addon-serialize`
|
||||
$ yarn benchmark-eval
|
||||
```
|
||||
@@ -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 'browser/public/Terminal';
|
||||
import { SerializeAddon } from 'SerializeAddon';
|
||||
|
||||
class TestTerminal extends Terminal {
|
||||
public writeSync(data: string): void {
|
||||
(this as any)._core.writeSync(data);
|
||||
}
|
||||
}
|
||||
|
||||
perfContext('Terminal: sh -c "dd if=/dev/urandom count=40 bs=1k | hexdump | lolcat -f"', () => {
|
||||
let content = '';
|
||||
let contentUtf8: Uint8Array;
|
||||
|
||||
before(async () => {
|
||||
const p = spawn('sh', ['-c', 'dd if=/dev/urandom 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.onData(data => {
|
||||
chunks.push(data as unknown as Buffer);
|
||||
length += data.length;
|
||||
});
|
||||
await new Promise<void>(resolve => p.onExit(() => 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();
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2021"
|
||||
],
|
||||
"outDir": "../out-benchmark",
|
||||
"types": ["../../../node_modules/@types/node"],
|
||||
"moduleResolution": "node",
|
||||
"strict": false,
|
||||
"target": "es2021",
|
||||
"module": "commonjs",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": ["../../../src/common/*"],
|
||||
"browser/*": ["../../../src/browser/*"],
|
||||
"SerializeAddon": ["../src/SerializeAddon"],
|
||||
"xterm-addon-serialize": [
|
||||
"../typings/xterm-addon-serialize.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["../**/*", "../../../typings/xterm.d.ts"],
|
||||
"exclude": ["../../../**/*test.ts", "../../**/*api.ts"],
|
||||
"references": [
|
||||
{ "path": "../../../src/common" },
|
||||
{ "path": "../../../src/browser" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@xterm/addon-serialize",
|
||||
"version": "0.11.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
},
|
||||
"main": "lib/addon-serialize.js",
|
||||
"types": "typings/addon-serialize.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-serialize",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"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/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/benchmark/*benchmark.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@xterm/xterm": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { SerializeAddon } from './SerializeAddon';
|
||||
import { Terminal } from 'browser/public/Terminal';
|
||||
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { OptionsService } from 'common/services/OptionsService';
|
||||
import { ThemeService } from 'browser/services/ThemeService';
|
||||
|
||||
function sgr(...seq: string[]): string {
|
||||
return `\x1b[${seq.join(';')}m`;
|
||||
}
|
||||
|
||||
function writeP(terminal: Terminal, data: string | Uint8Array): Promise<void> {
|
||||
return new Promise(r => terminal.write(data, r));
|
||||
}
|
||||
|
||||
class TestSelectionService {
|
||||
private _model: SelectionModel;
|
||||
private _hasSelection: boolean = false;
|
||||
|
||||
constructor(
|
||||
bufferService: IBufferService
|
||||
) {
|
||||
this._model = new SelectionModel(bufferService);
|
||||
}
|
||||
|
||||
public get model(): SelectionModel { return this._model; }
|
||||
|
||||
public get hasSelection(): boolean { return this._hasSelection; }
|
||||
|
||||
public get selectionStart(): [number, number] | undefined { return this._model.finalSelectionStart; }
|
||||
public get selectionEnd(): [number, number] | undefined { return this._model.finalSelectionEnd; }
|
||||
|
||||
public setSelection(col: number, row: number, length: number): void {
|
||||
this._model.selectionStart = [col, row];
|
||||
this._model.selectionStartLength = length;
|
||||
this._hasSelection = true;
|
||||
}
|
||||
}
|
||||
|
||||
describe('SerializeAddon', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
let window: jsdom.DOMWindow;
|
||||
|
||||
let serializeAddon: SerializeAddon;
|
||||
let terminal: Terminal;
|
||||
|
||||
before(() => {
|
||||
serializeAddon = new SerializeAddon();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
|
||||
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
|
||||
createLinearGradient(): any {
|
||||
return null;
|
||||
},
|
||||
|
||||
fillRect(): void { },
|
||||
|
||||
getImageData(): any {
|
||||
return { data: [0, 0, 0, 0xFF] };
|
||||
}
|
||||
});
|
||||
|
||||
terminal = new Terminal({ cols: 10, rows: 2, allowProposedApi: true });
|
||||
terminal.loadAddon(serializeAddon);
|
||||
|
||||
(terminal as any)._core._themeService = new ThemeService((terminal as any)._core.optionsService);
|
||||
(terminal as any)._core._selectionService = new TestSelectionService((terminal as any)._core._bufferService);
|
||||
});
|
||||
|
||||
describe('text', () => {
|
||||
it('restoring cursor styles', async () => {
|
||||
await writeP(terminal, sgr('32') + '> ' + sgr('0'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[32m> \u001b[0m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('html', () => {
|
||||
it('empty terminal with selection turned off', () => {
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.notEqual(output, '');
|
||||
assert.equal((output.match(/<div><span> {10}<\/span><\/div>/g) || []).length, 2);
|
||||
});
|
||||
|
||||
it('empty terminal with no selection', () => {
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
onlySelection: true
|
||||
});
|
||||
assert.equal(output, '');
|
||||
});
|
||||
|
||||
it('basic terminal with selection', async () => {
|
||||
await writeP(terminal, ' terminal ');
|
||||
terminal.select(1, 0, 8);
|
||||
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
onlySelection: true
|
||||
});
|
||||
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with bold styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-weight: bold;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with italic styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-style: italic;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with inverse styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #000000; background-color: #BFBFBF;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with invisible styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='visibility: hidden;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with dim styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='opacity: 0.5;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with strikethrough styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with combined styling', async () => {
|
||||
await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-weight: bold;'> <\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='font-weight: bold; text-decoration: line-through;'>termi<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>nal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with color styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with background styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='background-color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with default options', async () => {
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with custom options', async () => {
|
||||
terminal.options.fontFamily = 'verdana';
|
||||
terminal.options.fontSize = 20;
|
||||
terminal.options.theme = {
|
||||
foreground: '#ff00ff',
|
||||
background: '#00ff00'
|
||||
};
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
includeGlobalBackground: true
|
||||
});
|
||||
assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with background included', async () => {
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
includeGlobalBackground: true
|
||||
});
|
||||
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with custom color styling', async () => {
|
||||
terminal.options.theme.black = '#ffa500';
|
||||
terminal.options.theme = { ... terminal.options.theme };
|
||||
|
||||
await writeP(terminal, ' ' + sgr('38;5;0') + 'terminal' + sgr('39') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #ffa500;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with color styling - xterm headless', async () => {
|
||||
// a headless terminal doesn't have a themeservice
|
||||
(terminal as any)._core._themeService = undefined;
|
||||
|
||||
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,681 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* (EXPERIMENTAL) This Addon is still under development
|
||||
*/
|
||||
|
||||
import { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from '@xterm/xterm';
|
||||
import { SerializeAddon as ISerializeApi } from '@xterm/addon-serialize';
|
||||
import { DEFAULT_ANSI_COLORS } from 'browser/services/ThemeService';
|
||||
import { IAttributeData, IColor } from 'common/Types';
|
||||
|
||||
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(
|
||||
protected readonly _buffer: IBuffer
|
||||
) {
|
||||
}
|
||||
|
||||
public serialize(range: IBufferRange): 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;
|
||||
|
||||
const startRow = range.start.x;
|
||||
const endRow = range.end.x;
|
||||
const startColumn = range.start.y;
|
||||
const endColumn = range.end.y;
|
||||
|
||||
this._beforeSerialize(endRow - startRow, startRow, endRow);
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
const line = this._buffer.getLine(row);
|
||||
if (line) {
|
||||
const startLineColumn = row !== range.start.x ? 0 : startColumn;
|
||||
const endLineColumn = row !== range.end.x ? line.length : endColumn;
|
||||
for (let col = startLineColumn; col < endLineColumn; 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, row === endRow);
|
||||
}
|
||||
|
||||
this._afterSerialize();
|
||||
|
||||
return this._serializeString();
|
||||
}
|
||||
|
||||
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { }
|
||||
protected _rowEnd(row: number, isLastRow: boolean): void { }
|
||||
protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { }
|
||||
protected _afterSerialize(): void { }
|
||||
protected _serializeString(): string { return ''; }
|
||||
}
|
||||
|
||||
function equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
return cell1.getFgColorMode() === cell2.getFgColorMode()
|
||||
&& cell1.getFgColor() === cell2.getFgColor();
|
||||
}
|
||||
|
||||
function equalBg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
return cell1.getBgColorMode() === cell2.getBgColorMode()
|
||||
&& cell1.getBgColor() === cell2.getBgColor();
|
||||
}
|
||||
|
||||
function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
return cell1.isInverse() === cell2.isInverse()
|
||||
&& cell1.isBold() === cell2.isBold()
|
||||
&& cell1.isUnderline() === cell2.isUnderline()
|
||||
&& cell1.isOverline() === cell2.isOverline()
|
||||
&& cell1.isBlink() === cell2.isBlink()
|
||||
&& cell1.isInvisible() === cell2.isInvisible()
|
||||
&& cell1.isItalic() === cell2.isItalic()
|
||||
&& cell1.isDim() === cell2.isDim()
|
||||
&& cell1.isStrikethrough() === cell2.isStrikethrough();
|
||||
}
|
||||
|
||||
class StringSerializeHandler extends BaseSerializeHandler {
|
||||
private _rowIndex: number = 0;
|
||||
private _allRows: string[] = new Array<string>();
|
||||
private _allRowSeparators: string[] = new Array<string>();
|
||||
private _currentRow: string = '';
|
||||
private _nullCellCount: number = 0;
|
||||
|
||||
// we can see a full colored cell and a null cell that only have background the same style
|
||||
// but the information isn't preserved by null cell itself
|
||||
// so wee need to record it when required.
|
||||
private _cursorStyle: IBufferCell = this._buffer.getNullCell();
|
||||
|
||||
// where exact the cursor styles comes from
|
||||
// because we can't copy the cell directly
|
||||
// so we remember where the content comes from instead
|
||||
private _cursorStyleRow: number = 0;
|
||||
private _cursorStyleCol: number = 0;
|
||||
|
||||
// this is a null cell for reference for checking whether background is empty or not
|
||||
private _backgroundCell: IBufferCell = this._buffer.getNullCell();
|
||||
|
||||
private _firstRow: number = 0;
|
||||
private _lastCursorRow: number = 0;
|
||||
private _lastCursorCol: number = 0;
|
||||
private _lastContentCursorRow: number = 0;
|
||||
private _lastContentCursorCol: number = 0;
|
||||
|
||||
constructor(
|
||||
buffer: IBuffer,
|
||||
private readonly _terminal: Terminal
|
||||
) {
|
||||
super(buffer);
|
||||
}
|
||||
|
||||
protected _beforeSerialize(rows: number, start: number, end: number): void {
|
||||
this._allRows = new Array<string>(rows);
|
||||
this._lastContentCursorRow = start;
|
||||
this._lastCursorRow = start;
|
||||
this._firstRow = start;
|
||||
}
|
||||
|
||||
private _thisRowLastChar: IBufferCell = this._buffer.getNullCell();
|
||||
private _thisRowLastSecondChar: IBufferCell = this._buffer.getNullCell();
|
||||
private _nextRowFirstChar: IBufferCell = this._buffer.getNullCell();
|
||||
protected _rowEnd(row: number, isLastRow: boolean): void {
|
||||
// if there is colorful empty cell at line end, whe must pad it back, or the the color block
|
||||
// will missing
|
||||
if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||
// use clear right to set background.
|
||||
this._currentRow += `\u001b[${this._nullCellCount}X`;
|
||||
}
|
||||
|
||||
let rowSeparator = '';
|
||||
|
||||
// handle row separator
|
||||
if (!isLastRow) {
|
||||
// Enable BCE
|
||||
if (row - this._firstRow >= this._terminal.rows) {
|
||||
this._buffer.getLine(this._cursorStyleRow)?.getCell(this._cursorStyleCol, this._backgroundCell);
|
||||
}
|
||||
|
||||
// Fetch current line
|
||||
const currentLine = this._buffer.getLine(row)!;
|
||||
// Fetch next line
|
||||
const nextLine = this._buffer.getLine(row + 1)!;
|
||||
|
||||
if (!nextLine.isWrapped) {
|
||||
// just insert the line break
|
||||
rowSeparator = '\r\n';
|
||||
// we sended the enter
|
||||
this._lastCursorRow = row + 1;
|
||||
this._lastCursorCol = 0;
|
||||
} else {
|
||||
rowSeparator = '';
|
||||
const thisRowLastChar = currentLine.getCell(currentLine.length - 1, this._thisRowLastChar)!;
|
||||
const thisRowLastSecondChar = currentLine.getCell(currentLine.length - 2, this._thisRowLastSecondChar)!;
|
||||
const nextRowFirstChar = nextLine.getCell(0, this._nextRowFirstChar)!;
|
||||
const isNextRowFirstCharDoubleWidth = nextRowFirstChar.getWidth() > 1;
|
||||
|
||||
// validate whether this line wrap is ever possible
|
||||
// which mean whether cursor can placed at a overflow position (x === row) naturally
|
||||
let isValid = false;
|
||||
|
||||
if (
|
||||
// you must output character to cause overflow, control sequence can't do this
|
||||
nextRowFirstChar.getChars() &&
|
||||
isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0
|
||||
) {
|
||||
if (
|
||||
// the last character can't be null,
|
||||
// you can't use control sequence to move cursor to (x === row)
|
||||
(thisRowLastChar.getChars() || thisRowLastChar.getWidth() === 0) &&
|
||||
// change background of the first wrapped cell also affects BCE
|
||||
// so we mark it as invalid to simply the process to determine line separator
|
||||
equalBg(thisRowLastChar, nextRowFirstChar)
|
||||
) {
|
||||
isValid = true;
|
||||
}
|
||||
|
||||
if (
|
||||
// the second to last character can't be null if the next line starts with CJK,
|
||||
// you can't use control sequence to move cursor to (x === row)
|
||||
isNextRowFirstCharDoubleWidth &&
|
||||
(thisRowLastSecondChar.getChars() || thisRowLastSecondChar.getWidth() === 0) &&
|
||||
// change background of the first wrapped cell also affects BCE
|
||||
// so we mark it as invalid to simply the process to determine line separator
|
||||
equalBg(thisRowLastChar, nextRowFirstChar) &&
|
||||
equalBg(thisRowLastSecondChar, nextRowFirstChar)
|
||||
) {
|
||||
isValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
// force the wrap with magic
|
||||
// insert enough character to force the wrap
|
||||
rowSeparator = '-'.repeat(this._nullCellCount + 1);
|
||||
// move back and erase next line head
|
||||
rowSeparator += '\u001b[1D\u001b[1X';
|
||||
|
||||
if (this._nullCellCount > 0) {
|
||||
// do these because we filled the last several null slot, which we shouldn't
|
||||
rowSeparator += '\u001b[A';
|
||||
rowSeparator += `\u001b[${currentLine.length - this._nullCellCount}C`;
|
||||
rowSeparator += `\u001b[${this._nullCellCount}X`;
|
||||
rowSeparator += `\u001b[${currentLine.length - this._nullCellCount}D`;
|
||||
rowSeparator += '\u001b[B';
|
||||
}
|
||||
|
||||
// This is content and need the be serialized even it is invisible.
|
||||
// without this, wrap will be missing from outputs.
|
||||
this._lastContentCursorRow = row + 1;
|
||||
this._lastContentCursorCol = 0;
|
||||
|
||||
// force commit the cursor position
|
||||
this._lastCursorRow = row + 1;
|
||||
this._lastCursorCol = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._allRows[this._rowIndex] = this._currentRow;
|
||||
this._allRowSeparators[this._rowIndex++] = rowSeparator;
|
||||
this._currentRow = '';
|
||||
this._nullCellCount = 0;
|
||||
}
|
||||
|
||||
private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] {
|
||||
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()) {
|
||||
if (!oldCell.isAttributeDefault()) {
|
||||
sgrSeq.push(0);
|
||||
}
|
||||
} 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.isOverline() !== oldCell.isOverline()) { sgrSeq.push(cell.isOverline() ? 53 : 55); }
|
||||
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 (cell.isStrikethrough() !== oldCell.isStrikethrough()) { sgrSeq.push(cell.isStrikethrough() ? 9 : 29); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sgrSeq;
|
||||
}
|
||||
|
||||
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {
|
||||
// a width 0 cell don't need to be count because it is just a placeholder after a CJK character;
|
||||
const isPlaceHolderCell = cell.getWidth() === 0;
|
||||
|
||||
if (isPlaceHolderCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this cell don't have content
|
||||
const isEmptyCell = cell.getChars() === '';
|
||||
|
||||
const sgrSeq = this._diffStyle(cell, this._cursorStyle);
|
||||
|
||||
// the empty cell style is only assumed to be changed when background changed, because
|
||||
// foreground is always 0.
|
||||
const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0;
|
||||
|
||||
/**
|
||||
* handles style change
|
||||
*/
|
||||
if (styleChanged) {
|
||||
// before update the style, we need to fill empty cell back
|
||||
if (this._nullCellCount > 0) {
|
||||
// use clear right to set background.
|
||||
if (!equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||
this._currentRow += `\u001b[${this._nullCellCount}X`;
|
||||
}
|
||||
// use move right to move cursor.
|
||||
this._currentRow += `\u001b[${this._nullCellCount}C`;
|
||||
this._nullCellCount = 0;
|
||||
}
|
||||
|
||||
this._lastContentCursorRow = this._lastCursorRow = row;
|
||||
this._lastContentCursorCol = this._lastCursorCol = col;
|
||||
|
||||
this._currentRow += `\u001b[${sgrSeq.join(';')}m`;
|
||||
|
||||
// update the last cursor style
|
||||
const line = this._buffer.getLine(row);
|
||||
if (line !== undefined) {
|
||||
line.getCell(col, this._cursorStyle);
|
||||
this._cursorStyleRow = row;
|
||||
this._cursorStyleCol = col;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handles actual content
|
||||
*/
|
||||
if (isEmptyCell) {
|
||||
this._nullCellCount += cell.getWidth();
|
||||
} else {
|
||||
if (this._nullCellCount > 0) {
|
||||
// we can just assume we have same style with previous one here
|
||||
// because style change is handled by previous stage
|
||||
// use move right when background is empty, use clear right when there is background.
|
||||
if (equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||
this._currentRow += `\u001b[${this._nullCellCount}C`;
|
||||
} else {
|
||||
this._currentRow += `\u001b[${this._nullCellCount}X`;
|
||||
this._currentRow += `\u001b[${this._nullCellCount}C`;
|
||||
}
|
||||
this._nullCellCount = 0;
|
||||
}
|
||||
|
||||
this._currentRow += cell.getChars();
|
||||
|
||||
// update cursor
|
||||
this._lastContentCursorRow = this._lastCursorRow = row;
|
||||
this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth();
|
||||
}
|
||||
}
|
||||
|
||||
protected _serializeString(): string {
|
||||
let rowEnd = this._allRows.length;
|
||||
|
||||
// the fixup is only required for data without scrollback
|
||||
// because it will always be placed at last line otherwise
|
||||
if (this._buffer.length - this._firstRow <= this._terminal.rows) {
|
||||
rowEnd = this._lastContentCursorRow + 1 - this._firstRow;
|
||||
this._lastCursorCol = this._lastContentCursorCol;
|
||||
this._lastCursorRow = this._lastContentCursorRow;
|
||||
}
|
||||
|
||||
let content = '';
|
||||
|
||||
for (let i = 0; i < rowEnd; i++) {
|
||||
content += this._allRows[i];
|
||||
if (i + 1 < rowEnd) {
|
||||
content += this._allRowSeparators[i];
|
||||
}
|
||||
}
|
||||
|
||||
// restore the cursor
|
||||
const realCursorRow = this._buffer.baseY + this._buffer.cursorY;
|
||||
const realCursorCol = this._buffer.cursorX;
|
||||
|
||||
const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol);
|
||||
|
||||
const moveRight = (offset: number): void => {
|
||||
if (offset > 0) {
|
||||
content += `\u001b[${offset}C`;
|
||||
} else if (offset < 0) {
|
||||
content += `\u001b[${-offset}D`;
|
||||
}
|
||||
};
|
||||
const moveDown = (offset: number): void => {
|
||||
if (offset > 0) {
|
||||
content += `\u001b[${offset}B`;
|
||||
} else if (offset < 0) {
|
||||
content += `\u001b[${-offset}A`;
|
||||
}
|
||||
};
|
||||
|
||||
if (cursorMoved) {
|
||||
moveDown(realCursorRow - this._lastCursorRow);
|
||||
moveRight(realCursorCol - this._lastCursorCol);
|
||||
}
|
||||
|
||||
// Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677
|
||||
// HACK: Internal API access since it's awkward to expose this in the API and serialize will
|
||||
// likely be the only consumer
|
||||
const curAttrData: IAttributeData = (this._terminal as any)._core._inputHandler._curAttrData;
|
||||
const sgrSeq = this._diffStyle(curAttrData, this._cursorStyle);
|
||||
if (sgrSeq.length > 0) {
|
||||
content += `\u001b[${sgrSeq.join(';')}m`;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
export class SerializeAddon implements ITerminalAddon , ISerializeApi {
|
||||
private _terminal: Terminal | undefined;
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
}
|
||||
|
||||
private _serializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string {
|
||||
const maxRows = buffer.length;
|
||||
const handler = new StringSerializeHandler(buffer, terminal);
|
||||
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
|
||||
return handler.serialize({
|
||||
start: { x: maxRows - correctRows, y: 0 },
|
||||
end: { x: maxRows - 1, y: terminal.cols }
|
||||
});
|
||||
}
|
||||
|
||||
private _serializeBufferAsHTML(terminal: Terminal, options: Partial<IHTMLSerializeOptions>): string {
|
||||
const buffer = terminal.buffer.active;
|
||||
const handler = new HTMLSerializeHandler(buffer, terminal, options);
|
||||
const onlySelection = options.onlySelection ?? false;
|
||||
if (!onlySelection) {
|
||||
const maxRows = buffer.length;
|
||||
const scrollback = options.scrollback;
|
||||
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
|
||||
return handler.serialize({
|
||||
start: { x: maxRows - correctRows, y: 0 },
|
||||
end: { x: maxRows - 1, y: terminal.cols }
|
||||
});
|
||||
}
|
||||
|
||||
const selection = this._terminal?.getSelectionPosition();
|
||||
if (selection !== undefined) {
|
||||
return handler.serialize({
|
||||
start: { x: selection.start.y, y: selection.start.x },
|
||||
end: { x: selection.end.y, y: selection.end.x }
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private _serializeModes(terminal: Terminal): string {
|
||||
let content = '';
|
||||
const modes = terminal.modes;
|
||||
|
||||
// Default: false
|
||||
if (modes.applicationCursorKeysMode) content += '\x1b[?1h';
|
||||
if (modes.applicationKeypadMode) content += '\x1b[?66h';
|
||||
if (modes.bracketedPasteMode) content += '\x1b[?2004h';
|
||||
if (modes.insertMode) content += '\x1b[4h';
|
||||
if (modes.originMode) content += '\x1b[?6h';
|
||||
if (modes.reverseWraparoundMode) content += '\x1b[?45h';
|
||||
if (modes.sendFocusMode) content += '\x1b[?1004h';
|
||||
|
||||
// Default: true
|
||||
if (modes.wraparoundMode === false) content += '\x1b[?7l';
|
||||
|
||||
// Default: 'none'
|
||||
if (modes.mouseTrackingMode !== 'none') {
|
||||
switch (modes.mouseTrackingMode) {
|
||||
case 'x10': content += '\x1b[?9h'; break;
|
||||
case 'vt200': content += '\x1b[?1000h'; break;
|
||||
case 'drag': content += '\x1b[?1002h'; break;
|
||||
case 'any': content += '\x1b[?1003h'; break;
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
public serialize(options?: ISerializeOptions): string {
|
||||
// TODO: Add combinedData support
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
|
||||
// Normal buffer
|
||||
let content = this._serializeBuffer(this._terminal, this._terminal.buffer.normal, options?.scrollback);
|
||||
|
||||
// Alternate buffer
|
||||
if (!options?.excludeAltBuffer) {
|
||||
if (this._terminal.buffer.active.type === 'alternate') {
|
||||
const alternativeScreenContent = this._serializeBuffer(this._terminal, this._terminal.buffer.alternate, undefined);
|
||||
content += `\u001b[?1049h\u001b[H${alternativeScreenContent}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Modes
|
||||
if (!options?.excludeModes) {
|
||||
content += this._serializeModes(this._terminal);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
public serializeAsHTML(options?: Partial<IHTMLSerializeOptions>): string {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
|
||||
return this._serializeBufferAsHTML(this._terminal, options || {});
|
||||
}
|
||||
|
||||
public dispose(): void { }
|
||||
}
|
||||
|
||||
|
||||
interface ISerializeOptions {
|
||||
scrollback?: number;
|
||||
excludeModes?: boolean;
|
||||
excludeAltBuffer?: boolean;
|
||||
}
|
||||
|
||||
interface IHTMLSerializeOptions {
|
||||
scrollback: number;
|
||||
onlySelection: boolean;
|
||||
includeGlobalBackground: boolean;
|
||||
}
|
||||
|
||||
export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
private _currentRow: string = '';
|
||||
|
||||
private _htmlContent = '';
|
||||
|
||||
private _ansiColors: Readonly<IColor[]>;
|
||||
|
||||
constructor(
|
||||
buffer: IBuffer,
|
||||
private readonly _terminal: Terminal,
|
||||
private readonly _options: Partial<IHTMLSerializeOptions>
|
||||
) {
|
||||
super(buffer);
|
||||
|
||||
// For xterm headless: fallback to ansi colors
|
||||
if ((_terminal as any)._core._themeService) {
|
||||
this._ansiColors = (_terminal as any)._core._themeService.colors.ansi;
|
||||
}
|
||||
else {
|
||||
this._ansiColors = DEFAULT_ANSI_COLORS;
|
||||
}
|
||||
}
|
||||
|
||||
private _padStart(target: string, targetLength: number, padString: string): string {
|
||||
targetLength = targetLength >> 0;
|
||||
padString = padString ?? ' ';
|
||||
if (target.length > targetLength) {
|
||||
return target;
|
||||
}
|
||||
|
||||
targetLength -= target.length;
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length);
|
||||
}
|
||||
return padString.slice(0, targetLength) + target;
|
||||
}
|
||||
|
||||
protected _beforeSerialize(rows: number, start: number, end: number): void {
|
||||
this._htmlContent += '<html><body><!--StartFragment--><pre>';
|
||||
|
||||
let foreground = '#000000';
|
||||
let background = '#ffffff';
|
||||
if (this._options.includeGlobalBackground ?? false) {
|
||||
foreground = this._terminal.options.theme?.foreground ?? '#ffffff';
|
||||
background = this._terminal.options.theme?.background ?? '#000000';
|
||||
}
|
||||
|
||||
const globalStyleDefinitions = [];
|
||||
globalStyleDefinitions.push('color: ' + foreground + ';');
|
||||
globalStyleDefinitions.push('background-color: ' + background + ';');
|
||||
globalStyleDefinitions.push('font-family: ' + this._terminal.options.fontFamily + ';');
|
||||
globalStyleDefinitions.push('font-size: ' + this._terminal.options.fontSize + 'px;');
|
||||
this._htmlContent += '<div style=\'' + globalStyleDefinitions.join(' ') + '\'>';
|
||||
}
|
||||
|
||||
protected _afterSerialize(): void {
|
||||
this._htmlContent += '</div>';
|
||||
this._htmlContent += '</pre><!--EndFragment--></body></html>';
|
||||
}
|
||||
|
||||
protected _rowEnd(row: number, isLastRow: boolean): void {
|
||||
this._htmlContent += '<div><span>' + this._currentRow + '</span></div>';
|
||||
this._currentRow = '';
|
||||
}
|
||||
|
||||
private _getHexColor(cell: IBufferCell, isFg: boolean): string | undefined {
|
||||
const color = isFg ? cell.getFgColor() : cell.getBgColor();
|
||||
if (isFg ? cell.isFgRGB() : cell.isBgRGB()) {
|
||||
const rgb = [
|
||||
(color >> 16) & 255,
|
||||
(color >> 8) & 255,
|
||||
(color ) & 255
|
||||
];
|
||||
return rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
|
||||
}
|
||||
if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
|
||||
return this._ansiColors[color].css;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
|
||||
const content: string[] = [];
|
||||
|
||||
const fgChanged = !equalFg(cell, oldCell);
|
||||
const bgChanged = !equalBg(cell, oldCell);
|
||||
const flagsChanged = !equalFlags(cell, oldCell);
|
||||
|
||||
if (fgChanged || bgChanged || flagsChanged) {
|
||||
const fgHexColor = this._getHexColor(cell, true);
|
||||
if (fgHexColor) {
|
||||
content.push('color: ' + fgHexColor + ';');
|
||||
}
|
||||
|
||||
const bgHexColor = this._getHexColor(cell, false);
|
||||
if (bgHexColor) {
|
||||
content.push('background-color: ' + bgHexColor + ';');
|
||||
}
|
||||
|
||||
if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }
|
||||
if (cell.isBold()) { content.push('font-weight: bold;'); }
|
||||
if (cell.isUnderline() && cell.isOverline()) { content.push('text-decoration: overline underline;'); }
|
||||
else if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
|
||||
else if (cell.isOverline()) { content.push('text-decoration: overline;'); }
|
||||
if (cell.isBlink()) { content.push('text-decoration: blink;'); }
|
||||
if (cell.isInvisible()) { content.push('visibility: hidden;'); }
|
||||
if (cell.isItalic()) { content.push('font-style: italic;'); }
|
||||
if (cell.isDim()) { content.push('opacity: 0.5;'); }
|
||||
if (cell.isStrikethrough()) { content.push('text-decoration: line-through;'); }
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {
|
||||
// a width 0 cell don't need to be count because it is just a placeholder after a CJK character;
|
||||
const isPlaceHolderCell = cell.getWidth() === 0;
|
||||
if (isPlaceHolderCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this cell don't have content
|
||||
const isEmptyCell = cell.getChars() === '';
|
||||
|
||||
const styleDefinitions = this._diffStyle(cell, oldCell);
|
||||
|
||||
// handles style change
|
||||
if (styleDefinitions) {
|
||||
this._currentRow += styleDefinitions.length === 0 ?
|
||||
'</span><span>' :
|
||||
'</span><span style=\'' + styleDefinitions.join(' ') + '\'>';
|
||||
}
|
||||
|
||||
// handles actual content
|
||||
if (isEmptyCell) {
|
||||
this._currentRow += ' ';
|
||||
} else {
|
||||
this._currentRow += cell.getChars();
|
||||
}
|
||||
}
|
||||
|
||||
protected _serializeString(): string {
|
||||
return this._htmlContent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2021",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2015"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"@xterm/addon-serialize": [
|
||||
"../typings/addon-serialize.d.ts"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/browser"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,624 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, writeSync, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
const writeRawSync = (page: any, str: string): Promise<void> => writeSync(page, `' +` + JSON.stringify(str) + `+ '`);
|
||||
|
||||
const testNormalScreenEqual = async (page: any, str: string): Promise<void> => {
|
||||
await writeRawSync(page, str);
|
||||
const originalBuffer = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
const result = await page.evaluate(`serializeAddon.serialize();`) as string;
|
||||
await page.evaluate(`term.reset();`);
|
||||
await writeRawSync(page, result);
|
||||
const newBuffer = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
// chai decides -0 and 0 are different number...
|
||||
// and firefox have a bug that output -0 for unknown reason
|
||||
assert.equal(JSON.stringify(originalBuffer), JSON.stringify(newBuffer));
|
||||
};
|
||||
|
||||
async function testSerializeEquals(writeContent: string, expectedSerialized: string): Promise<void> {
|
||||
await writeRawSync(page, writeContent);
|
||||
const result = await page.evaluate(`serializeAddon.serialize();`) as string;
|
||||
assert.strictEqual(result, expectedSerialized);
|
||||
}
|
||||
|
||||
describe('SerializeAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.goto(APP);
|
||||
await openTerminal(page, { rows: 10, cols: 10 });
|
||||
await page.evaluate(`
|
||||
window.serializeAddon = new SerializeAddon();
|
||||
window.term.loadAddon(window.serializeAddon);
|
||||
window.inspectBuffer = (buffer) => {
|
||||
const lines = [];
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
// Do this intentionally to get content of underlining source
|
||||
const bufferLine = buffer.getLine(i)._line;
|
||||
lines.push(JSON.stringify(bufferLine));
|
||||
}
|
||||
return {
|
||||
x: buffer.cursorX,
|
||||
y: buffer.cursorY,
|
||||
data: lines
|
||||
};
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
after(async () => await browser.close());
|
||||
beforeEach(async () => await page.evaluate(`window.term.reset()`));
|
||||
|
||||
it('produce different output when we call test util with different text', async function(): Promise<any> {
|
||||
await writeRawSync(page, '12345');
|
||||
const buffer1 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
await page.evaluate(`term.reset();`);
|
||||
await writeRawSync(page, '67890');
|
||||
const buffer2 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
assert.throw(() => {
|
||||
assert.equal(JSON.stringify(buffer1), JSON.stringify(buffer2));
|
||||
});
|
||||
});
|
||||
|
||||
it('produce different output when we call test util with different line wrap', async function(): Promise<any> {
|
||||
await writeRawSync(page, '1234567890\r\n12345');
|
||||
const buffer3 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
await page.evaluate(`term.reset();`);
|
||||
await writeRawSync(page, '123456789012345');
|
||||
const buffer4 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
assert.throw(() => {
|
||||
assert.equal(JSON.stringify(buffer3), JSON.stringify(buffer4));
|
||||
});
|
||||
});
|
||||
|
||||
it('empty content', async function(): Promise<any> {
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), '');
|
||||
});
|
||||
|
||||
it('unwrap wrapped line', async function(): Promise<any> {
|
||||
const lines = ['123456789123456789'];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('does not unwrap non-wrapped line', async function(): Promise<any> {
|
||||
const lines = [
|
||||
'123456789',
|
||||
'123456789'
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
|
||||
it('preserve last empty lines', async function(): Promise<any> {
|
||||
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.join('\r\n'));
|
||||
});
|
||||
|
||||
it('digits content', async function(): Promise<any> {
|
||||
const rows = 10;
|
||||
const cols = 10;
|
||||
const digitsLine = digitsString(cols);
|
||||
const lines = newArray<string>(digitsLine, rows);
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize with half of scrollback', async function(): Promise<any> {
|
||||
const rows = 20;
|
||||
const scrollback = rows - 10;
|
||||
const halfScrollback = scrollback / 2;
|
||||
const cols = 10;
|
||||
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: ${halfScrollback} });`), lines.slice(halfScrollback, rows).join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize 0 rows of scrollback', async function(): Promise<any> {
|
||||
const rows = 20;
|
||||
const cols = 10;
|
||||
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: 0 });`), lines.slice(rows - 10, rows).join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize exclude modes', async () => {
|
||||
await writeSync(page, 'before\\x1b[?1hafter');
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), 'beforeafter\x1b[?1h');
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize({ excludeModes: true });`), 'beforeafter');
|
||||
});
|
||||
|
||||
it('serialize exclude alt buffer', async () => {
|
||||
await writeSync(page, 'normal\\x1b[?1049h\\x1b[Halt');
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), 'normal\x1b[?1049h\x1b[Halt');
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize({ excludeAltBuffer: true });`), 'normal');
|
||||
});
|
||||
|
||||
it('serialize all rows of content with color16', async function(): Promise<any> {
|
||||
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<string>(
|
||||
(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<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(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
|
||||
sgr(INVERSE) + line,
|
||||
sgr(BOLD) + line,
|
||||
sgr(UNDERLINED) + line,
|
||||
sgr(BLINK) + line,
|
||||
sgr(INVISIBLE) + line,
|
||||
sgr(STRIKETHROUGH) + line,
|
||||
sgr(NO_INVERSE) + line,
|
||||
sgr(NO_BOLD) + line,
|
||||
sgr(NO_UNDERLINED) + line,
|
||||
sgr(NO_BLINK) + line,
|
||||
sgr(NO_INVISIBLE) + line,
|
||||
sgr(NO_STRIKETHROUGH) + line
|
||||
];
|
||||
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<any> {
|
||||
const rows = 32;
|
||||
const cols = 10;
|
||||
const lines = newArray<string>(
|
||||
(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 overline', async () => {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(OVERLINED) + line, // Overlined
|
||||
sgr(UNDERLINED) + line, // Overlined, Underlined
|
||||
sgr(NORMAL) + line // 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 separately', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(FG_P16_RED) + line, // fg Red,
|
||||
sgr(UNDERLINED) + line, // fg Red, Underlined
|
||||
sgr(FG_P16_GREEN) + line, // fg Green, Underlined
|
||||
sgr(INVERSE) + line, // fg Green, Underlined, Inverse
|
||||
sgr(NO_INVERSE) + line, // fg Green, Underlined
|
||||
sgr(INVERSE) + line, // fg Green, Underlined, Inverse
|
||||
sgr(BG_P16_YELLOW) + line, // fg Green, bg Yellow, Underlined, Inverse
|
||||
sgr(FG_RESET) + line, // bg Yellow, Underlined, Inverse
|
||||
sgr(BG_RESET) + line, // Underlined, Inverse
|
||||
sgr(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<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(FG_P16_RED) + line, // fg Red
|
||||
sgr(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow
|
||||
sgr(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic
|
||||
sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow
|
||||
sgr(FG_RESET, ITALIC) + line, // bg Yellow, Italic
|
||||
sgr(BG_RESET) + line, // Italic
|
||||
sgr(NORMAL) + line, // Back to normal
|
||||
sgr(FG_P16_RED) + line, // fg Red
|
||||
sgr(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow
|
||||
sgr(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic
|
||||
sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow
|
||||
sgr(FG_RESET, ITALIC) + line, // bg Yellow, Italic
|
||||
sgr(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<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(FG_P256_RED) + line, // fg Red 256,
|
||||
sgr(UNDERLINED) + line, // fg Red 256, Underlined
|
||||
sgr(FG_P256_GREEN) + line, // fg Green 256, Underlined
|
||||
sgr(INVERSE) + line, // fg Green 256, Underlined, Inverse
|
||||
sgr(NO_INVERSE) + line, // fg Green 256, Underlined
|
||||
sgr(INVERSE) + line, // fg Green 256, Underlined, Inverse
|
||||
sgr(BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256, Underlined, Inverse
|
||||
sgr(FG_RESET) + line, // bg Yellow 256, Underlined, Inverse
|
||||
sgr(BG_RESET) + line, // Underlined, Inverse
|
||||
sgr(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<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(FG_P256_RED) + line, // fg Red 256
|
||||
sgr(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256
|
||||
sgr(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic
|
||||
sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256
|
||||
sgr(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic
|
||||
sgr(BG_RESET) + line, // Italic
|
||||
sgr(NORMAL) + line, // Back to normal
|
||||
sgr(FG_P256_RED) + line, // fg Red 256
|
||||
sgr(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256
|
||||
sgr(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic
|
||||
sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256
|
||||
sgr(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic
|
||||
sgr(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<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(FG_RGB_RED) + line, // fg Red RGB,
|
||||
sgr(UNDERLINED) + line, // fg Red RGB, Underlined
|
||||
sgr(FG_RGB_GREEN) + line, // fg Green RGB, Underlined
|
||||
sgr(INVERSE) + line, // fg Green RGB, Underlined, Inverse
|
||||
sgr(NO_INVERSE) + line, // fg Green RGB, Underlined
|
||||
sgr(INVERSE) + line, // fg Green RGB, Underlined, Inverse
|
||||
sgr(BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB, Underlined, Inverse
|
||||
sgr(FG_RESET) + line, // bg Yellow RGB, Underlined, Inverse
|
||||
sgr(BG_RESET) + line, // Underlined, Inverse
|
||||
sgr(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<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(FG_RGB_RED) + line, // fg Red RGB
|
||||
sgr(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB
|
||||
sgr(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic
|
||||
sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB
|
||||
sgr(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic
|
||||
sgr(BG_RESET) + line, // Italic
|
||||
sgr(NORMAL) + line, // Back to normal
|
||||
sgr(FG_RGB_RED) + line, // fg Red RGB
|
||||
sgr(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB
|
||||
sgr(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic
|
||||
sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB
|
||||
sgr(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic
|
||||
sgr(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'));
|
||||
});
|
||||
|
||||
it('serialize CJK correctly', async () => {
|
||||
const lines = [
|
||||
'中文中文',
|
||||
'12中文',
|
||||
'中文12',
|
||||
// This line is going to be wrapped at last character
|
||||
// because it has line length of 11 (1+2*5).
|
||||
// We concat it back without the null cell currently.
|
||||
// But this may be incorrect.
|
||||
// see also #3097
|
||||
'1中文中文中'
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize CJK Mixed with tab correctly', async () => {
|
||||
const lines = [
|
||||
'中文\t12' // CJK mixed with tab
|
||||
];
|
||||
const expected = [
|
||||
'中文\x1b[4C12'
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize with alt screen correctly', async () => {
|
||||
const SMCUP = '\u001b[?1049h';
|
||||
const CUP = '\u001b[H';
|
||||
|
||||
const lines = [
|
||||
`1${SMCUP}${CUP}2`
|
||||
];
|
||||
const expected = [
|
||||
`1${SMCUP}${CUP}2`
|
||||
];
|
||||
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'alternate');
|
||||
assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n')));
|
||||
});
|
||||
|
||||
it('serialize without alt screen correctly', async () => {
|
||||
const SMCUP = '\u001b[?1049h';
|
||||
const RMCUP = '\u001b[?1049l';
|
||||
|
||||
const lines = [
|
||||
`1${SMCUP}2${RMCUP}`
|
||||
];
|
||||
const expected = [
|
||||
`1`
|
||||
];
|
||||
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'normal');
|
||||
assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n')));
|
||||
});
|
||||
|
||||
it('serialize with background', async () => {
|
||||
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||
|
||||
const lines = [
|
||||
`1\u001b[44m${CLEAR_RIGHT(5)}`,
|
||||
`2${CLEAR_RIGHT(9)}`
|
||||
];
|
||||
|
||||
await testNormalScreenEqual(page, lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('cause the BCE on scroll', async () => {
|
||||
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||
|
||||
const padLines = newArray<string>(
|
||||
(index: number) => digitsString(10, index),
|
||||
10
|
||||
);
|
||||
|
||||
const lines = [
|
||||
...padLines,
|
||||
`\u001b[44m${CLEAR_RIGHT(5)}1111111111111111`
|
||||
];
|
||||
|
||||
await testNormalScreenEqual(page, lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('handle invalid wrap before scroll', async () => {
|
||||
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||
const MOVE_UP = (l: number): string => `\u001b[${l}A`;
|
||||
const MOVE_DOWN = (l: number): string => `\u001b[${l}B`;
|
||||
const MOVE_LEFT = (l: number): string => `\u001b[${l}D`;
|
||||
|
||||
// A line wrap happened after current line.
|
||||
// But there is no content.
|
||||
// so wrap shouldn't even be able to happen.
|
||||
const segments = [
|
||||
`123456789012345`,
|
||||
MOVE_UP(1),
|
||||
CLEAR_RIGHT(5),
|
||||
MOVE_DOWN(1),
|
||||
MOVE_LEFT(5),
|
||||
CLEAR_RIGHT(5),
|
||||
MOVE_UP(1),
|
||||
'1'
|
||||
];
|
||||
|
||||
await testNormalScreenEqual(page, segments.join(''));
|
||||
});
|
||||
|
||||
it('handle invalid wrap after scroll', async () => {
|
||||
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||
const MOVE_UP = (l: number): string => `\u001b[${l}A`;
|
||||
const MOVE_DOWN = (l: number): string => `\u001b[${l}B`;
|
||||
const MOVE_LEFT = (l: number): string => `\u001b[${l}D`;
|
||||
|
||||
const padLines = newArray<string>(
|
||||
(index: number) => digitsString(10, index),
|
||||
10
|
||||
);
|
||||
|
||||
// A line wrap happened after current line.
|
||||
// But there is no content.
|
||||
// so wrap shouldn't even be able to happen.
|
||||
const lines = [
|
||||
padLines.join('\r\n'),
|
||||
'\r\n',
|
||||
`123456789012345`,
|
||||
MOVE_UP(1),
|
||||
CLEAR_RIGHT(5),
|
||||
MOVE_DOWN(1),
|
||||
MOVE_LEFT(5),
|
||||
CLEAR_RIGHT(5),
|
||||
MOVE_UP(1),
|
||||
'1'
|
||||
];
|
||||
|
||||
await testNormalScreenEqual(page, lines.join(''));
|
||||
});
|
||||
|
||||
describe('handle modes', () => {
|
||||
it('applicationCursorKeysMode', async () => {
|
||||
await testSerializeEquals('test\u001b[?1h', 'test\u001b[?1h');
|
||||
await testSerializeEquals('\u001b[?1l', 'test');
|
||||
});
|
||||
it('applicationKeypadMode', async () => {
|
||||
await testSerializeEquals('test\u001b[?66h', 'test\u001b[?66h');
|
||||
await testSerializeEquals('\u001b[?66l', 'test');
|
||||
});
|
||||
it('bracketedPasteMode', async () => {
|
||||
await testSerializeEquals('test\u001b[?2004h', 'test\u001b[?2004h');
|
||||
await testSerializeEquals('\u001b[?2004l', 'test');
|
||||
});
|
||||
it('insertMode', async () => {
|
||||
await testSerializeEquals('test\u001b[4h', 'test\u001b[4h');
|
||||
await testSerializeEquals('\u001b[4l', 'test');
|
||||
});
|
||||
it('mouseTrackingMode', async () => {
|
||||
await testSerializeEquals('test\u001b[?9h', 'test\u001b[?9h');
|
||||
await testSerializeEquals('\u001b[?9l', 'test');
|
||||
await testSerializeEquals('\u001b[?1000h', 'test\u001b[?1000h');
|
||||
await testSerializeEquals('\u001b[?1000l', 'test');
|
||||
await testSerializeEquals('\u001b[?1002h', 'test\u001b[?1002h');
|
||||
await testSerializeEquals('\u001b[?1002l', 'test');
|
||||
await testSerializeEquals('\u001b[?1003h', 'test\u001b[?1003h');
|
||||
await testSerializeEquals('\u001b[?1003l', 'test');
|
||||
});
|
||||
it('originMode', async () => {
|
||||
// origin mode moves cursor to (0,0)
|
||||
await testSerializeEquals('test\u001b[?6h', 'test\u001b[4D\u001b[?6h');
|
||||
await testSerializeEquals('\u001b[?6l', 'test\u001b[4D');
|
||||
});
|
||||
it('reverseWraparoundMode', async () => {
|
||||
await testSerializeEquals('test\u001b[?45h', 'test\u001b[?45h');
|
||||
await testSerializeEquals('\u001b[?45l', 'test');
|
||||
});
|
||||
it('sendFocusMode', async () => {
|
||||
await testSerializeEquals('test\u001b[?1004h', 'test\u001b[?1004h');
|
||||
await testSerializeEquals('\u001b[?1004l', 'test');
|
||||
});
|
||||
it('wraparoundMode', async () => {
|
||||
await testSerializeEquals('test\u001b[?7l', 'test\u001b[?7l');
|
||||
await testSerializeEquals('\u001b[?7h', 'test');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function newArray<T>(initial: T | ((index: number) => T), count: number): T[] {
|
||||
const array: T[] = new Array<T>(count);
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (typeof initial === 'function') {
|
||||
array[i] = (initial as (index: number) => T)(i);
|
||||
} else {
|
||||
array[i] = initial as T;
|
||||
}
|
||||
}
|
||||
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 sgr(...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 BOLD = '1';
|
||||
const DIM = '2';
|
||||
const ITALIC = '3';
|
||||
const UNDERLINED = '4';
|
||||
const BLINK = '5';
|
||||
const INVERSE = '7';
|
||||
const INVISIBLE = '8';
|
||||
const STRIKETHROUGH = '9';
|
||||
const OVERLINED = '53';
|
||||
|
||||
const NO_BOLD = '22';
|
||||
const NO_DIM = '22';
|
||||
const NO_ITALIC = '23';
|
||||
const NO_UNDERLINED = '24';
|
||||
const NO_BLINK = '25';
|
||||
const NO_INVERSE = '27';
|
||||
const NO_INVISIBLE = '28';
|
||||
const NO_STRIKETHROUGH = '29';
|
||||
const NO_OVERLINED = '55';
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2021",
|
||||
"lib": [
|
||||
"es2015"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../node_modules/@types/node",
|
||||
"../../../out-test/api/TestUtils"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/common"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "./src" },
|
||||
{ "path": "./test" },
|
||||
{ "path": "./benchmark" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon } from '@xterm/xterm';
|
||||
|
||||
declare module '@xterm/addon-serialize' {
|
||||
/**
|
||||
* An xterm.js addon that enables serialization of terminal contents.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* It's recommended that you write the serialized data into a terminal of the same size in which
|
||||
* it originated from and then resize it after if needed.
|
||||
*
|
||||
* @param options Custom options to allow control over what gets serialized.
|
||||
*/
|
||||
public serialize(options?: ISerializeOptions): string;
|
||||
|
||||
/**
|
||||
* Serializes terminal content as HTML, which can be written to the clipboard using the
|
||||
* `text/html` mimetype. For applications that support it, the pasted text should then retain
|
||||
* its colors/styles.
|
||||
*
|
||||
* @param options Custom options to allow control over what gets serialized.
|
||||
*/
|
||||
public serializeAsHTML(options?: Partial<IHTMLSerializeOptions>): string;
|
||||
|
||||
/**
|
||||
* Disposes the addon.
|
||||
*/
|
||||
public dispose(): void;
|
||||
}
|
||||
|
||||
export interface ISerializeOptions {
|
||||
/**
|
||||
* The number of rows in the scrollback buffer to serialize, starting from the bottom of the
|
||||
* scrollback buffer. When not specified, all available rows in the scrollback buffer will be
|
||||
* serialized.
|
||||
*/
|
||||
scrollback?: number;
|
||||
|
||||
/**
|
||||
* Whether to exclude the terminal modes from the serialization. False by default.
|
||||
*/
|
||||
excludeModes?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to exclude the alt buffer from the serialization. False by default.
|
||||
*/
|
||||
excludeAltBuffer?: boolean;
|
||||
}
|
||||
|
||||
export interface IHTMLSerializeOptions {
|
||||
/**
|
||||
* The number of rows in the scrollback buffer to serialize, starting from the bottom of the
|
||||
* scrollback buffer. When not specified, all available rows in the scrollback buffer will be
|
||||
* serialized. This setting is ignored if {@link IHTMLSerializeOptions.onlySelection} is true.
|
||||
*/
|
||||
scrollback: number;
|
||||
|
||||
/**
|
||||
* Whether to only serialize the selection. If false, the whole active buffer is serialized in HTML.
|
||||
* False by default.
|
||||
*/
|
||||
onlySelection: boolean;
|
||||
|
||||
/**
|
||||
* Whether to include the global background of the terminal. False by default.
|
||||
*/
|
||||
includeGlobalBackground: boolean;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const addonName = 'SerializeAddon';
|
||||
const mainFile = '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/
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
modules: ['./node_modules'],
|
||||
extensions: [ '.js' ],
|
||||
alias: {
|
||||
common: path.resolve('../../out/common'),
|
||||
browser: path.resolve('../../out/browser')
|
||||
}
|
||||
},
|
||||
output: {
|
||||
filename: mainFile,
|
||||
path: path.resolve('./lib'),
|
||||
library: addonName,
|
||||
libraryTarget: 'umd',
|
||||
globalObject: 'this'
|
||||
},
|
||||
mode: 'production'
|
||||
};
|
||||
Reference in New Issue
Block a user