diff --git a/addons/xterm-addon-serialize/.gitignore b/addons/xterm-addon-serialize/.gitignore new file mode 100644 index 00000000..3063f07d --- /dev/null +++ b/addons/xterm-addon-serialize/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules diff --git a/addons/xterm-addon-serialize/.npmignore b/addons/xterm-addon-serialize/.npmignore new file mode 100644 index 00000000..1c794445 --- /dev/null +++ b/addons/xterm-addon-serialize/.npmignore @@ -0,0 +1,5 @@ +**/*.api.js +**/*.api.ts +tsconfig.json +.yarnrc +webpack.config.js diff --git a/addons/xterm-addon-serialize/package.json b/addons/xterm-addon-serialize/package.json new file mode 100644 index 00000000..c8a66c4b --- /dev/null +++ b/addons/xterm-addon-serialize/package.json @@ -0,0 +1,20 @@ +{ + "name": "xterm-addon-serialize", + "version": "0.1.0", + "author": { + "name": "The xterm.js authors", + "url": "https://xtermjs.org/" + }, + "main": "lib/xterm-addon-serialize.js", + "types": "typings/xterm-addon-serialize.d.ts", + "license": "MIT", + "scripts": { + "build": "../../node_modules/.bin/tsc -p src", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package" + }, + "peerDependencies": { + "xterm": "^3.14.0" + } +} diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts new file mode 100644 index 00000000..5543696b --- /dev/null +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as puppeteer from 'puppeteer'; +import * as util from 'util'; +import { assert } from 'chai'; +import { ITerminalOptions } from 'xterm'; + +const APP = 'http://127.0.0.1:3000/test'; + +let browser: puppeteer.Browser; +let page: puppeteer.Page; +const width = 800; +const height = 600; + +describe('SerializeAddon', () => { + before(async function (): Promise { + this.timeout(20000); + browser = await puppeteer.launch({ + headless: process.argv.indexOf('--headless') !== -1, + slowMo: 80, + args: [`--window-size=${width},${height}`] + }); + page = (await browser.pages())[0]; + await page.setViewport({ width, height }); + }); + + after(async () => { + await browser.close(); + }); + + beforeEach(async function (): Promise { + this.timeout(20000); + await page.goto(APP); + }); + + it('empty content', async function (): Promise { + this.timeout(20000); + const rows = 10; + const cols = 10; + const blankline = ' '.repeat(cols); + const lines = newArray(blankline, rows); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('digits content', async function (): Promise { + this.timeout(20000); + const rows = 10; + const cols = 10; + const digitsLine = digitsString(cols); + const lines = newArray(digitsLine, rows); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize n rows of content', async function (): Promise { + this.timeout(20000); + const rows = 10; + const halfRows = rows >> 1; + const cols = 10; + const lines = newArray((index: number) => digitsString(cols, index), rows); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize(${halfRows});`), lines.slice(halfRows, 2 * halfRows).join('\r\n')); + }); + + it('serialize 0 rows of content', async function (): Promise { + this.timeout(20000); + const rows = 10; + const cols = 10; + const lines = newArray((index: number) => digitsString(cols, index), rows); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `); + + assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), ''); + }); +}); + +async function openTerminal(options: ITerminalOptions = {}): Promise { + await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`); + await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + if (options.rendererType === 'dom') { + await page.waitForSelector('.xterm-rows'); + } else { + await page.waitForSelector('.xterm-text-layer'); + } +} + +function newArray(initial: T | ((index: number) => T), count: number): T[] { + const array: T[] = new Array(count); + for (let i = 0; i < array.length; i++) { + if (typeof initial === 'function') { + array[i] = (<(index: number) => T>initial)(i); + } else { + array[i] = initial; + } + } + return array; +} + +function digitsString(length: number, from: number = 0): string { + let s = ''; + for (let i = 0; i < length; i++) { + s += (from++) % 10; + } + return s; +} diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts new file mode 100644 index 00000000..9328e674 --- /dev/null +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ITerminalAddon } from 'xterm'; + +function crop(value: number, from: number, to: number): number { + return Math.max(from, Math.min(value, to)); +} + +export class SerializeAddon implements ITerminalAddon { + private _terminal: Terminal | undefined; + + constructor() { } + + public activate(terminal: Terminal): void { + this._terminal = terminal; + } + + public serialize(rows?: number): string { + // TODO: Add frontground/background color support later + if (!this._terminal) { + throw new Error('Cannot use addon until it has been loaded'); + } + const terminalRows = this._terminal.rows; + if (rows === undefined) { + rows = terminalRows; + } + rows = crop(rows, 0, terminalRows); + + const buffer = this._terminal.buffer; + const lines: string[] = new Array(rows); + + for (let i = terminalRows - rows; i < terminalRows; i++) { + const line = buffer.getLine(i); + lines[i - terminalRows + rows] = line ? line.translateToString() : ''; + } + + return lines.join('\r\n'); + } + + public dispose(): void { } +} diff --git a/addons/xterm-addon-serialize/src/tsconfig.json b/addons/xterm-addon-serialize/src/tsconfig.json new file mode 100644 index 00000000..5539aa56 --- /dev/null +++ b/addons/xterm-addon-serialize/src/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "lib": [ + "dom", + "es2015" + ], + "rootDir": ".", + "outDir": "../out", + "sourceMap": true, + "removeComments": true, + "strict": true + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ] +} diff --git a/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts new file mode 100644 index 00000000..9e7b500a --- /dev/null +++ b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + + +import { Terminal, ITerminalAddon } from 'xterm'; + +declare module 'xterm-addon-serialize' { + /** + * An xterm.js addon that enables web links. + */ + export class SerializeAddon implements ITerminalAddon { + + constructor(); + + /** + * Activates the addon + * @param terminal The terminal the addon is being loaded in. + */ + public activate(terminal: Terminal): void; + + /** + * Serializes terminal rows into a string that can be written back to the terminal + * to restore the state. The cursor will also be positioned to the correct cell. + * When restoring a terminal it is best to do before `Terminal.open` is called + * to avoid wasting CPU cycles rendering incomplete frames. + * @param rows The number of rows to serialize, starting from the bottom of the + * terminal. This defaults to the number of rows in the viewport. + */ + public serialize(rows?: number): string; + + /** + * Disposes the addon. + */ + public dispose(): void; + } +} diff --git a/addons/xterm-addon-serialize/webpack.config.js b/addons/xterm-addon-serialize/webpack.config.js new file mode 100644 index 00000000..4cabbad9 --- /dev/null +++ b/addons/xterm-addon-serialize/webpack.config.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'SerializeAddon'; +const mainFile = 'xterm-addon-serialize.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd' + }, + mode: 'production' +}; diff --git a/bin/test.js b/bin/test.js index 775ec004..7beb4f9a 100644 --- a/bin/test.js +++ b/bin/test.js @@ -12,7 +12,8 @@ env.NODE_PATH = path.resolve(__dirname, '../out'); let testFiles = [ './out/*test.js', - './out/**/*test.js' + './out/**/*test.js', + './addons/**/out/*test.js', ]; // ability to inject particular test files via diff --git a/demo/client.ts b/demo/client.ts index 040292e4..1ca35a6a 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -14,6 +14,7 @@ import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon'; import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon'; import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon'; import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon'; +import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAddon'; // Use webpacked version (yarn package) // import { Terminal } from '../lib/xterm'; @@ -35,6 +36,7 @@ export interface IWindowWithTerminal extends Window { SearchAddon?: typeof SearchAddon; WebLinksAddon?: typeof WebLinksAddon; WebglAddon?: typeof WebglAddon; + SerializeAddon?: typeof SerializeAddon; } declare let window: IWindowWithTerminal; @@ -89,6 +91,7 @@ if (document.location.pathname === '/test') { window.SearchAddon = SearchAddon; window.WebLinksAddon = WebLinksAddon; window.WebglAddon = WebglAddon; + window.SerializeAddon = SerializeAddon; } else { createTerminal(); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); diff --git a/tsconfig.all.json b/tsconfig.all.json index 7ddf33d5..5a7ae05b 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -9,6 +9,7 @@ { "path": "./addons/xterm-addon-fit/src" }, { "path": "./addons/xterm-addon-search/src" }, { "path": "./addons/xterm-addon-web-links/src" }, - { "path": "./addons/xterm-addon-webgl/src" } + { "path": "./addons/xterm-addon-webgl/src" }, + { "path": "./addons/xterm-addon-serialize/src" } ] }