diff --git a/addons/xterm-addon-attach/src/AttachAddon.test.ts b/addons/xterm-addon-attach/src/AttachAddon.test.ts index ea56fa38..9d7ebdff 100644 --- a/addons/xterm-addon-attach/src/AttachAddon.test.ts +++ b/addons/xterm-addon-attach/src/AttachAddon.test.ts @@ -15,7 +15,7 @@ let page: puppeteer.Page; const width = 800; const height = 600; -describe.only('API Integration Tests', () => { +describe('API Integration Tests', () => { before(async function(): Promise { this.timeout(10000); browser = await puppeteer.launch({ @@ -60,27 +60,6 @@ describe.only('API Integration Tests', () => { }); }); -// async function testHostName(hostname: string): Promise { -// await openTerminal({ rendererType: 'dom' }); -// await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`); -// await page.evaluate(` -// window.term.writeln(' http://${hostname} '); -// window.term.writeln(' http://${hostname}/a~b#c~d?e~f '); -// window.term.writeln(' http://${hostname}/colon:test '); -// window.term.writeln(' http://${hostname}/colon:test: '); -// window.term.writeln('"http://${hostname}/"'); -// window.term.writeln('\\'http://${hostname}/\\''); -// window.term.writeln('http://${hostname}/subpath/+/id'); -// `); -// assert.equal(await getLinkAtCell(3, 1), `http://${hostname}`); -// assert.equal(await getLinkAtCell(3, 2), `http://${hostname}/a~b#c~d?e~f`); -// assert.equal(await getLinkAtCell(3, 3), `http://${hostname}/colon:test`); -// assert.equal(await getLinkAtCell(3, 4), `http://${hostname}/colon:test`); -// assert.equal(await getLinkAtCell(2, 5), `http://${hostname}/`); -// assert.equal(await getLinkAtCell(2, 6), `http://${hostname}/`); -// assert.equal(await getLinkAtCell(1, 7), `http://${hostname}/subpath/+/id`); -// } - 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'))`); @@ -90,9 +69,3 @@ async function openTerminal(options: ITerminalOptions = {}): Promise { await page.waitForSelector('.xterm-text-layer'); } } - -// async function getLinkAtCell(col: number, row: number): Promise { -// const rowSelector = `.xterm-rows > :nth-child(${row})`; -// await page.hover(`${rowSelector} > :nth-child(${col})`); -// return await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`); -// } diff --git a/addons/xterm-addon-attach/typings/attach.d.ts b/addons/xterm-addon-attach/typings/attach.d.ts index 17dfab6c..a4d6deae 100644 --- a/addons/xterm-addon-attach/typings/attach.d.ts +++ b/addons/xterm-addon-attach/typings/attach.d.ts @@ -4,20 +4,14 @@ */ -import { Terminal, ILinkMatcherOptions } from 'xterm'; - -// TODO: This is temporary, link to xterm when the new version is published -export interface ITerminalAddon { - activate(terminal: Terminal): void; - dispose(): void; -} +import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm'; export interface IAttachOptions { /** * Whether input should be written to the backend. Defaults to `true`. */ bidirectional?: boolean, - + /** * Whether to use UTF8 binary transport for incoming messages. Defaults to `false`. * Note: This must be in line with the server side of the websocket. diff --git a/addons/xterm-addon-fit/.gitignore b/addons/xterm-addon-fit/.gitignore new file mode 100644 index 00000000..3063f07d --- /dev/null +++ b/addons/xterm-addon-fit/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules diff --git a/addons/xterm-addon-fit/.npmignore b/addons/xterm-addon-fit/.npmignore new file mode 100644 index 00000000..6e2d3d99 --- /dev/null +++ b/addons/xterm-addon-fit/.npmignore @@ -0,0 +1,3 @@ +**/*.api.js +**/*.api.ts +tsconfig.json diff --git a/addons/xterm-addon-fit/.yarnrc b/addons/xterm-addon-fit/.yarnrc new file mode 100644 index 00000000..a5311b44 --- /dev/null +++ b/addons/xterm-addon-fit/.yarnrc @@ -0,0 +1 @@ +--modules-folder "../../node_modules" diff --git a/addons/xterm-addon-fit/LICENSE b/addons/xterm-addon-fit/LICENSE new file mode 100644 index 00000000..8f178925 --- /dev/null +++ b/addons/xterm-addon-fit/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/addons/xterm-addon-fit/package.json b/addons/xterm-addon-fit/package.json new file mode 100644 index 00000000..f1085c10 --- /dev/null +++ b/addons/xterm-addon-fit/package.json @@ -0,0 +1,17 @@ +{ + "name": "xterm-addon-fit", + "version": "0.1.0-beta7", + "author": { + "name": "The xterm.js authors", + "url": "https://xtermjs.org/" + }, + "main": "lib/FitAddon.js", + "types": "typings/fit.d.ts", + "license": "MIT", + "scripts": { + "prepublish": "tsc -p src" + }, + "peerDependencies": { + "xterm": "^3.14.0" + } +} diff --git a/addons/xterm-addon-fit/src/FitAddon.api.ts b/addons/xterm-addon-fit/src/FitAddon.api.ts new file mode 100644 index 00000000..baa2cc0c --- /dev/null +++ b/addons/xterm-addon-fit/src/FitAddon.api.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import * as puppeteer from 'puppeteer'; +import { assert } from 'chai'; +import { ITerminalOptions } from 'xterm'; + +const APP = 'http://127.0.0.1:3000/test'; + +let browser: puppeteer.Browser; +let page: puppeteer.Page; +const width = 800; +const height = 600; + +describe('FitAddon', () => { + before(async function(): Promise { + this.timeout(10000); + 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(5000); + await page.goto(APP); + }); +}); + +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'); + } +} diff --git a/addons/xterm-addon-fit/src/FitAddon.ts b/addons/xterm-addon-fit/src/FitAddon.ts new file mode 100644 index 00000000..f2c37141 --- /dev/null +++ b/addons/xterm-addon-fit/src/FitAddon.ts @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ITerminalAddon } from 'xterm'; + +interface ITerminalDimensions { + /** + * The number of rows in the terminal. + */ + rows: number; + + /** + * The number of columns in the terminal. + */ + cols: number; +} + +export class FitAddon implements ITerminalAddon { + private _terminal: Terminal | undefined; + + constructor() {} + + public activate(terminal: Terminal): void { + this._terminal = terminal; + } + + public dispose(): void {} + + public fit(): void { + const dims = this.proposeDimensions(); + if (!dims || !this._terminal) { + return; + } + + // TODO: Remove reliance on private API + const core = (this._terminal)._core; + + // Force a full render + if (this._terminal.rows !== dims.rows || this._terminal.cols !== dims.cols) { + core._renderCoordinator.clear(); + this._terminal.resize(dims.cols, dims.rows); + } + } + + public proposeDimensions(): ITerminalDimensions | undefined { + if (!this._terminal) { + return undefined; + } + + if (!this._terminal.element.parentElement) { + return undefined; + } + + // TODO: Remove reliance on private API + const core = (this._terminal)._core; + + const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement); + const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); + const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'))); + const elementStyle = window.getComputedStyle(this._terminal.element); + const elementPadding = { + top: parseInt(elementStyle.getPropertyValue('padding-top')), + bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')), + right: parseInt(elementStyle.getPropertyValue('padding-right')), + left: parseInt(elementStyle.getPropertyValue('padding-left')) + }; + const elementPaddingVer = elementPadding.top + elementPadding.bottom; + const elementPaddingHor = elementPadding.right + elementPadding.left; + const availableHeight = parentElementHeight - elementPaddingVer; + const availableWidth = parentElementWidth - elementPaddingHor - core.viewport.scrollBarWidth; + const geometry = { + cols: Math.floor(availableWidth / core._renderCoordinator.dimensions.actualCellWidth), + rows: Math.floor(availableHeight / core._renderCoordinator.dimensions.actualCellHeight) + }; + return geometry; + } +} diff --git a/addons/xterm-addon-fit/src/tsconfig.json b/addons/xterm-addon-fit/src/tsconfig.json new file mode 100644 index 00000000..66bf3fb6 --- /dev/null +++ b/addons/xterm-addon-fit/src/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "lib": [ + "dom", + "es5", + "es2015" + ], + "rootDir": ".", + "outDir": "../lib", + "sourceMap": true, + "removeComments": true, + "strict": true + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ] +} diff --git a/addons/xterm-addon-fit/typings/fit.d.ts b/addons/xterm-addon-fit/typings/fit.d.ts new file mode 100644 index 00000000..c3b0b4af --- /dev/null +++ b/addons/xterm-addon-fit/typings/fit.d.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ITerminalAddon } from 'xterm'; + +declare module 'xterm-addon-fit' { + /** + * An xterm.js addon that enables resizing the terminal to the dimensions of + * its containing element. + */ + export class FitAddon implements ITerminalAddon { + /** + * Creates a new fit addon. + */ + constructor(); + + /** + * Activates the addon + * @param terminal The terminal the addon is being loaded in. + */ + public activate(terminal: Terminal): void; + + /** + * Disposes the addon. + */ + public dispose(): void; + + /** + * Resizes the terminal to the dimensions of its containing element. + */ + public fit(): void; + + /** + * Gets the proposed dimensions that will be used for a fit. + */ + public proposeDimensions(): ITerminalDimensions; + } + + /** + * Reprepresents the dimensions of a terminal. + */ + export interface ITerminalDimensions { + /** + * The number of rows in the terminal. + */ + rows: number; + + /** + * The number of columns in the terminal. + */ + cols: number; + } +} diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts index 7dfe9514..2c0044be 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts @@ -14,7 +14,7 @@ let page: puppeteer.Page; const width = 800; const height = 600; -describe.only('WebLinksAddon', () => { +describe('WebLinksAddon', () => { before(async function(): Promise { this.timeout(10000); browser = await puppeteer.launch({ diff --git a/demo/client.ts b/demo/client.ts index da63ec0d..1841e991 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -13,11 +13,10 @@ import { Terminal } from '../out/public/Terminal'; // import { Terminal } from '../lib/xterm'; import { AttachAddon } from 'xterm-addon-attach'; +import { FitAddon } from 'xterm-addon-fit'; import { SearchAddon, ISearchOptions } from 'xterm-addon-search'; import { WebLinksAddon } from 'xterm-addon-web-links'; -import * as fit from '../lib/addons/fit/fit'; - // Pulling in the module's types relies on the above, it's looks a // little weird here as we're importing "this" module import { Terminal as TerminalType, ITerminalOptions } from 'xterm'; @@ -26,14 +25,14 @@ export interface IWindowWithTerminal extends Window { term: TerminalType; Terminal?: typeof TerminalType; AttachAddon?: typeof AttachAddon; + FitAddon?: typeof FitAddon; SearchAddon?: typeof SearchAddon; WebLinksAddon?: typeof WebLinksAddon; } declare let window: IWindowWithTerminal; -Terminal.applyAddon(fit); - let term; +let fitAddon: FitAddon; let searchAddon: SearchAddon; let protocol; let socketURL; @@ -78,6 +77,7 @@ const disposeRecreateButtonHandler = () => { if (document.location.pathname === '/test') { window.Terminal = Terminal; window.AttachAddon = AttachAddon; + window.FitAddon = FitAddon; window.SearchAddon = SearchAddon; window.WebLinksAddon = WebLinksAddon; } else { @@ -101,6 +101,8 @@ function createTerminal(): void { typedTerm.loadAddon(new WebLinksAddon()); searchAddon = new SearchAddon(); typedTerm.loadAddon(searchAddon); + fitAddon = new FitAddon(); + typedTerm.loadAddon(fitAddon); window.term = term; // Expose `term` to window for debugging purposes term.onResize((size: { cols: number, rows: number }) => { @@ -117,7 +119,7 @@ function createTerminal(): void { socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; term.open(terminalContainer); - term.fit(); + fitAddon.fit(); term.focus(); addDomListener(paddingElement, 'change', setPadding); @@ -309,5 +311,5 @@ function updateTerminalSize(): void { const height = (rows * term._core._renderCoordinator.dimensions.actualCellHeight).toString() + 'px'; terminalContainer.style.width = width; terminalContainer.style.height = height; - term.fit(); + fitAddon.fit(); } diff --git a/demo/tsconfig.json b/demo/tsconfig.json index 06797fc1..2bf76f67 100644 --- a/demo/tsconfig.json +++ b/demo/tsconfig.json @@ -7,6 +7,7 @@ "baseUrl": ".", "paths": { "xterm-addon-attach": ["../addons/xterm-addon-attach"], + "xterm-addon-fit": ["../addons/xterm-addon-fit"], "xterm-addon-search": ["../addons/xterm-addon-search"], "xterm-addon-web-links": ["../addons/xterm-addon-web-links"] } diff --git a/tsconfig.all.json b/tsconfig.all.json index b2e74f03..12f9b3dc 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -4,6 +4,7 @@ "references": [ { "path": "./src" }, { "path": "./addons/xterm-addon-attach/src" }, + { "path": "./addons/xterm-addon-fit/src" }, { "path": "./addons/xterm-addon-search/src" }, { "path": "./addons/xterm-addon-web-links/src" }, { "path": "./src/addons/fit" }