diff --git a/.eslintrc.json b/.eslintrc.json index 822ee4bad6..3b4cd13489 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -24,6 +24,9 @@ "addons/xterm-addon-serialize/src/tsconfig.json", "addons/xterm-addon-serialize/test/tsconfig.json", "addons/xterm-addon-serialize/benchmark/tsconfig.json", + "addons/xterm-addon-serialize2/src/tsconfig.json", + "addons/xterm-addon-serialize2/test/tsconfig.json", + "addons/xterm-addon-serialize2/benchmark/tsconfig.json", "addons/xterm-addon-unicode11/src/tsconfig.json", "addons/xterm-addon-unicode11/test/tsconfig.json", "addons/xterm-addon-web-links/src/tsconfig.json", diff --git a/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts b/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts index 87741b6367..65f12895ed 100644 --- a/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts +++ b/addons/xterm-addon-serialize/benchmark/SerializeAddon.benchmark.ts @@ -53,10 +53,10 @@ perfContext('Terminal: sh -c "dd if=/dev/urandom count=40 bs=1k | hexdump | lolc perfContext('serialize', () => { let terminal: TestTerminal; const serializeAddon = new SerializeAddon(); - before(() => { + before(async () => { terminal = new TestTerminal({ cols: 80, rows: 25, scrollback: 5000 }); serializeAddon.activate(terminal); - terminal.writeSync(content); + await new Promise(r => terminal.write(content, r)); }); new ThroughputRuntimeCase('', () => { return { payloadSize: serializeAddon.serialize().length }; diff --git a/addons/xterm-addon-serialize2/.gitignore b/addons/xterm-addon-serialize2/.gitignore new file mode 100644 index 0000000000..10e27d6b85 --- /dev/null +++ b/addons/xterm-addon-serialize2/.gitignore @@ -0,0 +1,10 @@ +lib +node_modules +out-benchmark + +# skip inwasm folders +inwasm-sdks/ +inwasm-builds/ +!inwasm-builds/**/final.wat +!inwasm-builds/**/final.wasm +!inwasm-builds/**/definition.json diff --git a/addons/xterm-addon-serialize2/.npmignore b/addons/xterm-addon-serialize2/.npmignore new file mode 100644 index 0000000000..b203232aff --- /dev/null +++ b/addons/xterm-addon-serialize2/.npmignore @@ -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 diff --git a/addons/xterm-addon-serialize2/README.md b/addons/xterm-addon-serialize2/README.md new file mode 100644 index 0000000000..12e0ac8761 --- /dev/null +++ b/addons/xterm-addon-serialize2/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-serialize2/benchmark/Serialize2Addon.benchmark.ts b/addons/xterm-addon-serialize2/benchmark/Serialize2Addon.benchmark.ts new file mode 100644 index 0000000000..b4121fa96d --- /dev/null +++ b/addons/xterm-addon-serialize2/benchmark/Serialize2Addon.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 'browser/public/Terminal'; +import { Serialize2Addon } from 'Serialize2Addon'; + +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.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 Serialize2Addon(); + before(async () => { + terminal = new TestTerminal({ cols: 80, rows: 25, scrollback: 5000 }); + serializeAddon.activate(terminal); + await new Promise(r => terminal.write(content, r)); + }); + new ThroughputRuntimeCase('', () => { + return { payloadSize: serializeAddon.serialize().length }; + }, { fork: false }).showAverageThroughput(); + }); +}); diff --git a/addons/xterm-addon-serialize2/benchmark/benchmark.json b/addons/xterm-addon-serialize2/benchmark/benchmark.json new file mode 100644 index 0000000000..f8b99b5565 --- /dev/null +++ b/addons/xterm-addon-serialize2/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-serialize2/benchmark/tsconfig.json b/addons/xterm-addon-serialize2/benchmark/tsconfig.json new file mode 100644 index 0000000000..a4aba8e6b7 --- /dev/null +++ b/addons/xterm-addon-serialize2/benchmark/tsconfig.json @@ -0,0 +1,23 @@ +{ + "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/*"], + "Serialize2Addon": ["../src/Serialize2Addon"] + } + }, + "include": ["../**/*", "../../../typings/xterm.d.ts"], + "exclude": ["../../../**/*test.ts", "../../**/*api.ts"], + "references": [ + { "path": "../../../src/common" }, + { "path": "../../../src/browser" } + ] +} diff --git a/addons/xterm-addon-serialize2/inwasm-builds/out/serializer.wasm.js/serialize/final.wasm b/addons/xterm-addon-serialize2/inwasm-builds/out/serializer.wasm.js/serialize/final.wasm new file mode 100644 index 0000000000..b4a5372099 Binary files /dev/null and b/addons/xterm-addon-serialize2/inwasm-builds/out/serializer.wasm.js/serialize/final.wasm differ diff --git a/addons/xterm-addon-serialize2/package.json b/addons/xterm-addon-serialize2/package.json new file mode 100644 index 0000000000..6582441c0a --- /dev/null +++ b/addons/xterm-addon-serialize2/package.json @@ -0,0 +1,32 @@ +{ + "name": "xterm-addon-serialize2", + "version": "0.1.0", + "author": { + "name": "The xterm.js authors", + "url": "https://xtermjs.org/" + }, + "main": "lib/xterm-addon-serialize2.js", + "types": "typings/xterm-addon-serialize2.d.ts", + "repository": "https://github.com/xtermjs/xterm.js", + "license": "MIT", + "keywords": [ + "terminal", + "xterm", + "xterm.js" + ], + "scripts": { + "inwasm": "inwasm out/*.wasm.js", + "prepackage": "../../node_modules/.bin/tsc -p . && inwasm -f out/*.wasm.js", + "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": "^5.0.0" + }, + "devDependencies": { + "inwasm": "^0.0.13" + } +} diff --git a/addons/xterm-addon-serialize2/src-wasm/serialize.c b/addons/xterm-addon-serialize2/src-wasm/serialize.c new file mode 100644 index 0000000000..c54f2c0611 --- /dev/null +++ b/addons/xterm-addon-serialize2/src-wasm/serialize.c @@ -0,0 +1,445 @@ +/** + * @file serialize.c + * @brief Wasm terminal line serializer + * @version 0.1 + * @copyright Copyright (c) 2023 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * TODO: proper memory layout + * + * memory layout: + * 0 - 16: global state variables size: 4 * int32 + * 16 - 256: free + * 256 - 656: LUT100 size: 100 * int32 + * 656 - 1024: free + * 1024 - ????: extended attribs + urlId size: 2 * int32 * cols = 8 * cols + * ???? - ????: line data (src) size: 3 * int32 * cols = 12 * cols + * ???? - ????: dst + */ + + +#ifndef TS_OVERRIDE + /** + * Note on the defines here: + * Simply copied over from TS sources and unmaintained here. + * The defines are still in place here just to make the editor happy. + * + * They get overloaded with real values imported on TS side. + */ + + #define CODEPOINT_MASK 0x1FFFFF + #define IS_COMBINED_MASK 0x200000 + #define HAS_CONTENT_MASK 0x3FFFFF + #define WIDTH_MASK 0xC00000 + #define WIDTH_SHIFT 22 + + /* bit 1..8 blue in RGB, color in P256 and P16 */ + #define BLUE_MASK 0xFF + #define BLUE_SHIFT 0 + #define PCOLOR_MASK 0xFF + #define PCOLOR_SHIFT 0 + + /* bit 9..16 green in RGB */ + #define GREEN_MASK 0xFF00 + #define GREEN_SHIFT 8 + + /* bit 17..24 red in RGB */ + #define RED_MASK 0xFF0000 + #define RED_SHIFT 16 + + /* bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3) */ + #define CM_MASK 0x3000000 + #define CM_DEFAULT 0 + #define CM_P16 0x1000000 + #define CM_P256 0x2000000 + #define CM_RGB 0x3000000 + + /* bit 1..24 RGB room */ + #define RGB_MASK 0xFFFFFF + #define COLOR_MASK 0x3FFFFFF /* == CM_MASK | RGB_MASK */ + + /* fg flags: bit 27..32 */ + #define INVERSE 0x4000000 + #define BOLD 0x8000000 + #define UNDERLINE 0x10000000 + #define BLINK 0x20000000 + #define INVISIBLE 0x40000000 + #define STRIKETHROUGH 0x80000000 + + /* bg flags: bit 27..32 (upper 2 unused) */ + #define ITALIC 0x4000000 + #define DIM 0x8000000 + #define HAS_EXTENDED 0x10000000 + #define PROTECTED 0x20000000 + + /* ext flags: bit 27..32 (upper 3 unused) */ + #define UNDERLINE_STYLE 0x1C000000 + + /* underline style */ + #define UL_NONE 0 + #define UL_SINGLE 1 + #define UL_DOUBLE 2 + #define UL_CURLY 3 + #define UL_DOTTED 4 + #define UL_DASHED 5 + + + /* memory locations */ + #define P_LUT100 256 + #define P_EXT 16384 + +#endif /* TS_OVERRIDE */ + + + +/** + * Imported functions from JS side. + */ + +/** + * Write combined string data for a single cell on JS side. + * The callback should write all codepoints of the combined string + * for cell `x` beginning at `dst` and return the new write position. + */ +__attribute__((import_module("env"), import_name("writeCombined"))) +unsigned short* js_write_combined(unsigned short* dst, int x); + +/** + * Write URL sequence on JS side. + * The callback should write the URL sequence beginning at `dst` + * for the URL with the urlID `link` (from OscLinkService), + * and return the new write position. + */ +__attribute__((import_module("env"), import_name("writeLink"))) +unsigned short* js_write_link(unsigned short* dst, int link); + + +/** + * Cell struct as defined in Bufferline.ts + * FIXME: Enhance with bitfield unions? (might save some bit juggling further down...) + */ +typedef struct __attribute__((packed, aligned(4))) { + unsigned int content; + unsigned int fg; + unsigned int bg; +} Cell; + + +// FIXME: any nicer way to express this? +#define SGR_FLAG(V, DIFF, FLAG, HI, LO) \ +if ((DIFF) & (FLAG)) { \ + if ((V) & (FLAG)) { \ + *(unsigned int*) dst = 0x3b0000 | (HI); \ + dst += 2; \ + } else { \ + *(unsigned long long*) dst = 0x3b00000032ULL | (LO); \ + dst += 3; \ + } \ +} \ + +#define W_CSI(dst) *(unsigned int*) dst = 0x5b001b; dst += 2; + + +/** + * State to be preserve between multiple lines. + */ +unsigned int old_fg = 0; +unsigned int old_bg = 0; +unsigned int old_ul = 0; +unsigned int old_link = 0; + + +/** + * itoa implementation for unsigned short to utf16. + * + * Note: Clang compiles with the div instruction into wasm. + * Since tests with shift mul in source show no runtime difference, + * wasm engines prolly optimize the division on their own. + */ +const unsigned int * LUT100 = (unsigned int *) P_LUT100; + +__attribute__((noinline)) +unsigned short* itoa(unsigned short n, unsigned short *dst) { + if (n < 10) { + *dst++ = n + 48; + } else if (n < 100) { + *(unsigned int*) dst = LUT100[n]; + dst += 2; + } else if (n < 1000) { + int h = n / 100; + *dst++ = h + 48; + *(unsigned int*) dst = LUT100[n - h * 100]; + dst += 2; + } else if (n < 10000) { + int h = n / 100; + *(unsigned int*) dst = LUT100[h]; + *((unsigned int*) dst+1) = LUT100[n - h * 100]; + dst += 4; + } else { + int h = n / 10000; + *dst++ = h + 48; + n -= h * 10000; + h = n / 100; + *(unsigned int*) dst = LUT100[h]; + *((unsigned int*) dst+1) = LUT100[n - h * 100]; + dst += 4; + } + return dst; +} + + +/** + * Set SGR colors for FG / BG / UL. + * c denotes the color target as {FG: '3', BG: '4', UL: '5'}. + */ +__attribute__((noinline)) +static unsigned short* color(unsigned short *dst, unsigned int v, char c) { + int mode = v & CM_MASK; + if (mode == CM_DEFAULT) { + /* default is 39; | 49; | 59; */ + *(unsigned long long*) dst = 0x3b00390000ULL | c; + dst += 3; + } else if (mode == CM_P16) { + unsigned long long color = 48 + (v & 7); + if (v & 8) { + /* bright for FG | BG (no UL color here) */ + if (c == '3') { + *(unsigned long long*) dst = 0x003b00000039ULL | color << 16; + dst += 3; + } else if (c == '4') { + *(unsigned long long*) dst = 0x003b000000300031ULL | color << 32; + dst += 4; + } + } else { + /* handles normal FG | BG | UL */ + *(unsigned long long*) dst = 0x3b00000000ULL | color << 16 | c; + dst += 3; + } + } else if (mode == CM_P256) { + /* 256 indexed written in ; notation */ + *dst++ = c; + *(unsigned long long*) dst = 0x3b0035003b0038ULL; + dst += 4; + dst = itoa(v & 0xFF, dst); + *dst++ = ';'; + } else { + /* RGB written in ; notation */ + *dst++ = c; + *(unsigned long long*) dst = 0x3b0032003b0038ULL; + dst += 4; + dst = itoa((v >> 16) & 0xFF, dst); + *dst++ = ';'; + dst = itoa((v >> 8) & 0xFF, dst); + *dst++ = ';'; + dst = itoa(v & 0xFF, dst); + *dst++ = ';'; + } + return dst; +} + +/** + * Write SGR sequence into `dst` from FG, BG and UL diffs. + */ +unsigned short* sgr( + unsigned short* dst, + unsigned int fg, + unsigned int bg, + unsigned int diff_fg, + unsigned int diff_bg, + unsigned int ul, + unsigned int diff_ul +) { + W_CSI(dst) + + if (!fg && !bg) { + /* SGR 0 */ + *dst++ = ';'; + } else { + /* fg flags */ + if (diff_fg >> 26) { + SGR_FLAG(fg, diff_fg, INVERSE, '7', '7' << 16) + SGR_FLAG(fg, diff_fg, BOLD, '1', '2' << 16) + // SGR_FLAG(fg, diff_fg, UNDERLINE, '4', '4' << 16) // commented out: covered by ext ul attribs + SGR_FLAG(fg, diff_fg, BLINK, '5', '5' << 16) + SGR_FLAG(fg, diff_fg, INVISIBLE, '8', '8' << 16) + SGR_FLAG(fg, diff_fg, STRIKETHROUGH, '9', '9' << 16) + } + /* fg color */ + if (diff_fg & COLOR_MASK) dst = color(dst, fg, '3'); + + /* bg flags */ + if (diff_bg >> 26) { + SGR_FLAG(bg, diff_bg, ITALIC, '3', '3') + SGR_FLAG(bg, diff_bg, DIM, '2', '2') + } + /* bg color */ + if (diff_bg & COLOR_MASK) dst = color(dst, bg, '4'); + + /* ul ext attributes */ + /* safety measure: check against HAS_EXTENDED in case we have spurious ext attrib values */ + if (bg & HAS_EXTENDED) { + if (diff_ul & UNDERLINE_STYLE) { + *dst++ = '4'; + *dst++ = ':'; + *dst++ = ((ul & UNDERLINE_STYLE) >> 26) + 48; + *dst++ = ';'; + } + if (diff_ul & COLOR_MASK) dst = color(dst, ul, '5'); + } + } + + /* all params above are added with final ';', overwrite last one hereby -1 */ + *(dst - 1) = 'm'; + return dst; +} + + +/** + * Exported functions. + */ + + +/** + * @brief Reset internal state for FG, BG, UL and link. + * + * Should b called at the beginning of a serialization. + * FG, BG and UL should be set to the terminal's default values (null cell), + * happens to be 0 for all. + * `link` is the urlId for a link as in the OscLinkService, 0 for unset. + */ +void reset(int fg, int bg, int ul, int link) { + old_fg = fg; + old_bg = bg; + old_ul = ul; + old_link = link; +} + + +/** + * @brief Serialize terminal bufferline data with SGR attributes, + * extended attributes and OSC8 hyperlinks. + * + * `src` is the start pointer, where Bufferline._data got copied to, + * `length` denotes the Cell-length (Bufferline.length). + * + * The function will write the serialized line data to `dst` as 2-byte UTF-16 + * without additional size check (make sure to have enough space on TS side). + */ +// FIXME: how to get rid of the weird BCE hack? +void* line(Cell *src, int length, unsigned short *dst) { + int cur_jmp = 0; + unsigned int bce = old_bg; + unsigned ul = old_ul; + unsigned link = old_link; + + for (int i = 0; i < length;) { + Cell cell = src[i]; + + /** + * apply SGR differences + * We have to nullify HAS_EXTENDED due to its overloaded meaning, + * otherwise we would introduce nonsense jump/erase sequences here. + * SGR ext attributes for UL are covered by the explicit comparison, + * URL/hyperlink entry needs a separate control path (TODO). + */ + unsigned bg = cell.bg & ~HAS_EXTENDED; + ul = *((unsigned int *) P_EXT + i); + if (cell.fg != old_fg || bg != old_bg || ul != old_ul) { + if (cur_jmp) { + /** + * We are in the middle of jumped over cells, + * thus still need to apply BG changes first. + */ + if (old_bg != bce) { + W_CSI(dst) + dst = itoa(cur_jmp, dst); + *dst++ = 'X'; + } + W_CSI(dst) + dst = itoa(cur_jmp, dst); + *dst++ = 'C'; + cur_jmp = 0; + } + /* write new SGR sequence, advance fg/bg/ul colors */ + dst = sgr(dst, cell.fg, cell.bg, cell.fg ^ old_fg, cell.bg ^ old_bg, ul, ul ^ old_ul); + old_fg = cell.fg; + old_bg = bg; + old_ul = ul; + } + + /* OSC 8 link handling */ + link = *((unsigned int *) P_EXT + (length + i)); // FIXME: merge memory segment with UL above + if (link != old_link) { + if (old_link) { + /* close old link */ + *(unsigned long long*) dst = 0x003b0038005d001bULL; // ; 8 ] ESC + dst += 4; + *(unsigned int*) dst = 0x0007003b; // BEL ; + dst += 2; + } + /** + * safety measure: check against HAS_EXTENDED in case + * we have a spurious ext attrib object on JS side + */ + if ((cell.bg & HAS_EXTENDED) && link) { + /* compose and write url sequence on JS side */ + dst = js_write_link(dst, link); + old_link = link; + } else { + /* we have spurious ext object, explicitly nullify here */ + old_link = 0; + } + } + + /* text content handling */ + if (cell.content & HAS_CONTENT_MASK) { + if (cur_jmp) { + /** + * We are in the middle of jumped over cells, thus apply cursor jump. + * We have to check again in case there were no SGR changes before. + */ + if (old_bg != bce) { + W_CSI(dst) + dst = itoa(cur_jmp, dst); + *dst++ = 'X'; + } + W_CSI(dst) + dst = itoa(cur_jmp, dst); + *dst++ = 'C'; + cur_jmp = 0; + } + if (cell.content & IS_COMBINED_MASK) { + /* combined chars are written from JS */ + dst = js_write_combined(dst, i); + } else { + /* utf32 to utf16 conversion */ + unsigned int cp = cell.content & 0x1FFFFF; + if (cp > 0xFFFF) { + cp -= 0x10000; + *dst++ = (cp >> 10) + 0xD800; + *dst++ = (cp % 0x400) + 0xDC00; + } else { + *dst++ = cp; + } + } + } else { + /* empty cells are treated by cursor jumps */ + cur_jmp++; + } + + /* advance cell read position by wcwidth or 1 */ + int width = cell.content >> WIDTH_SHIFT; + i += width ? width : 1; + } + + /* clear cells if we have jumped over cells and bce color != current bg */ + if (cur_jmp && old_bg != bce) { + W_CSI(dst) + dst = itoa(cur_jmp, dst); + *dst++ = 'X'; + } + + return dst; +} diff --git a/addons/xterm-addon-serialize2/src/Serialize2Addon.test.ts b/addons/xterm-addon-serialize2/src/Serialize2Addon.test.ts new file mode 100644 index 0000000000..9428aee39e --- /dev/null +++ b/addons/xterm-addon-serialize2/src/Serialize2Addon.test.ts @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import jsdom = require('jsdom'); +import { assert } from 'chai'; +import { Serialize2Addon } from './Serialize2Addon'; +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 { + 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('xterm-addon-serialize2', () => { + let dom: jsdom.JSDOM; + let window: jsdom.DOMWindow; + + let serializeAddon: Serialize2Addon; + let terminal: Terminal; + + before(() => { + serializeAddon = new Serialize2Addon(); + }); + + 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(new OptionsService({})); + (terminal as any)._core._selectionService = new TestSelectionService((terminal as any)._core._bufferService); + }); + + describe('text', () => { + // TODO: wirte alot more test cases here... + it('restoring cursor styles', async () => { + await writeP(terminal, sgr('32') + '> ' + sgr('0')); + assert.equal(serializeAddon.serialize(), '\u001b[32m> \u001b[m'); + }); + }); +}); diff --git a/addons/xterm-addon-serialize2/src/Serialize2Addon.ts b/addons/xterm-addon-serialize2/src/Serialize2Addon.ts new file mode 100644 index 0000000000..cbf34e75fa --- /dev/null +++ b/addons/xterm-addon-serialize2/src/Serialize2Addon.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2023 The xterm.js authors. All rights reserved. + * @license MIT + * + * (EXPERIMENTAL) This Addon is still under development + */ + +import { serialize } from './serializer.wasm'; +import { Terminal, ITerminalAddon } from 'xterm'; + + +interface ISerializeOptions { + scrollback?: number; + excludeModes?: boolean; + excludeAltBuffer?: boolean; +} + + +export class Serialize2Addon implements ITerminalAddon { + private _terminal: Terminal | undefined; + + constructor() { } + + public activate(terminal: Terminal): void { + this._terminal = terminal; + } + + public serialize(options?: ISerializeOptions): string { + // TODO: Add combinedData support + if (!this._terminal) { + throw new Error('Cannot use addon until it has been loaded'); + } + + return serialize((this._terminal as any)._core); + } + + public dispose(): void { } +} + diff --git a/addons/xterm-addon-serialize2/src/serializer.wasm.ts b/addons/xterm-addon-serialize2/src/serializer.wasm.ts new file mode 100644 index 0000000000..3cf1c07290 --- /dev/null +++ b/addons/xterm-addon-serialize2/src/serializer.wasm.ts @@ -0,0 +1,214 @@ + +import { InWasm, OutputMode, OutputType } from 'inwasm'; +import { ITerminal } from 'browser/Types'; +import { IBufferLine, IExtendedAttrs } from 'common/Types'; +import { Attributes, BgFlags, Content, ExtFlags, FgFlags, UnderlineStyle } from 'common/buffer/Constants'; + +/* eslint-disable */ +interface IExtendedAttrsInternal extends IExtendedAttrs { + _urlId: number; + _ext: number; +} + +interface IBufferLineInternal extends IBufferLine { + _data: Uint32Array; + _extendedAttrs: {[index: number]: IExtendedAttrsInternal | undefined}; + _combined: {[index: number]: string}; +} +/* eslint-enable */ + + +const wasmSerialize = InWasm({ + name: 'serialize', + type: OutputType.INSTANCE, + mode: OutputMode.SYNC, + srctype: 'Clang-C', + imports: { + env: { + memory: new WebAssembly.Memory({ initial: 1 }), + writeCombined: (dst: number, x: number) => 0, + writeLink: (dst: number, linkId: number) => 0 + } + }, + exports: { + line: (src: number, length: number, dst: number) => 0, + reset: (fg: number, bg: number, ul: number, link: number) => {} + }, + compile: { + switches: ['-Wl,-z,stack-size=0', '-Wl,--stack-first'] + }, + trackChanges: ['src-wasm/serialize.c'], + code: ` + #define TS_OVERRIDE + + #define CODEPOINT_MASK ${Content.CODEPOINT_MASK} + #define IS_COMBINED_MASK ${Content.IS_COMBINED_MASK} + #define HAS_CONTENT_MASK ${Content.HAS_CONTENT_MASK} + #define WIDTH_MASK ${Content.WIDTH_MASK} + #define WIDTH_SHIFT ${Content.WIDTH_SHIFT} + + /* bit 1..8 blue in RGB, color in P256 and P16 */ + #define BLUE_MASK ${Attributes.BLUE_MASK} + #define BLUE_SHIFT ${Attributes.BLUE_SHIFT} + #define PCOLOR_MASK ${Attributes.PCOLOR_MASK} + #define PCOLOR_SHIFT ${Attributes.PCOLOR_SHIFT} + + /* bit 9..16 green in RGB */ + #define GREEN_MASK ${Attributes.GREEN_MASK} + #define GREEN_SHIFT ${Attributes.GREEN_SHIFT} + + /* bit 17..24 red in RGB */ + #define RED_MASK ${Attributes.RED_MASK} + #define RED_SHIFT ${Attributes.RED_SHIFT} + + /* bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3) */ + #define CM_MASK ${Attributes.CM_MASK} + #define CM_DEFAULT ${Attributes.CM_DEFAULT} + #define CM_P16 ${Attributes.CM_P16} + #define CM_P256 ${Attributes.CM_P256} + #define CM_RGB ${Attributes.CM_RGB} + + /* bit 1..24 RGB room */ + #define RGB_MASK ${Attributes.RGB_MASK} + #define COLOR_MASK ${Attributes.CM_MASK | Attributes.RGB_MASK} + + /* fg flags: bit 27..32 */ + #define INVERSE ${FgFlags.INVERSE} + #define BOLD ${FgFlags.BOLD} + #define UNDERLINE ${FgFlags.UNDERLINE} + #define BLINK ${FgFlags.BLINK} + #define INVISIBLE ${FgFlags.INVISIBLE} + #define STRIKETHROUGH ${FgFlags.STRIKETHROUGH} + + /* bg flags: bit 27..32 (upper 2 unused) */ + #define ITALIC ${BgFlags.ITALIC} + #define DIM ${BgFlags.DIM} + #define HAS_EXTENDED ${BgFlags.HAS_EXTENDED} + #define PROTECTED ${BgFlags.PROTECTED} + + /* ext flags: bit 27..32 (upper 3 unused) */ + #define UNDERLINE_STYLE ${ExtFlags.UNDERLINE_STYLE} + + /* underline style */ + #define UL_NONE ${UnderlineStyle.NONE} + #define UL_SINGLE ${UnderlineStyle.SINGLE} + #define UL_DOUBLE ${UnderlineStyle.DOUBLE} + #define UL_CURLY ${UnderlineStyle.CURLY} + #define UL_DOTTED ${UnderlineStyle.DOTTED} + #define UL_DASHED ${UnderlineStyle.DASHED} + + + /* memory locations */ + #define P_LUT100 256 + #define P_EXT 16384 + + ${require('fs').readFileSync('src-wasm/serialize.c')} + ` +}); + +// itoa LUT +// note: performant decimal conversion of numbers is actually a hard problem +// we use a LUT approach with 0-99 precomputed in utf16 for better performance +const LUT100 = new Uint32Array(100); +for (let i1 = 0; i1 < 10; ++i1) { + for (let i2 = 0; i2 < 10; ++i2) { + LUT100[i1 * 10 + i2] = (48 + i2) << 16 | (48 + i1); + } +} + +const mem = new WebAssembly.Memory({ initial: 3000 }); +const d16 = new Uint16Array(mem.buffer); +const d32 = new Uint32Array(mem.buffer); +let writeCombinedOverload: (dst: number, x: number) => number = (dst, x) => dst; +const writeCombined: (dst: number, x: number) => number = (dst, x) => writeCombinedOverload(dst, x); +let writeLinkOverload: (dst: number, x: number) => number = (dst, x) => dst; +const writeLink: (dst: number, x: number) => number = (dst, x) => writeLinkOverload(dst, x); +const inst = wasmSerialize({ env: { memory: mem, writeCombined, writeLink } }); +d32.set(LUT100, 64); +const td = new TextDecoder('UTF-16LE'); + + +export function serialize(t: ITerminal): string { + // reset FG/BG/ext + inst.exports.reset(0, 0, 0, 0); + + const buffer = t.buffer; + const len = t.buffer.lines.length; + let wPos = 150*65536; + + let line: IBufferLineInternal; + + // single call is pretty wasteful? preload combines once instead? + writeCombinedOverload = (dst, x) => { + let dp = dst >> 1; + const comb: string = (line as any)._combined[x] || ''; + for (let i = 0; i < comb.length; ++i) { + d16[dp++] = comb.charCodeAt(i); + } + return dp << 1; + }; + + // write link to buffer + writeLinkOverload = (dst, linkId) => { + const entry = (t as any)._oscLinkService._dataByLinkId.get(linkId); + if (!entry) { + return dst; + } + const osc8 = `\x1b]8;${entry.data.id ? 'id='+entry.data.id : ''};${entry.data.uri}\x07`; + let dp = dst >> 1; + for (let i = 0; i < osc8.length; ++i) { + d16[dp++] = osc8.charCodeAt(i); + } + return dp << 1; + }; + + const ext = d32.subarray(4096, 4096 + t.cols*2); + const ext2 = ext.subarray(t.cols); + let clearExt = false; + + for (let row = 0; row < len; ++row) { + line = buffer.lines.get(row) as IBufferLineInternal; + if (!line) break; + + // insert CRLF + if (!line.isWrapped) { + const wPos16 = wPos >> 1; + d16[wPos16] = 13; + d16[wPos16+1] = 10; + wPos += 4; + } + + // TODO: start of line hook goes here... + + // load extended attributes + if (clearExt) { + ext.fill(0); + clearExt = false; + } + const keys = Object.keys(line._extendedAttrs) as unknown as number[]; + if (keys.length) { + for (let k = 0; k < keys.length; ++k) { + const rk = keys[k]; + ext[rk] = line._extendedAttrs[rk]!._ext; // UL color & style + ext2[rk] = line._extendedAttrs[rk]!._urlId; // OSC 8 link + } + clearExt = true; + } + + d32.set(line._data, 16384); + wPos = inst.exports.line(65536, t.cols, wPos); + + // TODO: end of line hook goes here... + } + const finalData = d16.subarray(75*65536, wPos >> 1); + // strip empty lines at bottom + let fdp = finalData.length - 1; + while (fdp && finalData[fdp] === 10 && finalData[fdp-1] === 13) fdp -= 2; + + // strip leading CRLF + const offset = finalData[0] === 13 && finalData[1] === 10 ? 2 : 0; + + // return as string + // TODO: compose from hook parts (needs to store wPos line offsets?) + return td.decode(d16.subarray(75*65536+offset, 75*65536+fdp+1)); +} diff --git a/addons/xterm-addon-serialize2/src/tsconfig.json b/addons/xterm-addon-serialize2/src/tsconfig.json new file mode 100644 index 0000000000..ba26f22e4a --- /dev/null +++ b/addons/xterm-addon-serialize2/src/tsconfig.json @@ -0,0 +1,39 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2015", + "lib": [ + "dom", + "es2015" + ], + "rootDir": ".", + "outDir": "../out", + "sourceMap": true, + "removeComments": true, + "baseUrl": ".", + "paths": { + "common/*": [ + "../../../src/common/*" + ], + "browser/*": [ + "../../../src/browser/*" + ] + }, + "strict": true, + "types": [ + "../../../node_modules/@types/mocha" + ] + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../../src/common" + }, + { + "path": "../../../src/browser" + } + ] +} diff --git a/addons/xterm-addon-serialize2/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize2/test/SerializeAddon.api.ts new file mode 100644 index 0000000000..157b7072fc --- /dev/null +++ b/addons/xterm-addon-serialize2/test/SerializeAddon.api.ts @@ -0,0 +1,612 @@ +/** + * 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'; + +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 => writeSync(page, `' +` + JSON.stringify(str) + `+ '`); + +const testNormalScreenEqual = async (page: any, str: string): Promise => { + 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 { + await writeRawSync(page, writeContent); + const result = await page.evaluate(`serializeAddon.serialize();`) as string; + assert.strictEqual(result, expectedSerialized); +} + +describe('SerializeAddon', () => { + before(async function(): Promise { + 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 { + 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 { + 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 { + assert.equal(await page.evaluate(`serializeAddon.serialize();`), ''); + }); + + it('unwrap wrapped line', async function(): Promise { + 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 { + 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 { + 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 { + 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 with half of scrollback', async function(): Promise { + const rows = 20; + const scrollback = rows - 10; + const halfScrollback = scrollback / 2; + 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({ scrollback: ${halfScrollback} });`), lines.slice(halfScrollback, rows).join('\r\n')); + }); + + it('serialize 0 rows of scrollback', async function(): Promise { + const rows = 20; + 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({ 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 { + 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[] = [ + 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 + ]; + 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[] = [ + 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 { + 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 { + 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 { + 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 { + 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 { + 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( + (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( + (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(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] = (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 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'; diff --git a/addons/xterm-addon-serialize2/test/tsconfig.json b/addons/xterm-addon-serialize2/test/tsconfig.json new file mode 100644 index 0000000000..971f3e92f5 --- /dev/null +++ b/addons/xterm-addon-serialize2/test/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2015", + "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" + } + ] +} diff --git a/addons/xterm-addon-serialize2/tsconfig.json b/addons/xterm-addon-serialize2/tsconfig.json new file mode 100644 index 0000000000..0e7b5c3502 --- /dev/null +++ b/addons/xterm-addon-serialize2/tsconfig.json @@ -0,0 +1,9 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "./src" }, + { "path": "./test" }, + { "path": "./benchmark" } + ] +} diff --git a/addons/xterm-addon-serialize2/typings/xterm-addon-serialize.d.ts b/addons/xterm-addon-serialize2/typings/xterm-addon-serialize.d.ts new file mode 100644 index 0000000000..4cb6283db0 --- /dev/null +++ b/addons/xterm-addon-serialize2/typings/xterm-addon-serialize.d.ts @@ -0,0 +1,88 @@ +/** + * 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 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): 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; + } +} diff --git a/addons/xterm-addon-serialize2/webpack.config.js b/addons/xterm-addon-serialize2/webpack.config.js new file mode 100644 index 0000000000..7c0ccd016a --- /dev/null +++ b/addons/xterm-addon-serialize2/webpack.config.js @@ -0,0 +1,32 @@ +/** + * 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', + globalObject: 'this' + }, + mode: 'production' +}; diff --git a/addons/xterm-addon-serialize2/yarn.lock b/addons/xterm-addon-serialize2/yarn.lock new file mode 100644 index 0000000000..73e9bfed35 --- /dev/null +++ b/addons/xterm-addon-serialize2/yarn.lock @@ -0,0 +1,324 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +acorn-walk@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + +acorn@^8.8.2: + version "8.8.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +cliui@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorette@^2.0.20: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +cross-spawn@^7.0.0: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^10.0.0: + version "10.2.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.1.tgz#44288e9186b5cd5baa848728533ba21a94aa8f33" + integrity sha512-ngom3wq2UhjdbmRE/krgkD8BQyi1KZ5l+D2dVm4+Yj+jJIBp74/ZGunL6gNGc/CYuQmvUBiavWEXIotRiv5R6A== + dependencies: + foreground-child "^3.1.0" + fs.realpath "^1.0.0" + jackspeak "^2.0.3" + minimatch "^9.0.0" + minipass "^5.0.0" + path-scurry "^1.7.0" + +inwasm@^0.0.13: + version "0.0.13" + resolved "https://registry.yarnpkg.com/inwasm/-/inwasm-0.0.13.tgz#cbbbbc566b86d876edc1a385cfae32864a4bcaab" + integrity sha512-gmULhw1wfF3tQ19y0TvcNH6A5jN7IuTD51kbZuy+ittUU59d+ZTQMb53wbGQuciMrledgagL3/ohnjUj5qJikQ== + dependencies: + acorn "^8.8.2" + acorn-walk "^8.2.0" + chokidar "^3.5.3" + colorette "^2.0.20" + glob "^10.0.0" + wabt "^1.0.32" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +jackspeak@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.0.3.tgz#672eb397b97744a265b5862d7762b96e8dad6e61" + integrity sha512-0Jud3OMUdMbrlr3PyUMKESq51LXVAB+a239Ywdvd+Kgxj3MaBRml/nVRxf8tQFyfthMjuRkxkv7Vg58pmIMfuQ== + dependencies: + cliui "^7.0.4" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +lru-cache@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.0.tgz#19efafa9d08d1c08eb8efd78876075f0b8b1b07b" + integrity sha512-qFXQEwchrZcMVen2uIDceR8Tii6kCJak5rzDStfEM0qA3YLMswaxIEZO0DhIbJ3aqaJiDjt+3crlplOb0tDtKQ== + +minimatch@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.0.tgz#bfc8e88a1c40ffd40c172ddac3decb8451503b56" + integrity sha512-0jJj8AvgKqWN05mrwuqi8QYKx1WmYSUoKSxu5Qhs9prezTz10sxAHGNZe9J9cqIJzta8DWsleh2KaVaLl6Ru2w== + dependencies: + brace-expansion "^2.0.1" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-scurry@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.7.0.tgz#99c741a2cfbce782294a39994d63748b5a24f6db" + integrity sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg== + dependencies: + lru-cache "^9.0.0" + minipass "^5.0.0" + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.1.tgz#96a61033896120ec9335d96851d902cc98f0ba2a" + integrity sha512-uUWsN4aOxJAS8KOuf3QMyFtgm1pkb6I+KRZbRF/ghdf5T7sM+B1lLLzPDxswUjkmHyxQAVzEgG35E3NzDM9GVw== + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +wabt@^1.0.32: + version "1.0.32" + resolved "https://registry.yarnpkg.com/wabt/-/wabt-1.0.32.tgz#a4611728e67f1c3f7c546fabb7bc4da3e84a67a7" + integrity sha512-1aHvkKaSrrl7qFtAbQ1RWVHLuJApRh7PtUdYvRtiUEKEhk0MOV0sTuz5cLF6jL5jPLRyifLbZcR65AEga/xBhQ== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" diff --git a/package.json b/package.json index 1423cab0ed..878fc579f1 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "prepare": "npm run setup", "setup": "npm run build", "presetup": "node ./bin/install-addons.js", + "postsetup": "cd addons/xterm-addon-serialize2 && npm run inwasm -- -S", "prepublishOnly": "npm run package", "watch": "tsc -b -w ./tsconfig.all.json --preserveWatchOutput", "benchmark": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json", diff --git a/tsconfig.all.json b/tsconfig.all.json index 4d2df3066a..cf91391fde 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -12,6 +12,7 @@ { "path": "./addons/xterm-addon-ligatures" }, { "path": "./addons/xterm-addon-search" }, { "path": "./addons/xterm-addon-serialize" }, + { "path": "./addons/xterm-addon-serialize2" }, { "path": "./addons/xterm-addon-unicode11" }, { "path": "./addons/xterm-addon-web-links" }, { "path": "./addons/xterm-addon-webgl" }