diff --git a/addons/xterm-addon-serialize/.gitignore b/addons/xterm-addon-serialize/.gitignore new file mode 100644 index 00000000..3063f07d --- /dev/null +++ b/addons/xterm-addon-serialize/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules diff --git a/addons/xterm-addon-serialize/.npmignore b/addons/xterm-addon-serialize/.npmignore new file mode 100644 index 00000000..1c794445 --- /dev/null +++ b/addons/xterm-addon-serialize/.npmignore @@ -0,0 +1,5 @@ +**/*.api.js +**/*.api.ts +tsconfig.json +.yarnrc +webpack.config.js diff --git a/addons/xterm-addon-serialize/package.json b/addons/xterm-addon-serialize/package.json new file mode 100644 index 00000000..c8a66c4b --- /dev/null +++ b/addons/xterm-addon-serialize/package.json @@ -0,0 +1,20 @@ +{ + "name": "xterm-addon-serialize", + "version": "0.1.0", + "author": { + "name": "The xterm.js authors", + "url": "https://xtermjs.org/" + }, + "main": "lib/xterm-addon-serialize.js", + "types": "typings/xterm-addon-serialize.d.ts", + "license": "MIT", + "scripts": { + "build": "../../node_modules/.bin/tsc -p src", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package" + }, + "peerDependencies": { + "xterm": "^3.14.0" + } +} diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.api.ts b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts new file mode 100644 index 00000000..88e4503e --- /dev/null +++ b/addons/xterm-addon-serialize/src/SerializeAddon.api.ts @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as puppeteer from 'puppeteer'; +import * as util from 'util'; +import { assert } from 'chai'; +import { ITerminalOptions } from 'xterm'; + +const APP = 'http://127.0.0.1:3000/test'; + +let browser: puppeteer.Browser; +let page: puppeteer.Page; +const width = 800; +const height = 600; + +describe('SerializeAddon', () => { + before(async function (): Promise { + this.timeout(20000); + browser = await puppeteer.launch({ + headless: process.argv.indexOf('--headless') !== -1, + slowMo: 80, + args: [`--window-size=${width},${height}`] + }); + page = (await browser.pages())[0]; + await page.setViewport({ width, height }); + }); + + after(async () => { + await browser.close(); + }); + + beforeEach(async function (): Promise { + this.timeout(20000); + await page.goto(APP); + }); + + it('empty content', async function (): Promise { + this.timeout(20000); + const rows = 10; + const cols = 10; + const blankline = ' '.repeat(cols); + const lines = newArray(blankline, rows); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + `) + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('digits content', async function (): Promise { + this.timeout(20000); + const rows = 10; + const cols = 10; + const digitsLine = digitsString(cols); + const lines = newArray(digitsLine, rows); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `) + + assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n')); + }); + + it('serialize n rows of content', async function (): Promise { + this.timeout(20000); + const rows = 10; + const halfRows = rows >> 1; + const cols = 10; + const lines = newArray((index: number) => digitsString(cols, index), rows); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `) + + assert.equal(await page.evaluate(`serializeAddon.serialize(${halfRows});`), lines.slice(0, halfRows).join('\r\n')); + }); + + it('serialize 0 rows of content', async function (): Promise { + this.timeout(20000); + const rows = 10; + const cols = 10; + const lines = newArray((index: number) => digitsString(cols, index), rows); + + await openTerminal({ rows: rows, cols: cols, rendererType: 'dom' }); + await page.evaluate(` + window.serializeAddon = new SerializeAddon(); + window.term.loadAddon(window.serializeAddon); + window.term.write(${util.inspect(lines.join('\r\n'))}); + `) + + assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), ''); + }); +}); + +async function openTerminal(options: ITerminalOptions = {}): Promise { + await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`); + await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); + if (options.rendererType === 'dom') { + await page.waitForSelector('.xterm-rows'); + } else { + await page.waitForSelector('.xterm-text-layer'); + } +} + +function newArray(initial: T | ((index: number) => T), count: number): T[] { + const array: T[] = new Array(count); + for (let i = 0; i < array.length; i++) { + if (typeof initial === 'function') { + array[i] = (<(index: number) => T>initial)(i); + } else { + array[i] = initial; + } + } + return array; +} + +function digitsString(length: number, from: number = 0) { + let s = ''; + for (let i = 0; i < length; i++) { + s += (from++) % 10; + } + return s; +} diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts new file mode 100644 index 00000000..8dd5b194 --- /dev/null +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ITerminalAddon } from 'xterm'; + +export class SerializeAddon implements ITerminalAddon { + private _terminal: Terminal | undefined; + + constructor() { } + + public activate(terminal: Terminal): void { + this._terminal = terminal; + } + + public serialize(rows?: number): string { + if (!this._terminal) { + return ''; + } + const buffer = this._terminal.buffer; + const length = Math.max(0, Math.min((rows === undefined ? buffer.length : rows), buffer.length)); + let data = ''; + + for (let i = 0; i < length; i++) { + const line = buffer.getLine(i); + const last = i === length - 1; + if (line) { + data += line.translateToString(); + } + if (!last) { + data += '\r\n'; + } + } + + return data; + } + + public dispose(): void { + if (this._terminal !== undefined) { } + } +} diff --git a/addons/xterm-addon-serialize/src/tsconfig.json b/addons/xterm-addon-serialize/src/tsconfig.json new file mode 100644 index 00000000..5539aa56 --- /dev/null +++ b/addons/xterm-addon-serialize/src/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "lib": [ + "dom", + "es2015" + ], + "rootDir": ".", + "outDir": "../out", + "sourceMap": true, + "removeComments": true, + "strict": true + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ] +} diff --git a/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts new file mode 100644 index 00000000..e36e724a --- /dev/null +++ b/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + + +import { Terminal, ITerminalAddon } from 'xterm'; + +declare module 'xterm-addon-serialize' { + /** + * An xterm.js addon that enables web links. + */ + export class SerializeAddon implements ITerminalAddon { + + constructor(); + + /** + * Activates the addon + * @param terminal The terminal the addon is being loaded in. + */ + public activate(terminal: Terminal): void; + + public serialize(rows?: number): string; + + /** + * Disposes the addon. + */ + public dispose(): void; + } +} diff --git a/addons/xterm-addon-serialize/webpack.config.js b/addons/xterm-addon-serialize/webpack.config.js new file mode 100644 index 00000000..4cabbad9 --- /dev/null +++ b/addons/xterm-addon-serialize/webpack.config.js @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'SerializeAddon'; +const mainFile = 'xterm-addon-serialize.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd' + }, + mode: 'production' +}; diff --git a/demo/client.ts b/demo/client.ts index e2259841..eb8b1053 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -14,6 +14,7 @@ import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon'; import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon'; import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon'; import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon'; +import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAddon'; // Use webpacked version (yarn package) // import { Terminal } from '../lib/xterm'; @@ -35,6 +36,7 @@ export interface IWindowWithTerminal extends Window { SearchAddon?: typeof SearchAddon; WebLinksAddon?: typeof WebLinksAddon; WebglAddon?: typeof WebglAddon; + SerializeAddon?: typeof SerializeAddon; } declare let window: IWindowWithTerminal; @@ -88,6 +90,7 @@ if (document.location.pathname === '/test') { window.SearchAddon = SearchAddon; window.WebLinksAddon = WebLinksAddon; window.WebglAddon = WebglAddon; + window.SerializeAddon = SerializeAddon; } else { createTerminal(); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); diff --git a/package.json b/package.json index f26c010f..1ee4986d 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test": "npm run test-unit", "posttest": "npm run lint", "test-api": "mocha \"**/*.api.js\"", + "test-addons": "mocha \"**/*.test.js\"", "test-unit": "node ./bin/test.js", "build": "tsc -b ./tsconfig.all.json", "prepare": "npm run build", diff --git a/tsconfig.all.json b/tsconfig.all.json index 7ddf33d5..5a7ae05b 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -9,6 +9,7 @@ { "path": "./addons/xterm-addon-fit/src" }, { "path": "./addons/xterm-addon-search/src" }, { "path": "./addons/xterm-addon-web-links/src" }, - { "path": "./addons/xterm-addon-webgl/src" } + { "path": "./addons/xterm-addon-webgl/src" }, + { "path": "./addons/xterm-addon-serialize/src" } ] }