diff --git a/.gitignore b/.gitignore index 8e28c797..bf0c3d8c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ fixtures/typings-test/*.js dist/ src/utils/TestUtils.ts src/xterm.js + +# Keep the demo builds out of Git +demo/dist/ diff --git a/AUTHORS b/AUTHORS index 984b896f..1bd4c87d 100644 --- a/AUTHORS +++ b/AUTHORS @@ -19,45 +19,63 @@ Bill Church Bob Reid bottleofwater Brian Mock +Bruno Ribeito Carson Anderson CHaBou Christian Budde Christensen Christof Marti Christopher Jeffrey coderaiser +Dan Brown +Daniel Griffen Daniel Imms Daniel Risacher Dan Kaplun Darin Morrison +dcylabs +Dominik Csapak Edgar Andrés Margffoy Tuay Elliot Saba +Exile +Felipe Gasper +ficristo Gary Ritchie hiro-su Ian Lewis imoses InDieTasten irokas +Jakob Gillich Jean Bruenn Jeremy Danyow Joao Moreno Joao Moreno +Johannes Zellner Jon Masters Jörg Breitbart Justin Mecham Kirill Merkushev +Luca Lucian Buzzo +Lukas Drgon Maël Nison +Marc Dumais Martin Chloride +Martin Koppehel Martin Wang Matt Bierner Michael Irwin Mikko Karvonen mofux Nicolas Ramz +npezza93 Oleksandr Andriienko Paris Kasidiaris Paris Kasidiaris +Peter Baumgarten +Rick Baker runarberg +Samuel Williams Saswat Das Saul Costa Shuanglei Tao diff --git a/Dockerfile b/Dockerfile index 1f0db1f9..22556f43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:6.9 +FROM node:6 MAINTAINER Paris Kasidiaris # Set the working directory @@ -6,7 +6,7 @@ WORKDIR /usr/src/app # Set an entrypoint, to automatically install node modules ENTRYPOINT ["/bin/bash", "-c", "if [[ ! -d node_modules ]]; then npm install; fi; exec \"${@:0}\";"] -CMD ["npm", "run", "dev"] +CMD ["npm", "run", "start"] # First, install dependencies to improve layer caching COPY package.json /usr/src/app/ diff --git a/Procfile.dev b/Procfile.dev deleted file mode 100644 index 8d2c5445..00000000 --- a/Procfile.dev +++ /dev/null @@ -1 +0,0 @@ -web: npm run dev diff --git a/README.md b/README.md index 4d7596ab..5a77806f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # [![xterm.js logo](logo-full.png)](https://xtermjs.org) -[![xterm.js build status](https://api.travis-ci.org/sourcelair/xterm.js.svg)](https://travis-ci.org/sourcelair/xterm.js) [![Coverage Status](https://coveralls.io/repos/github/sourcelair/xterm.js/badge.svg)](https://coveralls.io/github/sourcelair/xterm.js) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm) +[![xterm.js build status](https://api.travis-ci.org/xtermjs/xterm.js.svg)](https://travis-ci.org/xtermjs/xterm.js) [![Coverage Status](https://coveralls.io/repos/github/sourcelair/xterm.js/badge.svg)](https://coveralls.io/github/sourcelair/xterm.js) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm) Xterm.js is a terminal front-end component written in JavaScript that works in the browser. @@ -49,50 +49,33 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`. -### Addons - -Addons are JavaScript modules that attach functions to the `Terminal` prototype to extend its functionality. There are a handful available in the main repository in the `dist/addons` directory, you can even write your own (though they may break when the internals of xterm.js change across versions). - -To use an addon, just include the JavaScript file after xterm.js and before the `Terminal` object has been instantiated. The function should then be exposed on the `Terminal` object: - -```html - - -``` - -```js -// Instantiate the terminal and call fit -var xterm = new Terminal(); -xterm.fit(); -``` - ### Importing -If the environment allows it, you can import xterm.js like so: +The proposed way to load xterm.js is via the ES6 module syntax. -```ts -// CommonJS -var Terminal = require('xterm').Terminal; - -// ES6 / TypeScript +```javascript import { Terminal } from 'xterm'; ``` -Importing addons in this environment can be done using a `Terminal.loadAddon` call: +*Note: There are currently no typings for addons so you will need to upcast if using TypeScript, eg. `(xterm).fit()`.* -```ts -import { Terminal } from 'xterm'; +### Addons -// Notice it's called statically on the type, not an object -Terminal.loadAddon('fit'); +Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own, by using xterm.js' public API. -// Instantiate the terminal and call fit -var xterm = new Terminal(); -xterm.fit(); +To use an addon, just import the JavaScript module and pass it to `Terminal`'s `applyAddon` method: + +```javascript +import { Terminal } from xterm; +import * as fit from 'xterm/lib/addons/fit/fit'; + + +Terminal.applyAddon(fit); + +var xterm = new Terminal(); // Instantiate the terminal +xterm.fit(); // Use the `fit` method, provided by the `fit` addon ``` -*Note: There are currently no typings for addons so you will need to upcast if using TypeScript, eg. `(xterm).fit()`* - ## Browser Support Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support: @@ -131,6 +114,11 @@ Xterm.js is used in several world-class applications to provide great terminal e computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages. - [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript. - [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations. +- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses xterm.js for container terminals and the host shell. +- [**Script Runner**](https://github.com/ioquatix/script-runner): Run scripts (or a shell) in Atom. +- [**Whack Whack Terminal**](https://github.com/Microsoft/WhackWhackTerminal): Terminal emulator for Visual Studio 2017. +- [**VTerm**](https://github.com/vterm/vterm): Extensible terminal emulator based on Electron and React. +- [**electerm**](http://electerm.html5beta.com): electerm is a terminal/ssh/sftp client(mac, win, linux) based on electron/node-pty/xterm. Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. diff --git a/demo/app.js b/demo/app.js index 8637dad8..7604e9cd 100644 --- a/demo/app.js +++ b/demo/app.js @@ -17,8 +17,12 @@ app.get('/style.css', function(req, res){ res.sendFile(__dirname + '/style.css'); }); -app.get('/main.js', function(req, res){ - res.sendFile(__dirname + '/main.js'); +app.get('/dist/bundle.js', function(req, res){ + res.sendFile(__dirname + '/dist/bundle.js'); +}); + +app.get('/dist/bundle.js.map', function(req, res){ + res.sendFile(__dirname + '/dist/bundle.js.map'); }); app.post('/terminals', function (req, res) { diff --git a/demo/index.html b/demo/index.html index 8d900903..93399f9c 100644 --- a/demo/index.html +++ b/demo/index.html @@ -7,12 +7,6 @@ - - - - - -

xterm.js: xterm, in the browser

@@ -71,6 +65,6 @@

Attention: The demo is a barebones implementation and is designed for xterm.js evaluation purposes only. Exposing the demo to the public as is would introduce security risks for the host.

- + diff --git a/demo/main.js b/demo/main.js index 9de682bf..95b15a0d 100644 --- a/demo/main.js +++ b/demo/main.js @@ -1,3 +1,18 @@ +import * as Terminal from '../build/xterm'; +import * as attach from '../build/addons/attach/attach'; +import * as fit from '../build/addons/fit/fit'; +import * as fullscreen from '../build/addons/fullscreen/fullscreen'; +import * as search from '../build/addons/search/search'; +import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat'; + + +Terminal.applyAddon(attach); +Terminal.applyAddon(fit); +Terminal.applyAddon(fullscreen); +Terminal.applyAddon(search); +Terminal.applyAddon(winptyCompat); + + var term, protocol, socketURL, @@ -24,8 +39,8 @@ function setTerminalSize() { var rows = parseInt(rowsElement.value, 10); var viewportElement = document.querySelector('.xterm-viewport'); var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth; - var width = (cols * term.charMeasure.width + 20 /*room for scrollbar*/).toString() + 'px'; - var height = (rows * term.charMeasure.height).toString() + 'px'; + var width = (cols * term.renderer.dimensions.actualCellWidth + 20 /*room for scrollbar*/).toString() + 'px'; + var height = (rows * term.renderer.dimensions.actualCellHeight).toString() + 'px'; terminalContainer.style.width = width; terminalContainer.style.height = height; @@ -76,6 +91,7 @@ function createTerminal() { scrollback: parseInt(optionElements.scrollback.value, 10), tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10) }); + window.term = term; // Expose `term` to window for debugging purposes term.on('resize', function (size) { if (!pid) { return; @@ -90,8 +106,9 @@ function createTerminal() { socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; term.open(terminalContainer); - term.fit(); term.winptyCompatInit(); + term.fit(); + term.focus(); // fit is called within a setTimeout, cols and rows need this. setTimeout(function () { @@ -103,9 +120,9 @@ function createTerminal() { fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) { - res.text().then(function (pid) { - window.pid = pid; - socketURL += pid; + res.text().then(function (processId) { + pid = processId; + socketURL += processId; socket = new WebSocket(socketURL); socket.onopen = runRealTerminal; socket.onclose = runFakeTerminal; diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 9579dcf5..00000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,9 +0,0 @@ -version: '2' - -services: - web: - build: ./ - volumes: - - ./:/usr/src/app - ports: - - 3000:3000 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..03e88fbc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3" + +services: + web: + build: . + volumes: + - ./:/usr/src/app + ports: + - ${XTERMJS_PORT:3000}:3000 diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts index 887eb322..5a5f6c50 100644 --- a/fixtures/typings-test/typings-test.ts +++ b/fixtures/typings-test/typings-test.ts @@ -36,12 +36,12 @@ namespace properties { namespace static_methods { { - Terminal.loadAddon('attach'); - Terminal.loadAddon('fit'); - Terminal.loadAddon('fullscreen'); - Terminal.loadAddon('search'); - Terminal.loadAddon('terminado'); - Terminal.loadAddon('winptyCompat'); + Terminal.applyAddon({}); + Terminal.applyAddon({}); + Terminal.applyAddon({}); + Terminal.applyAddon({}); + Terminal.applyAddon({}); + Terminal.applyAddon({}); } } @@ -63,7 +63,7 @@ namespace methods_core { // no arg t.on('blur', () => {}); t.on('focus', () => {}); - t.on('lineFeed', () => {}); + t.on('linefeed', () => {}); t.on('selection', () => {}); // args t.on('data', () => {}); @@ -89,7 +89,7 @@ namespace methods_core { // no arg t.off('blur', () => {}); t.off('focus', () => {}); - t.off('lineFeed', () => {}); + t.off('linefeed', () => {}); t.off('selection', () => {}); // args t.off('data', () => {}); @@ -170,9 +170,9 @@ namespace methods_core { t.setOption('handler', (data: string) => console.log(data)); t.setOption('bellSound', 'foo'); t.setOption('bellStyle', 'none'); - t.setOption('bellStyle', 'visual'); + // t.setOption('bellStyle', 'visual'); t.setOption('bellStyle', 'sound'); - t.setOption('bellStyle', 'both'); + // t.setOption('bellStyle', 'both'); t.setOption('fontSize', 1); t.setOption('lineHeight', 1); t.setOption('fontFamily', 'foo'); diff --git a/gulpfile.js b/gulpfile.js index 13a3ddac..49d67ed6 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -16,6 +16,7 @@ const source = require('vinyl-source-stream'); const sourcemaps = require('gulp-sourcemaps'); const ts = require('gulp-typescript'); const util = require('gulp-util'); +const webpack = require('webpack-stream'); let buildDir = process.env.BUILD_DIR || 'build'; let tsProject = ts.createProject('tsconfig.json'); @@ -44,30 +45,31 @@ gulp.task('tsc', function () { tsResult.dts.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir)) ); - fs.emptyDirSync(`${outDir}/addons/search`); - fs.emptyDirSync(`${outDir}/addons/winptyCompat`); + let addons = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem']; + let addonStreams = addons.map(function(addon) { + fs.emptyDirSync(`${outDir}/addons/${addon}`); - let tsProjectSearchAddon = ts.createProject('./src/addons/search/tsconfig.json'); - let tsResultSearchAddon = tsProjectSearchAddon.src().pipe(sourcemaps.init()).pipe(tsProjectSearchAddon()); - let tscSearchAddon = tsResultSearchAddon.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(`${outDir}/addons/search`)); + let tsProjectAddon = ts.createProject(`./src/addons/${addon}/tsconfig.json`); + let tsResultAddon = tsProjectAddon.src().pipe(sourcemaps.init()).pipe(tsProjectAddon()); + let tscAddon = tsResultAddon.js + .pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})) + .pipe(gulp.dest(`${outDir}/addons/${addon}`)); - let tsProjectWinptyCompatAddon = ts.createProject('./src/addons/winptyCompat/tsconfig.json'); - let tsResultWinptyCompatAddon = tsProjectWinptyCompatAddon.src().pipe(sourcemaps.init()).pipe(tsProjectWinptyCompatAddon()); - let tscWinptyCompatAddon = tsResultWinptyCompatAddon.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(`${outDir}/addons/winptyCompat`)); + return tscAddon; + }); // Copy all addons from ${srcDir}/ to ${outDir}/ let copyAddons = gulp.src([ - `${srcDir}/addons/**/*`, - `!${srcDir}/addons/search`, - `!${srcDir}/addons/search/**`, - `!${srcDir}/addons/winptyCompat`, - `!${srcDir}/addons/winptyCompat/**` + `${srcDir}/addons/**/**` ]).pipe(gulp.dest(`${outDir}/addons`)); // Copy stylesheets from ${srcDir}/ to ${outDir}/ let copyStylesheets = gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir)); - return merge(tsc, tscSearchAddon, tscWinptyCompatAddon, copyAddons, copyStylesheets); + // Join all streams into a single array + let streams = [tsc].concat(addonStreams).concat([copyAddons, copyStylesheets]); + + return merge.apply(this, streams); }); /** @@ -136,10 +138,7 @@ gulp.task('browserify-addons', ['tsc'], function() { // Copy all add-ons from outDir to buildDir let copyAddons = gulp.src([ // Copy JS addons - `${outDir}/addons/**/*`, - // Exclude TS addons from copy as they are being built via browserify - `!${outDir}/addons/search`, - `!${outDir}/addons/search/**`, + `${outDir}/addons/**/*` ]).pipe(gulp.dest(`${buildDir}/addons`)); return merge(searchBundle, winptyCompatBundle, copyAddons); @@ -195,6 +194,12 @@ gulp.task('sorcery-addons', ['browserify-addons'], function () { chain.writeSync(); }); +gulp.task('webpack', ['build'], function() { + return gulp.src('demo/main.js') + .pipe(webpack(require('./webpack.config.js'))) + .pipe(gulp.dest('demo/dist/')); +}); + /** * Submit coverage results to coveralls.io */ diff --git a/package-lock.json b/package-lock.json index 75d2afca..c83a4d9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "xterm", - "version": "2.8.1", + "version": "2.9.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -44,6 +44,12 @@ "integrity": "sha512-7F3/P6MkTPA0QxOstRqfcnoReCUy5V/QG92cyBoZSPnqdX44L8TtNELSVfN56gAttm3YWj9cEi8FRIPVq0WmeQ==", "dev": true }, + "@types/text-encoding": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/text-encoding/-/text-encoding-0.0.32.tgz", + "integrity": "sha512-kQ79aFmYcD/DR3QKo6wXyvNrKi7PunY0KYTBUhHjHl0SXwWuLRl9Leh73YtnsSJMBi9qSiK+fxWhdJNQPbhc9A==", + "dev": true + }, "@types/tough-cookie": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-2.3.0.tgz", @@ -555,12 +561,29 @@ "integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk=", "dev": true }, + "clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", + "dev": true + }, "clone-stats": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", "dev": true }, + "cloneable-readable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.0.0.tgz", + "integrity": "sha1-pikNQT8hemEjL5XkWP84QYz7ARc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "process-nextick-args": "1.0.7", + "through2": "2.0.3" + } + }, "combine-source-map": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.7.2.tgz", @@ -620,6 +643,15 @@ } } }, + "concat-with-sourcemaps": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.0.4.tgz", + "integrity": "sha1-9Vs74q60dgGxCi1SWcz7cP0vHdY=", + "dev": true, + "requires": { + "source-map": "0.5.6" + } + }, "console-browserify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", @@ -662,6 +694,16 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "crc-32": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.1.1.tgz", + "integrity": "sha1-XXOdXkxuNSrYME1zIj1IP+Va240=", + "dev": true, + "requires": { + "exit-on-epipe": "1.0.1", + "printj": "1.1.0" + } + }, "create-ecdh": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.0.tgz", @@ -1021,6 +1063,12 @@ } } }, + "exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", + "dev": true + }, "express": { "version": "4.13.4", "resolved": "https://registry.npmjs.org/express/-/express-4.13.4.tgz", @@ -4190,6 +4238,51 @@ } } }, + "gulp-concat": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/gulp-concat/-/gulp-concat-2.6.1.tgz", + "integrity": "sha1-Yz0WyV2IUEYorQJmVmPO5aR5M1M=", + "dev": true, + "requires": { + "concat-with-sourcemaps": "1.0.4", + "through2": "2.0.3", + "vinyl": "2.1.0" + }, + "dependencies": { + "clone": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", + "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", + "dev": true + }, + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "vinyl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.1.0.tgz", + "integrity": "sha1-Ah+cLPlR1rk5lDyJ617lrdT9kkw=", + "dev": true, + "requires": { + "clone": "2.1.1", + "clone-buffer": "1.0.0", + "clone-stats": "1.0.0", + "cloneable-readable": "1.0.0", + "remove-trailing-separator": "1.0.2", + "replace-ext": "1.0.0" + } + } + } + }, "gulp-coveralls": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/gulp-coveralls/-/gulp-coveralls-0.1.4.tgz", @@ -9519,28 +9612,19 @@ "duplexer2": "0.0.2" } }, + "nan": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", + "integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=", + "dev": true + }, "node-pty": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-0.4.1.tgz", - "integrity": "sha1-qAs1/le2TwVasZsteqYE2DYVXAQ=", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-0.7.3.tgz", + "integrity": "sha1-S7NqJKYu6fMzLi4jENZkLZ2u0fc=", "dev": true, "requires": { - "extend": "1.2.1", - "nan": "2.2.1" - }, - "dependencies": { - "extend": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz", - "integrity": "sha1-oPX9bPyDpf5J72mNYOyKYk3UV2w=", - "dev": true - }, - "nan": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.2.1.tgz", - "integrity": "sha1-1oaT9rNLtB1mvGizpPne/HnXFJs=", - "dev": true - } + "nan": "2.8.0" } }, "nodemon": { @@ -10899,6 +10983,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "printj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.0.tgz", + "integrity": "sha512-NbiNBOQ0GioHyeD3ni8wZB7ZmfU7mxIrqhWR5XSreX3rUVvk5UOwpzxOnWqrLdCtoBbdQ40sEwC+nXxxjlUo0A==", + "dev": true + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -12271,9 +12361,9 @@ "dev": true }, "typescript": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.2.2.tgz", - "integrity": "sha1-YGAiUIR5tV/6NotY/uljoD39eww=", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.2.tgz", + "integrity": "sha1-+DlfhdRZJ2BnyYiqQYN6j4KHCEQ=", "dev": true }, "umd": { @@ -12547,6 +12637,15 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true + }, + "zmodem.js": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/zmodem.js/-/zmodem.js-0.1.6.tgz", + "integrity": "sha1-QNeLS0uYQiBo5CB5II/wgFcLrdk=", + "dev": true, + "requires": { + "crc-32": "1.1.1" + } } } } diff --git a/package.json b/package.json index 6cdebd90..1dfadf9f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "xterm", "description": "Full xterm terminal, in your browser", - "version": "2.9.1", + "version": "3.0.0", "ignore": [ "demo", "test", @@ -9,7 +9,7 @@ ], "main": "lib/Terminal.js", "types": "typings/xterm.d.ts", - "repository": "https://github.com/sourcelair/xterm.js", + "repository": "https://github.com/xtermjs/xterm.js", "license": "MIT", "files": [ "*.js", @@ -35,13 +35,15 @@ "src/*.ts", "src/**/*.js", "src/**/*.js.map", - "src/**/*.ts" + "src/**/*.ts", + "typings/*.d.ts" ], "devDependencies": { "@types/chai": "^3.4.34", "@types/jsdom": "^11.0.1", "@types/mocha": "^2.2.33", "@types/node": "^6.0.41", + "@types/text-encoding": "0.0.32", "browserify": "^13.3.0", "chai": "3.5.0", "docdash": "0.4.0", @@ -68,20 +70,22 @@ "typescript": "~2.4.0", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", + "webpack": "^3.10.0", + "webpack-stream": "^4.0.0", "zmodem.js": "^0.1.5" }, "scripts": { - "prestart": "npm run build", + "prestart": "gulp webpack", "start": "node demo/app", "prestart-zmodem": "npm run build", "start-zmodem": "node build/addons/zmodem/demo/app", - "dev": "nodemon -e js,ts,css --watch src --watch demo --exec npm start", "lint": "tslint src/*.ts src/**/*.ts", "test": "gulp test", "build:docs": "jsdoc -c jsdoc.json", "build": "gulp build", "prepublish": "npm run build", - "coveralls": "gulp coveralls" + "coveralls": "gulp coveralls", + "webpack": "gulp webpack" }, "dependencies": {} } diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 512cf878..1ba6dc7a 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -134,9 +134,9 @@ export class InputHandler implements IInputHandler { /** * This event is emitted whenever the terminal outputs a LF or NL. * - * @event lineFeed + * @event linefeed */ - this._terminal.emit('lineFeed'); + this._terminal.emit('linefeed'); } /** diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 8c6ea3e1..a10dc669 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -14,6 +14,10 @@ import { BufferSet } from './BufferSet'; import { MockTerminal } from './utils/TestUtils.test'; import { LineData, CharData } from './Types'; +class TestMockTerminal extends MockTerminal { + emit(event: string, data: any): void {} +} + class TestSelectionManager extends SelectionManager { constructor( terminal: ITerminal, @@ -48,7 +52,7 @@ describe('SelectionManager', () => { dom = new jsdom.JSDOM(''); window = dom.window; document = window.document; - terminal = new MockTerminal(); + terminal = new TestMockTerminal(); terminal.cols = 80; terminal.rows = 2; terminal.options.scrollback = 100; diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 387ce8d1..cd6fc996 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -255,7 +255,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager public selectAll(): void { this._model.isSelectAllActive = true; this.refresh(); - this.emit('selection'); + this._terminal.emit('selection'); } /** @@ -528,7 +528,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager this._terminal.scrollLines(this._dragScrollAmount, false); // Re-evaluate selection if (this._dragScrollAmount > 0) { - this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.buffer.ydisp + this._terminal.rows]; + this._model.selectionEnd = [this._terminal.cols - 1, Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1)]; } else { this._model.selectionEnd = [0, this._terminal.buffer.ydisp]; } @@ -544,7 +544,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager this._removeMouseDownListeners(); if (this.hasSelection) - this.emit('selection'); + this._terminal.emit('selection'); } /** diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index e9acd537..9cb0b75c 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -3,8 +3,9 @@ * @license MIT */ - import { assert, expect } from 'chai'; +import { assert, expect } from 'chai'; import { Terminal } from './Terminal'; +import * as attach from './addons/attach/attach'; import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; @@ -42,9 +43,9 @@ describe('term.js addons', () => { }; }); - it('should load addons with Terminal.loadAddon', () => { - Terminal.loadAddon('attach'); - // Test that addon was loaded successfully, adding attach to Terminal's + it('should apply addons with Terminal.applyAddon', () => { + Terminal.applyAddon(attach); + // Test that addon was applied successfully, adding attach to Terminal's // prototype. assert.equal(typeof (Terminal).prototype.attach, 'function'); }); diff --git a/src/Terminal.ts b/src/Terminal.ts index 9e6312a5..7abb32b7 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -47,12 +47,6 @@ import { MouseZoneManager } from './input/MouseZoneManager'; import { initialize as initializeCharAtlas } from './renderer/CharAtlas'; import { IRenderer } from './renderer/Interfaces'; -// Declares required for loadAddon -declare var exports: any; -declare var module: any; -declare var define: any; -declare var require: any; - // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -322,7 +316,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT * Focus the terminal. Delegates focus handling to the terminal's DOM element. */ public focus(): void { - this.textarea.focus(); + if (this.textarea) { + this.textarea.focus(); + } } public get isFocused(): boolean { @@ -584,19 +580,19 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.element = this.document.createElement('div'); this.element.classList.add('terminal'); this.element.classList.add('xterm'); - this.element.setAttribute('tabindex', '0'); + this.parent.appendChild(this.element); + // Performance: Use a document fragment to build the terminal + // viewport and helper elements detached from the DOM + const fragment = document.createDocumentFragment(); this.viewportElement = document.createElement('div'); this.viewportElement.classList.add('xterm-viewport'); - this.element.appendChild(this.viewportElement); + fragment.appendChild(this.viewportElement); this.viewportScrollArea = document.createElement('div'); this.viewportScrollArea.classList.add('xterm-scroll-area'); this.viewportElement.appendChild(this.viewportScrollArea); - // preload audio - this.syncBellSound(); - this._mouseZoneManager = new MouseZoneManager(this); this.on('scroll', () => this._mouseZoneManager.clearAll()); this.linkifier.attachToDom(this._mouseZoneManager); @@ -605,8 +601,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // capturing DOM Events. Then produce the helpers. this.helperContainer = document.createElement('div'); this.helperContainer.classList.add('xterm-helpers'); - // TODO: This should probably be inserted once it's filled to prevent an additional layout - this.element.appendChild(this.helperContainer); + fragment.appendChild(this.helperContainer); + this.textarea = document.createElement('textarea'); this.textarea.classList.add('xterm-helper-textarea'); this.textarea.setAttribute('autocorrect', 'off'); @@ -624,11 +620,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.charSizeStyleElement = document.createElement('style'); this.helperContainer.appendChild(this.charSizeStyleElement); - - this.parent.appendChild(this.element); - this.charMeasure = new CharMeasure(document, this.helperContainer); + // Preload audio, this relied on helperContainer + this.syncBellSound(); + + // Performance: Add viewport and helper elements from the fragment + this.element.appendChild(fragment); + this.renderer = new Renderer(this, this.options.theme); this.options.theme = null; this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); @@ -673,6 +672,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Listen for mouse events and translate // them into terminal mouse protocols. this.bindMouse(); + } /** @@ -687,22 +687,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } /** - * Attempts to load an add-on using CommonJS or RequireJS (whichever is available). - * @param {string} addon The name of the addon to load - * @static + * Apply the provided addon on the `Terminal` class. + * @param addon The addon to apply. */ - public static loadAddon(addon: string, callback?: Function): boolean | any { - // TODO: Improve return type and documentation - if (typeof exports === 'object' && typeof module === 'object') { - // CommonJS - return require('./addons/' + addon + '/' + addon); - } else if (typeof define === 'function') { - // RequireJS - return (require)(['./addons/' + addon + '/' + addon], callback); - } else { - console.error('Cannot load a module without a CommonJS or RequireJS environment.'); - return false; - } + public static applyAddon(addon: any): void { + addon.apply(Terminal); } /** @@ -1460,6 +1449,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT if (ev.shiftKey) { result.key = C0.BS; // ^H break; + } else if (ev.altKey) { + result.key = C0.ESC + C0.DEL; // \e ^? + break; } result.key = C0.DEL; // ^? break; @@ -2127,6 +2119,11 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT } private syncBellSound(): void { + // Don't update anything if the terminal has not been opened yet + if (!this.element) { + return; + } + if (this.soundBell() && this.bellAudioElement) { this.bellAudioElement.setAttribute('src', this.options.bellSound); } else if (this.soundBell()) { diff --git a/src/addons/attach/attach.js b/src/addons/attach/attach.js deleted file mode 100644 index 712d911d..00000000 --- a/src/addons/attach/attach.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Copyright (c) 2014 The xterm.js authors. All rights reserved. - * @license MIT - * - * Implements the attach method, that attaches the terminal to a WebSocket stream. - */ - -(function (attach) { - if (typeof exports === 'object' && typeof module === 'object') { - /* - * CommonJS environment - */ - module.exports = attach(require('../../Terminal').Terminal); - } else if (typeof define == 'function') { - /* - * Require.js is available - */ - define(['../../xterm'], attach); - } else { - /* - * Plain browser environment - */ - attach(window.Terminal); - } -})(function (Terminal) { - 'use strict'; - - var exports = {}; - - /** - * Attaches the given terminal to the given socket. - * - * @param {Terminal} term - The terminal to be attached to the given socket. - * @param {WebSocket} socket - The socket to attach the current terminal. - * @param {boolean} bidirectional - Whether the terminal should send data - * to the socket as well. - * @param {boolean} buffered - Whether the rendering of incoming data - * should happen instantly or at a maximum - * frequency of 1 rendering per 10ms. - */ - exports.attach = function (term, socket, bidirectional, buffered) { - bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional; - term.socket = socket; - - term._flushBuffer = function () { - term.write(term._attachSocketBuffer); - term._attachSocketBuffer = null; - }; - - term._pushToBuffer = function (data) { - if (term._attachSocketBuffer) { - term._attachSocketBuffer += data; - } else { - term._attachSocketBuffer = data; - setTimeout(term._flushBuffer, 10); - } - }; - - var myTextDecoder; - - term._getMessage = function (ev) { - var str; - if (typeof ev.data === "object") { - if (ev.data instanceof ArrayBuffer) { - if (!myTextDecoder) { - myTextDecoder = new TextDecoder(); - } - - str = myTextDecoder.decode( ev.data ); - } - else { - throw "TODO: handle Blob?"; - } - } - - if (buffered) { - term._pushToBuffer(str || ev.data); - } else { - term.write(str || ev.data); - } - }; - - term._sendData = function (data) { - socket.send(data); - }; - - socket.addEventListener('message', term._getMessage); - - if (bidirectional) { - term.on('data', term._sendData); - } - - socket.addEventListener('close', term.detach.bind(term, socket)); - socket.addEventListener('error', term.detach.bind(term, socket)); - }; - - /** - * Detaches the given terminal from the given socket - * - * @param {Terminal} term - The terminal to be detached from the given socket. - * @param {WebSocket} socket - The socket from which to detach the current - * terminal. - */ - exports.detach = function (term, socket) { - term.off('data', term._sendData); - - socket = (typeof socket == 'undefined') ? term.socket : socket; - - if (socket) { - socket.removeEventListener('message', term._getMessage); - } - - delete term.socket; - }; - - /** - * Attaches the current terminal to the given socket - * - * @param {WebSocket} socket - The socket to attach the current terminal. - * @param {boolean} bidirectional - Whether the terminal should send data - * to the socket as well. - * @param {boolean} buffered - Whether the rendering of incoming data - * should happen instantly or at a maximum - * frequency of 1 rendering per 10ms. - */ - Terminal.prototype.attach = function (socket, bidirectional, buffered) { - return exports.attach(this, socket, bidirectional, buffered); - }; - - /** - * Detaches the current terminal from the given socket. - * - * @param {WebSocket} socket - The socket from which to detach the current - * terminal. - */ - Terminal.prototype.detach = function (socket) { - return exports.detach(this, socket); - }; - - return exports; -}); diff --git a/src/addons/attach/attach.test.ts b/src/addons/attach/attach.test.ts new file mode 100644 index 00000000..9bf0e070 --- /dev/null +++ b/src/addons/attach/attach.test.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert, expect } from 'chai'; + +import * as attach from './attach' + +class MockTerminal {} + +describe('attach addon', () => { + describe('apply', () => { + it('should do register the `attach` and `detach` methods', () => { + attach.apply(MockTerminal); + assert.equal(typeof (MockTerminal).prototype.attach, 'function'); + assert.equal(typeof (MockTerminal).prototype.detach, 'function'); + }); + }); +}); diff --git a/src/addons/attach/attach.ts b/src/addons/attach/attach.ts new file mode 100644 index 00000000..4e0d5941 --- /dev/null +++ b/src/addons/attach/attach.ts @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * @license MIT + * + * Implements the attach method, that attaches the terminal to a WebSocket stream. + */ + +/** + * Attaches the given terminal to the given socket. + * + * @param {Terminal} term - The terminal to be attached to the given socket. + * @param {WebSocket} socket - The socket to attach the current terminal. + * @param {boolean} bidirectional - Whether the terminal should send data + * to the socket as well. + * @param {boolean} buffered - Whether the rendering of incoming data + * should happen instantly or at a maximum + * frequency of 1 rendering per 10ms. + */ +export function attach(term, socket, bidirectional, buffered) { + bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional; + term.socket = socket; + + term._flushBuffer = function() { + term.write(term._attachSocketBuffer); + term._attachSocketBuffer = null; + }; + + term._pushToBuffer = function(data) { + if (term._attachSocketBuffer) { + term._attachSocketBuffer += data; + } else { + term._attachSocketBuffer = data; + setTimeout(term._flushBuffer, 10); + } + }; + + var myTextDecoder; + + term._getMessage = function(ev) { + var str; + if (typeof ev.data === "object") { + if (ev.data instanceof ArrayBuffer) { + if (!myTextDecoder) { + myTextDecoder = new TextDecoder(); + } + + str = myTextDecoder.decode( ev.data ); + } + else { + throw "TODO: handle Blob?"; + } + } + + if (buffered) { + term._pushToBuffer(str || ev.data); + } else { + term.write(str || ev.data); + } + }; + + term._sendData = function(data) { + if (socket.readyState !== 1) { + return; + } + socket.send(data); + }; + + socket.addEventListener('message', term._getMessage); + + if (bidirectional) { + term.on('data', term._sendData); + } + + socket.addEventListener('close', term.detach.bind(term, socket)); + socket.addEventListener('error', term.detach.bind(term, socket)); +}; + +/** + * Detaches the given terminal from the given socket + * + * @param {Terminal} term - The terminal to be detached from the given socket. + * @param {WebSocket} socket - The socket from which to detach the current + * terminal. + */ +export function detach(term, socket) { + term.off('data', term._sendData); + + socket = (typeof socket == 'undefined') ? term.socket : socket; + + if (socket) { + socket.removeEventListener('message', term._getMessage); + } + + delete term.socket; +}; + + +export function apply(terminalConstructor) { + /** + * Attaches the current terminal to the given socket + * + * @param {WebSocket} socket - The socket to attach the current terminal. + * @param {boolean} bidirectional - Whether the terminal should send data + * to the socket as well. + * @param {boolean} buffered - Whether the rendering of incoming data + * should happen instantly or at a maximum + * frequency of 1 rendering per 10ms. + */ + terminalConstructor.prototype.attach = function(socket, bidirectional, buffered) { + return attach(this, socket, bidirectional, buffered); + }; + + /** + * Detaches the current terminal from the given socket. + * + * @param {WebSocket} socket - The socket from which to detach the current + * terminal. + */ + terminalConstructor.prototype.detach = function(socket) { + return detach(this, socket); + }; +} diff --git a/src/addons/attach/tsconfig.json b/src/addons/attach/tsconfig.json new file mode 100644 index 00000000..97722324 --- /dev/null +++ b/src/addons/attach/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "rootDir": ".", + "outDir": "../../../lib/addons/attach/", + "sourceMap": true, + "removeComments": true + } +} diff --git a/src/addons/fit/fit.js b/src/addons/fit/fit.js deleted file mode 100644 index c82039ab..00000000 --- a/src/addons/fit/fit.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright (c) 2014 The xterm.js authors. All rights reserved. - * @license MIT - * - * Fit terminal columns and rows to the dimensions of its DOM element. - * - * ## Approach - * - * Rows: Truncate the division of the terminal parent element height by the - * terminal row height. - * Columns: Truncate the division of the terminal parent element width by the - * terminal character width (apply display: inline at the terminal - * row and truncate its width with the current number of columns). - */ - -(function (fit) { - if (typeof exports === 'object' && typeof module === 'object') { - /* - * CommonJS environment - */ - module.exports = fit(require('../../Terminal').Terminal); - } else if (typeof define == 'function') { - /* - * Require.js is available - */ - define(['../../xterm'], fit); - } else { - /* - * Plain browser environment - */ - fit(window.Terminal); - } -})(function (Terminal) { - var exports = {}; - - exports.proposeGeometry = function (term) { - if (!term.element.parentElement) { - return null; - } - var parentElementStyle = window.getComputedStyle(term.element.parentElement); - var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); - var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17); - var elementStyle = window.getComputedStyle(term.element); - var elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')); - var elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')); - var availableHeight = parentElementHeight - elementPaddingVer; - var availableWidth = parentElementWidth - elementPaddingHor; - var geometry = { - cols: Math.floor(availableWidth / term.charMeasure.width), - rows: Math.floor(availableHeight / Math.floor(term.charMeasure.height * term.getOption('lineHeight'))) - }; - - return geometry; - }; - - exports.fit = function (term) { - // Wrap fit in a setTimeout as charMeasure needs time to get initialized - // after calling Terminal.open - setTimeout(function () { - var geometry = exports.proposeGeometry(term); - - if (geometry) { - // Force a full render - if (term.rows !== geometry.rows || term.cols !== geometry.cols) { - term.renderer.clear(); - term.resize(geometry.cols, geometry.rows); - } - } - }, 0); - }; - - Terminal.prototype.proposeGeometry = function () { - return exports.proposeGeometry(this); - }; - - Terminal.prototype.fit = function () { - return exports.fit(this); - }; - - return exports; -}); diff --git a/src/addons/fit/fit.test.ts b/src/addons/fit/fit.test.ts new file mode 100644 index 00000000..159cc987 --- /dev/null +++ b/src/addons/fit/fit.test.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert, expect } from 'chai'; + +import * as fit from './fit' + +class MockTerminal {} + +describe('fit addon', () => { + describe('apply', () => { + it('should do register the `proposeGeometry` and `fit` methods', () => { + fit.apply(MockTerminal); + assert.equal(typeof (MockTerminal).prototype.proposeGeometry, 'function'); + assert.equal(typeof (MockTerminal).prototype.fit, 'function'); + }); + }); +}); diff --git a/src/addons/fit/fit.ts b/src/addons/fit/fit.ts new file mode 100644 index 00000000..b87ccc07 --- /dev/null +++ b/src/addons/fit/fit.ts @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * @license MIT + * + * Fit terminal columns and rows to the dimensions of its DOM element. + * + * ## Approach + * + * Rows: Truncate the division of the terminal parent element height by the + * terminal row height. + * Columns: Truncate the division of the terminal parent element width by the + * terminal character width (apply display: inline at the terminal + * row and truncate its width with the current number of columns). + */ + +export function proposeGeometry(term) { + if (!term.element.parentElement) { + return null; + } + var parentElementStyle = window.getComputedStyle(term.element.parentElement); + var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')); + var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17); + var elementStyle = window.getComputedStyle(term.element); + var elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom')); + var elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left')); + var availableHeight = parentElementHeight - elementPaddingVer; + var availableWidth = parentElementWidth - elementPaddingHor; + var geometry = { + cols: Math.floor(availableWidth / term.renderer.dimensions.actualCellWidth), + rows: Math.floor(availableHeight / term.renderer.dimensions.actualCellHeight) + }; + + return geometry; +}; + +export function fit(term) { + var geometry = proposeGeometry(term); + if (geometry) { + // Force a full render + if (term.rows !== geometry.rows || term.cols !== geometry.cols) { + term.renderer.clear(); + term.resize(geometry.cols, geometry.rows); + } + } +}; + +export function apply(terminalConstructor) { + terminalConstructor.prototype.proposeGeometry = function() { + return proposeGeometry(this); + } + + terminalConstructor.prototype.fit = function() { + return fit(this); + } +} diff --git a/src/addons/fit/tsconfig.json b/src/addons/fit/tsconfig.json new file mode 100644 index 00000000..949f8b60 --- /dev/null +++ b/src/addons/fit/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "rootDir": ".", + "outDir": "../../../lib/addons/fit/", + "sourceMap": true, + "removeComments": true + } +} diff --git a/src/addons/fullscreen/fullscreen.js b/src/addons/fullscreen/fullscreen.js deleted file mode 100644 index 1474d2db..00000000 --- a/src/addons/fullscreen/fullscreen.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) 2014 The xterm.js authors. All rights reserved. - * @license MIT - */ - -(function (fullscreen) { - if (typeof exports === 'object' && typeof module === 'object') { - /* - * CommonJS environment - */ - module.exports = fullscreen(require('../../Terminal').Terminal); - } else if (typeof define == 'function') { - /* - * Require.js is available - */ - define(['../../xterm'], fullscreen); - } else { - /* - * Plain browser environment - */ - fullscreen(window.Terminal); - } -})(function (Terminal) { - var exports = {}; - - /** - * Toggle the given terminal's fullscreen mode. - * @param {Terminal} term - The terminal to toggle full screen mode - * @param {boolean} fullscreen - Toggle fullscreen on (true) or off (false) - */ - exports.toggleFullScreen = function (term, fullscreen) { - var fn; - - if (typeof fullscreen == 'undefined') { - fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add'; - } else if (!fullscreen) { - fn = 'remove'; - } else { - fn = 'add'; - } - - term.element.classList[fn]('fullscreen'); - }; - - Terminal.prototype.toggleFullscreen = function (fullscreen) { - exports.toggleFullScreen(this, fullscreen); - }; - - return exports; -}); diff --git a/src/addons/fullscreen/fullscreen.test.ts b/src/addons/fullscreen/fullscreen.test.ts new file mode 100644 index 00000000..c77ea75c --- /dev/null +++ b/src/addons/fullscreen/fullscreen.test.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert, expect } from 'chai'; + +import * as fullscreen from './fullscreen' + +class MockTerminal {} + +describe('fullscreen addon', () => { + describe('apply', () => { + it('should do register the `toggleFullscreen` method', () => { + fullscreen.apply(MockTerminal); + assert.equal(typeof (MockTerminal).prototype.toggleFullScreen, 'function'); + }); + }); +}); diff --git a/src/addons/fullscreen/fullscreen.ts b/src/addons/fullscreen/fullscreen.ts new file mode 100644 index 00000000..31ab5189 --- /dev/null +++ b/src/addons/fullscreen/fullscreen.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * @license MIT + */ + +/** + * Toggle the given terminal's fullscreen mode. + * @param {Terminal} term - The terminal to toggle full screen mode + * @param {boolean} fullscreen - Toggle fullscreen on (true) or off (false) + */ +export function toggleFullScreen(term, fullscreen) { + var fn; + + if (typeof fullscreen == 'undefined') { + fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add'; + } else if (!fullscreen) { + fn = 'remove'; + } else { + fn = 'add'; + } + + term.element.classList[fn]('fullscreen'); +}; + +export function apply(terminalConstructor) { + terminalConstructor.prototype.toggleFullScreen = function (fullscreen) { + return toggleFullScreen(this, fullscreen); + }; +} diff --git a/src/addons/fullscreen/tsconfig.json b/src/addons/fullscreen/tsconfig.json new file mode 100644 index 00000000..b04144c1 --- /dev/null +++ b/src/addons/fullscreen/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "rootDir": ".", + "outDir": "../../../lib/addons/fullscreen/", + "sourceMap": true, + "removeComments": true + } +} diff --git a/src/addons/search/package.json b/src/addons/search/package.json new file mode 100644 index 00000000..552e9332 --- /dev/null +++ b/src/addons/search/package.json @@ -0,0 +1,5 @@ +{ + "name": "xterm.search", + "main": "search.js", + "private": true +} diff --git a/src/addons/search/search.ts b/src/addons/search/search.ts index 69c5737b..480fea7f 100644 --- a/src/addons/search/search.ts +++ b/src/addons/search/search.ts @@ -5,53 +5,39 @@ import { SearchHelper } from './SearchHelper'; -declare var exports: any; -declare var module: any; -declare var define: any; -declare var require: any; -declare var window: any; -(function (addon) { - if (typeof window !== 'undefined' && 'Terminal' in window) { - /** - * Plain browser environment - */ - addon(window.Terminal); - } else if (typeof exports === 'object' && typeof module === 'object') { - /** - * CommonJS environment - */ - module.exports = addon(require('../../Terminal').Terminal); - } else if (typeof define === 'function') { - /** - * Require.js is available - */ - define(['../../xterm'], addon); +/** + * Find the next instance of the term, then scroll to and select it. If it + * doesn't exist, do nothing. + * @param term Tne search term. + * @return Whether a result was found. + */ +export function findNext(terminal: any, term: string): boolean { + if (!terminal._searchHelper) { + terminal.searchHelper = new SearchHelper(terminal); } -})((Terminal: any) => { - /** - * Find the next instance of the term, then scroll to and select it. If it - * doesn't exist, do nothing. - * @param term Tne search term. - * @return Whether a result was found. - */ - Terminal.prototype.findNext = function(term: string): boolean { - if (!this._searchHelper) { - this.searchHelper = new SearchHelper(this); - } - return (this.searchHelper).findNext(term); - }; + return (terminal.searchHelper).findNext(term); +}; - /** - * Find the previous instance of the term, then scroll to and select it. If it - * doesn't exist, do nothing. - * @param term Tne search term. - * @return Whether a result was found. - */ - Terminal.prototype.findPrevious = function(term: string): boolean { - if (!this._searchHelper) { - this.searchHelper = new SearchHelper(this); - } - return (this.searchHelper).findPrevious(term); - }; -}); +/** + * Find the previous instance of the term, then scroll to and select it. If it + * doesn't exist, do nothing. + * @param term Tne search term. + * @return Whether a result was found. + */ +export function findPrevious(terminal: any, term: string): boolean { + if (!terminal._searchHelper) { + terminal.searchHelper = new SearchHelper(terminal); + } + return (terminal.searchHelper).findPrevious(term); +}; + +export function apply(terminalConstructor) { + terminalConstructor.prototype.findNext = function(term) { + return findNext(this, term); + } + + terminalConstructor.prototype.findPrevious = function(term) { + return findPrevious(this, term); + } +} diff --git a/src/addons/terminado/terminado.js b/src/addons/terminado/terminado.js deleted file mode 100644 index 4cf08227..00000000 --- a/src/addons/terminado/terminado.js +++ /dev/null @@ -1,134 +0,0 @@ -/** - * Copyright (c) 2016 The xterm.js authors. All rights reserved. - * @license MIT - * - * This module provides methods for attaching a terminal to a terminado - * WebSocket stream. - */ - -(function (attach) { - if (typeof exports === 'object' && typeof module === 'object') { - /* - * CommonJS environment - */ - module.exports = attach(require('../../Terminal').Terminal); - } else if (typeof define == 'function') { - /* - * Require.js is available - */ - define(['../../xterm'], attach); - } else { - /* - * Plain browser environment - */ - attach(window.Terminal); - } -})(function (Terminal) { - 'use strict'; - - var exports = {}; - - /** - * Attaches the given terminal to the given socket. - * - * @param {Terminal} term - The terminal to be attached to the given socket. - * @param {WebSocket} socket - The socket to attach the current terminal. - * @param {boolean} bidirectional - Whether the terminal should send data - * to the socket as well. - * @param {boolean} buffered - Whether the rendering of incoming data - * should happen instantly or at a maximum - * frequency of 1 rendering per 10ms. - */ - exports.terminadoAttach = function (term, socket, bidirectional, buffered) { - bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional; - term.socket = socket; - - term._flushBuffer = function () { - term.write(term._attachSocketBuffer); - term._attachSocketBuffer = null; - }; - - term._pushToBuffer = function (data) { - if (term._attachSocketBuffer) { - term._attachSocketBuffer += data; - } else { - term._attachSocketBuffer = data; - setTimeout(term._flushBuffer, 10); - } - }; - - term._getMessage = function (ev) { - var data = JSON.parse(ev.data) - if( data[0] == "stdout" ) { - if (buffered) { - term._pushToBuffer(data[1]); - } else { - term.write(data[1]); - } - } - }; - - term._sendData = function (data) { - socket.send(JSON.stringify(['stdin', data])); - }; - - term._setSize = function (size) { - socket.send(JSON.stringify(['set_size', size.rows, size.cols])); - }; - - socket.addEventListener('message', term._getMessage); - - if (bidirectional) { - term.on('data', term._sendData); - } - term.on('resize', term._setSize); - - socket.addEventListener('close', term.terminadoDetach.bind(term, socket)); - socket.addEventListener('error', term.terminadoDetach.bind(term, socket)); - }; - - /** - * Detaches the given terminal from the given socket - * - * @param {Xterm} term - The terminal to be detached from the given socket. - * @param {WebSocket} socket - The socket from which to detach the current - * terminal. - */ - exports.terminadoDetach = function (term, socket) { - term.off('data', term._sendData); - - socket = (typeof socket == 'undefined') ? term.socket : socket; - - if (socket) { - socket.removeEventListener('message', term._getMessage); - } - - delete term.socket; - }; - - /** - * Attaches the current terminal to the given socket - * - * @param {WebSocket} socket - The socket to attach the current terminal. - * @param {boolean} bidirectional - Whether the terminal should send data - * to the socket as well. - * @param {boolean} buffered - Whether the rendering of incoming data - * should happen instantly or at a maximum - * frequency of 1 rendering per 10ms. - */ - Terminal.prototype.terminadoAttach = function (socket, bidirectional, buffered) { - return exports.terminadoAttach(this, socket, bidirectional, buffered); - }; - - /** - * Detaches the current terminal from the given socket. - * - * @param {WebSocket} socket - The socket from which to detach the current - * terminal. - */ - Terminal.prototype.terminadoDetach = function (socket) { - return exports.terminadoDetach(this, socket); - }; - - return exports; -}); diff --git a/src/addons/terminado/terminado.test.ts b/src/addons/terminado/terminado.test.ts new file mode 100644 index 00000000..fe059898 --- /dev/null +++ b/src/addons/terminado/terminado.test.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert, expect } from 'chai'; + +import * as terminado from './terminado' + +class MockTerminal {} + +describe('terminado addon', () => { + describe('apply', () => { + it('should do register the `terminadoAttach` and `terminadoDetach` methods', () => { + terminado.apply(MockTerminal); + assert.equal(typeof (MockTerminal).prototype.terminadoAttach, 'function'); + assert.equal(typeof (MockTerminal).prototype.terminadoDetach, 'function'); + }); + }); +}); diff --git a/src/addons/terminado/terminado.ts b/src/addons/terminado/terminado.ts new file mode 100644 index 00000000..9a7c7ad6 --- /dev/null +++ b/src/addons/terminado/terminado.ts @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + * + * This module provides methods for attaching a terminal to a terminado + * WebSocket stream. + */ + +/** + * Attaches the given terminal to the given socket. + * + * @param {Terminal} term - The terminal to be attached to the given socket. + * @param {WebSocket} socket - The socket to attach the current terminal. + * @param {boolean} bidirectional - Whether the terminal should send data + * to the socket as well. + * @param {boolean} buffered - Whether the rendering of incoming data + * should happen instantly or at a maximum + * frequency of 1 rendering per 10ms. + */ +export function terminadoAttach(term, socket, bidirectional, buffered) { + bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional; + term.socket = socket; + + term._flushBuffer = function() { + term.write(term._attachSocketBuffer); + term._attachSocketBuffer = null; + }; + + term._pushToBuffer = function(data) { + if (term._attachSocketBuffer) { + term._attachSocketBuffer += data; + } else { + term._attachSocketBuffer = data; + setTimeout(term._flushBuffer, 10); + } + }; + + term._getMessage = function(ev) { + var data = JSON.parse(ev.data) + if( data[0] == "stdout" ) { + if (buffered) { + term._pushToBuffer(data[1]); + } else { + term.write(data[1]); + } + } + }; + + term._sendData = function(data) { + socket.send(JSON.stringify(['stdin', data])); + }; + + term._setSize = function(size) { + socket.send(JSON.stringify(['set_size', size.rows, size.cols])); + }; + + socket.addEventListener('message', term._getMessage); + + if (bidirectional) { + term.on('data', term._sendData); + } + term.on('resize', term._setSize); + + socket.addEventListener('close', term.terminadoDetach.bind(term, socket)); + socket.addEventListener('error', term.terminadoDetach.bind(term, socket)); +}; + +/** + * Detaches the given terminal from the given socket + * + * @param {Xterm} term - The terminal to be detached from the given socket. + * @param {WebSocket} socket - The socket from which to detach the current + * terminal. + */ +export function terminadoDetach(term, socket) { + term.off('data', term._sendData); + + socket = (typeof socket == 'undefined') ? term.socket : socket; + + if (socket) { + socket.removeEventListener('message', term._getMessage); + } + + delete term.socket; +}; + +export function apply(terminalConstructor) { + /** + * Attaches the current terminal to the given socket + * + * @param {WebSocket} socket - The socket to attach the current terminal. + * @param {boolean} bidirectional - Whether the terminal should send data + * to the socket as well. + * @param {boolean} buffered - Whether the rendering of incoming data + * should happen instantly or at a maximum + * frequency of 1 rendering per 10ms. + */ + terminalConstructor.prototype.terminadoAttach = function(socket, bidirectional, buffered) { + return terminadoAttach(this, socket, bidirectional, buffered); + }; + + /** + * Detaches the current terminal from the given socket. + * + * @param {WebSocket} socket - The socket from which to detach the current + * terminal. + */ + terminalConstructor.prototype.terminadoDetach = function(socket) { + return terminadoDetach(this, socket); + }; +} diff --git a/src/addons/terminado/tsconfig.json b/src/addons/terminado/tsconfig.json new file mode 100644 index 00000000..775b20d5 --- /dev/null +++ b/src/addons/terminado/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "rootDir": ".", + "outDir": "../../../lib/addons/terminado/", + "sourceMap": true, + "removeComments": true + } +} diff --git a/src/addons/winptyCompat/package.json b/src/addons/winptyCompat/package.json new file mode 100644 index 00000000..2e9fc164 --- /dev/null +++ b/src/addons/winptyCompat/package.json @@ -0,0 +1,5 @@ +{ + "name": "xterm.winptyCompat", + "main": "winptyCompat.js", + "private": true +} diff --git a/src/addons/winptyCompat/winptyCompat.test.ts b/src/addons/winptyCompat/winptyCompat.test.ts new file mode 100644 index 00000000..21e54e56 --- /dev/null +++ b/src/addons/winptyCompat/winptyCompat.test.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert, expect } from 'chai'; + +import * as winptyCompat from './winptyCompat' + +class MockTerminal {} + +describe('winptyCompat addon', () => { + describe('apply', () => { + it('should do register the `winptyCompatInit` method', () => { + winptyCompat.apply(MockTerminal); + assert.equal(typeof (MockTerminal).prototype.winptyCompatInit, 'function'); + }); + }); +}); diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index 7474df2e..723bf011 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -3,28 +3,7 @@ * @license MIT */ -declare var exports: any; -declare var define: any; - -(function (addon) { - if (typeof window !== 'undefined' && 'Terminal' in window) { - /** - * Plain browser environment - */ - addon((window).Terminal); - } else if (typeof exports === 'object' && typeof module === 'object') { - /** - * CommonJS environment - */ - module.exports = addon(require('../../Terminal').Terminal); - } else if (typeof define === 'function') { - /** - * Require.js is available - */ - define(['../../xterm'], addon); - } -})((Terminal: any) => { - Terminal.prototype.winptyCompatInit = function(): void { +export function winptyCompatInit(terminal): void { // Don't do anything when the platform is not Windows const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; if (!isWindows) { @@ -41,13 +20,19 @@ declare var define: any; // space. This is certainly not without its problems, but generally on // Windows when text reaches the end of the terminal it's likely going to be // wrapped. - this.on('lineFeed', () => { - const line = this.buffer.lines.get(this.buffer.ybase + this.buffer.y - 1); - const lastChar = line[this.cols - 1]; + terminal.on('linefeed', () => { + const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1); + const lastChar = line[terminal.cols - 1]; + if (lastChar[3] !== 32 /* ' ' */) { - const nextLine = this.buffer.lines.get(this.buffer.ybase + this.buffer.y); + const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y); (nextLine).isWrapped = true; } }); +} + +export function apply(terminalConstructor) { + terminalConstructor.prototype.winptyCompatInit = function(): void { + winptyCompatInit(this); }; -}); +} diff --git a/src/addons/zmodem/package.json b/src/addons/zmodem/package.json new file mode 100644 index 00000000..218130ab --- /dev/null +++ b/src/addons/zmodem/package.json @@ -0,0 +1,5 @@ +{ + "name": "xterm.zmodem", + "main": "zmodem.js", + "private": true +} diff --git a/src/addons/zmodem/tsconfig.json b/src/addons/zmodem/tsconfig.json new file mode 100644 index 00000000..9ded72e5 --- /dev/null +++ b/src/addons/zmodem/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "rootDir": ".", + "outDir": "../../../lib/addons/zmodem/", + "sourceMap": true, + "removeComments": true + } +} diff --git a/src/addons/zmodem/zmodem.js b/src/addons/zmodem/zmodem.js deleted file mode 100644 index 1bab2976..00000000 --- a/src/addons/zmodem/zmodem.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * - * Allow xterm.js to handle ZMODEM uploads and downloads. - * - * This addon is a wrapper around zmodem.js. It adds the following to the - * Terminal class: - * - * - function `zmodemAttach(, )` - creates a Zmodem.Sentry - * on the passed WebSocket object. The Object passed is optional and - * can contain: - * - noTerminalWriteOutsideSession: Suppress writes from the Sentry - * object to the Terminal while there is no active Session. This - * is necessary for compatibility with, for example, the - * `attach.js` addon. - * - * - event `zmodemDetect` - fired on Zmodem.Sentry’s `on_detect` callback. - * Passes the zmodem.js Detection object. - * - * - event `zmodemRetract` - fired on Zmodem.Sentry’s `on_retract` callback. - * - * You’ll need to provide logic to handle uploads and downloads. - * See zmodem.js’s documentation for more details. - * - * **IMPORTANT:** After you confirm() a zmodem.js Detection, if you have - * used the `attach` or `terminado` addons, you’ll need to suspend their - * operation for the duration of the ZMODEM session. (The demo does this - * via `detach()` and a re-`attach()`.) - */ -(function (addon) { - if (typeof exports === 'object' && typeof module === 'object') { - /* - * CommonJS environment - */ - module.exports = addon(require('../../Terminal').Terminal); - } else if (typeof define == 'function') { - /* - * Require.js is available - */ - define(['../../xterm'], addon); - } else { - /* - * Plain browser environment - */ - addon(window.Terminal); - } -})(function _zmodemAddon(Terminal) { - Object.assign( - Terminal.prototype, - { - zmodemAttach: function zmodemAttach(ws, opts) { - var term = this; - - if (!opts) opts = {}; - - var senderFunc = function _ws_sender_func(octets) { - ws.send( new Uint8Array(octets) ); - }; - - var zsentry; - - function _shouldWrite() { - return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession; - } - - zsentry = new Zmodem.Sentry( { - to_terminal: function _to_terminal(octets) { - if (_shouldWrite()) { - term.write( - String.fromCharCode.apply(String, octets) - ); - } - }, - - sender: senderFunc, - - on_retract: function _on_retract() { - term.emit("zmodemRetract"); - }, - - on_detect: function _on_detect(detection) { - term.emit("zmodemDetect", detection); - }, - } ); - - function handleWSMessage(evt) { - - //In testing with xterm.js’s demo the first message was - //always text even if the rest were binary. While that - //may be specific to xterm.js’s demo, ultimately we - //should reject anything that isn’t binary. - if (typeof evt.data === "string") { - if (_shouldWrite()) { - term.write(evt.data); - } - } - else { - zsentry.consume(evt.data); - } - } - - ws.binaryType = "arraybuffer"; - ws.addEventListener("message", handleWSMessage); - }, - - zmodemBrowser: Zmodem.Browser, - } - ); -}); diff --git a/src/addons/zmodem/zmodem.test.ts b/src/addons/zmodem/zmodem.test.ts new file mode 100644 index 00000000..3cb28316 --- /dev/null +++ b/src/addons/zmodem/zmodem.test.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert, expect } from 'chai'; + +import * as zmodem from './zmodem' + +class MockTerminal {} + +describe('zmodem addon', () => { + describe('apply', () => { + it('should do register the `zmodemAttach` method and `zmodemBrowser` attribute', () => { + zmodem.apply(MockTerminal); + assert.equal(typeof (MockTerminal).prototype.zmodemAttach, 'function'); + assert.equal(typeof (MockTerminal).prototype.zmodemBrowser, 'object'); + }); + }); +}); diff --git a/src/addons/zmodem/zmodem.ts b/src/addons/zmodem/zmodem.ts new file mode 100644 index 00000000..57bdde3c --- /dev/null +++ b/src/addons/zmodem/zmodem.ts @@ -0,0 +1,90 @@ +/** + * + * Allow xterm.js to handle ZMODEM uploads and downloads. + * + * This addon is a wrapper around zmodem.js. It adds the following to the + * Terminal class: + * + * - function `zmodemAttach(, )` - creates a Zmodem.Sentry + * on the passed WebSocket object. The Object passed is optional and + * can contain: + * - noTerminalWriteOutsideSession: Suppress writes from the Sentry + * object to the Terminal while there is no active Session. This + * is necessary for compatibility with, for example, the + * `attach.js` addon. + * + * - event `zmodemDetect` - fired on Zmodem.Sentry’s `on_detect` callback. + * Passes the zmodem.js Detection object. + * + * - event `zmodemRetract` - fired on Zmodem.Sentry’s `on_retract` callback. + * + * You’ll need to provide logic to handle uploads and downloads. + * See zmodem.js’s documentation for more details. + * + * **IMPORTANT:** After you confirm() a zmodem.js Detection, if you have + * used the `attach` or `terminado` addons, you’ll need to suspend their + * operation for the duration of the ZMODEM session. (The demo does this + * via `detach()` and a re-`attach()`.) + */ + +let Zmodem; + +export function zmodemAttach(term, ws, opts) { + if (!opts) opts = {}; + + var senderFunc = function _ws_sender_func(octets) { + ws.send(new Uint8Array(octets)); + }; + + var zsentry; + + function _shouldWrite() { + return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession; + } + + zsentry = new Zmodem.Sentry( { + to_terminal: function _to_terminal(octets) { + if (_shouldWrite()) { + term.write( + String.fromCharCode.apply(String, octets) + ); + } + }, + + sender: senderFunc, + + on_retract: function _on_retract() { + term.emit('zmodemRetract'); + }, + + on_detect: function _on_detect(detection) { + term.emit('zmodemDetect', detection); + }, + } ); + + function handleWSMessage(evt) { + + //In testing with xterm.js’s demo the first message was + //always text even if the rest were binary. While that + //may be specific to xterm.js’s demo, ultimately we + //should reject anything that isn’t binary. + if (typeof evt.data === 'string') { + if (_shouldWrite()) { + term.write(evt.data); + } + } + else { + zsentry.consume(evt.data); + } + } + + ws.binaryType = 'arraybuffer'; + ws.addEventListener('message', handleWSMessage); +} + +export function apply(terminalConstructor) { + Zmodem = (typeof window == 'object') ? (window).ZModem : {Browser: null}; // Nullify browser for tests + + terminalConstructor.prototype.zmodemAttach = zmodemAttach.bind(this, this); + terminalConstructor.prototype.zmodemBrowser = Zmodem.Browser +} diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index ea373e5e..36727eac 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -52,6 +52,7 @@ export class Renderer extends EventEmitter implements IRenderer { actualCellHeight: null }; this._devicePixelRatio = window.devicePixelRatio; + this._updateDimensions(); } public onWindowResize(devicePixelRatio: number): void { @@ -78,59 +79,8 @@ export class Renderer extends EventEmitter implements IRenderer { } public onResize(cols: number, rows: number, didCharSizeChange: boolean): void { - if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) { - return; - } - - // Calculate the scaled character width. Width is floored as it must be - // drawn to an integer grid in order for the CharAtlas "stamps" to not be - // blurry. When text is drawn to the grid not using the CharAtlas, it is - // clipped to ensure there is no overlap with the next cell. - this.dimensions.scaledCharWidth = Math.floor(this._terminal.charMeasure.width * window.devicePixelRatio); - - // Calculate the scaled character height. Height is ceiled in case - // devicePixelRatio is a floating point number in order to ensure there is - // enough space to draw the character to the cell. - this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio); - - // Calculate the scaled cell height, if lineHeight is not 1 then the value - // will be floored because since lineHeight can never be lower then 1, there - // is a guarentee that the scaled line height will always be larger than - // scaled char height. - this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); - - // Calculate the y coordinate within a cell that text should draw from in - // order to draw in the center of a cell. - this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); - - // Calculate the scaled cell width, taking the letterSpacing into account. - this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); - - // Calculate the x coordinate with a cell that text should draw from in - // order to draw in the center of a cell. - this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2); - - // Recalculate the canvas dimensions; scaled* define the actual number of - // pixel in the canvas - this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight; - this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth; - - // The the size of the canvas on the page. It's very important that this - // rounds to nearest integer and not ceils as browsers often set - // window.devicePixelRatio as something like 1.100000023841858, when it's - // actually 1.1. Ceiling causes blurriness as the backing canvas image is 1 - // pixel too large for the canvas element size. - this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio); - this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio); - - // Get the _actual_ dimensions of an individual cell. This needs to be - // derived from the canvasWidth/Height calculated above which takes into - // account window.devicePixelRatio. CharMeasure.width/height by itself is - // insufficient when the page is not at 100% zoom level as CharMeasure is - // measured in CSS pixels, but the actual char size on the canvas can - // differ. - this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; - this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; + // Update character and canvas dimensions + this._updateDimensions(); // Resize all render layers this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange)); @@ -218,4 +168,66 @@ export class Renderer extends EventEmitter implements IRenderer { this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end)); this._terminal.emit('refresh', {start, end}); } + + /** + * Recalculates the character and canvas dimensions. + */ + private _updateDimensions(): void { + // Perform a new measure if the CharMeasure dimensions are not yet available + if (!this._terminal.charMeasure.width || !this._terminal.charMeasure.height) { + return; + } + + // Calculate the scaled character width. Width is floored as it must be + // drawn to an integer grid in order for the CharAtlas "stamps" to not be + // blurry. When text is drawn to the grid not using the CharAtlas, it is + // clipped to ensure there is no overlap with the next cell. + this.dimensions.scaledCharWidth = Math.floor(this._terminal.charMeasure.width * window.devicePixelRatio); + + // Calculate the scaled character height. Height is ceiled in case + // devicePixelRatio is a floating point number in order to ensure there is + // enough space to draw the character to the cell. + this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio); + + // Calculate the scaled cell height, if lineHeight is not 1 then the value + // will be floored because since lineHeight can never be lower then 1, there + // is a guarentee that the scaled line height will always be larger than + // scaled char height. + this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); + + // Calculate the y coordinate within a cell that text should draw from in + // order to draw in the center of a cell. + this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); + + // Calculate the scaled cell width, taking the letterSpacing into account. + this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); + + // Calculate the x coordinate with a cell that text should draw from in + // order to draw in the center of a cell. + this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2); + + // Recalculate the canvas dimensions; scaled* define the actual number of + // pixel in the canvas + this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight; + this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth; + + // The the size of the canvas on the page. It's very important that this + // rounds to nearest integer and not ceils as browsers often set + // window.devicePixelRatio as something like 1.100000023841858, when it's + // actually 1.1. Ceiling causes blurriness as the backing canvas image is 1 + // pixel too large for the canvas element size. + this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio); + this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio); + + // Get the _actual_ dimensions of an individual cell. This needs to be + // derived from the canvasWidth/Height calculated above which takes into + // account window.devicePixelRatio. CharMeasure.width/height by itself is + // insufficient when the page is not at 100% zoom level as CharMeasure is + // measured in CSS pixels, but the actual char size on the canvas can + // differ. + this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; + this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; + + } + } diff --git a/src/utils/CharMeasure.test.ts b/src/utils/CharMeasure.test.ts index 5d2358f4..f50ce96b 100644 --- a/src/utils/CharMeasure.test.ts +++ b/src/utils/CharMeasure.test.ts @@ -25,41 +25,18 @@ describe('CharMeasure', () => { }); describe('measure', () => { - it('should set _measureElement on first call', () => { - charMeasure.measure({}); - assert.isDefined((charMeasure)._measureElement, 'CharMeasure.measure should have created _measureElement'); + it('should have _measureElement', () => { + assert.isDefined((charMeasure)._measureElement, 'new CharMeasure() should have created _measureElement'); }); - it('should be performed async on first call', done => { - assert.equal(charMeasure.width, null); - charMeasure.measure({}); + it('should be performed sync', () => { // Mock getBoundingClientRect since jsdom doesn't have a layout engine (charMeasure)._measureElement.getBoundingClientRect = () => { return { width: 1, height: 1 }; }; - assert.equal(charMeasure.width, null); - setTimeout(() => { - assert.equal(charMeasure.width, 1); - done(); - }, 0); - }); - - it('should be performed sync on successive calls', done => { charMeasure.measure({}); - // Mock getBoundingClientRect since jsdom doesn't have a layout engine - (charMeasure)._measureElement.getBoundingClientRect = () => { - return { width: 1, height: 1 }; - }; - setTimeout(() => { - const firstWidth = charMeasure.width; - // Mock getBoundingClientRect since jsdom doesn't have a layout engine - (charMeasure)._measureElement.getBoundingClientRect = () => { - return { width: 2, height: 2 }; - }; - charMeasure.measure({}); - assert.equal(charMeasure.width, firstWidth * 2); - done(); - }, 0); + assert.equal(charMeasure.height, 1); + assert.equal(charMeasure.width, 1); }); it('should NOT do a measure when the parent is hidden', done => { diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts index 779ecbe8..62291ab2 100644 --- a/src/utils/CharMeasure.ts +++ b/src/utils/CharMeasure.ts @@ -22,6 +22,14 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { super(); this._document = document; this._parentElement = parentElement; + this._measureElement = this._document.createElement('span'); + this._measureElement.style.position = 'absolute'; + this._measureElement.style.top = '0'; + this._measureElement.style.left = '-9999em'; + this._measureElement.style.lineHeight = 'normal'; + this._measureElement.textContent = 'W'; + this._measureElement.setAttribute('aria-hidden', 'true'); + this._parentElement.appendChild(this._measureElement); } public get width(): number { @@ -33,24 +41,6 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { } public measure(options: ITerminalOptions): void { - if (!this._measureElement) { - this._measureElement = this._document.createElement('span'); - this._measureElement.style.position = 'absolute'; - this._measureElement.style.top = '0'; - this._measureElement.style.left = '-9999em'; - this._measureElement.style.lineHeight = 'normal'; - this._measureElement.textContent = 'W'; - this._measureElement.setAttribute('aria-hidden', 'true'); - this._parentElement.appendChild(this._measureElement); - // Perform _doMeasure async if the element was just attached as sometimes - // getBoundingClientRect does not return accurate values without this. - setTimeout(() => this._doMeasure(options), 0); - } else { - this._doMeasure(options); - } - } - - private _doMeasure(options: ITerminalOptions): void { this._measureElement.style.fontFamily = options.fontFamily; this._measureElement.style.fontSize = `${options.fontSize}px`; const geometry = this._measureElement.getBoundingClientRect(); @@ -65,4 +55,5 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { this.emit('charsizechanged'); } } + } diff --git a/src/xterm.css b/src/xterm.css index 35e92f63..b3ee50f0 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -95,6 +95,7 @@ /* On OS X this is required in order for the scroll bar to appear fully opaque */ background-color: #000; overflow-y: scroll; + cursor: default; } .xterm canvas { diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 43be69c2..83199e08 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -20,7 +20,7 @@ declare module 'xterm' { /** * The type of the bell notification the terminal will use. */ - bellStyle?: 'none' | 'visual' | 'sound' | 'both'; + bellStyle?: 'none' /*| 'visual'*/ | 'sound' /*| 'both'*/; /** * The number of columns in the terminal. @@ -218,7 +218,7 @@ declare module 'xterm' { * @param type The type of the event. * @param listener The listener. */ - on(type: 'blur' | 'focus' | 'lineFeed' | 'selection', listener: () => void): void; + on(type: 'blur' | 'focus' | 'linefeed' | 'selection', listener: () => void): void; /** * Registers an event listener. * @param type The type of the event. @@ -273,7 +273,7 @@ declare module 'xterm' { * @param type The type of the event. * @param listener The listener. */ - off(type: 'blur' | 'focus' | 'lineFeed' | 'selection' | 'data' | 'key' | 'keypress' | 'keydown' | 'refresh' | 'resize' | 'scroll' | 'title' | string, listener: (...args: any[]) => void): void; + off(type: 'blur' | 'focus' | 'linefeed' | 'selection' | 'data' | 'key' | 'keypress' | 'keydown' | 'refresh' | 'resize' | 'scroll' | 'title' | string, listener: (...args: any[]) => void): void; /** * Resizes the terminal. @@ -500,10 +500,10 @@ declare module 'xterm' { reset(): void /** - * Loads an addon, attaching it to the Terminal prototype and making it - * available to all newly created Terminals. - * @param addon The addon to load. + * Applies an addon to the Terminal prototype, making it available to all + * newly created Terminals. + * @param addon The addon to apply. */ - static loadAddon(addon: 'attach' | 'fit' | 'fullscreen' | 'search' | 'terminado' | 'winptyCompat'): void; + static applyAddon(addon: any): void; } } diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 00000000..0c39626e --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,10 @@ +const path = require('path'); + +module.exports = { + entry: './demo/main.js', + output: { + path: path.resolve(__dirname, 'demo/dist'), + filename: 'bundle.js' + }, + devtool: 'source-map' +};