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/.gitmodules b/.gitmodules new file mode 100644 index 00000000..e69de29b 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 e16d7b87..5a77806f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# [![xterm.js logo](logo.png)](https://xtermjs.org) +# [![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 d61b22f2..5b646299 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,8 @@ namespace methods_core { // no arg t.on('blur', () => {}); t.on('focus', () => {}); - t.on('lineFeed', () => {}); + t.on('linefeed', () => {}); + t.on('selection', () => {}); // args t.on('data', () => {}); t.on('data', (data: string) => console.log(data)); @@ -88,7 +89,8 @@ namespace methods_core { // no arg t.off('blur', () => {}); t.off('focus', () => {}); - t.off('lineFeed', () => {}); + t.off('linefeed', () => {}); + t.off('selection', () => {}); // args t.off('data', () => {}); t.off('data', (data: string) => console.log(data)); @@ -137,7 +139,6 @@ namespace methods_core { const r14: number = t.getOption('rows'); const r15: number = t.getOption('tabStopWidth'); const r16: number = t.getOption('scrollback'); - const r17: [number, number] = t.getOption('geometry'); const r18: (data: string) => void = t.getOption('handler'); const r19: string = t.getOption('bellSound'); const r20: string = t.getOption('bellStyle'); @@ -167,13 +168,12 @@ namespace methods_core { t.setOption('rows', 1); t.setOption('tabStopWidth', 1); t.setOption('scrollback', 1); - t.setOption('geometry', [1, 1]); 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 7e852cf2..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/logo-full.png b/logo-full.png new file mode 100644 index 00000000..53e0827c Binary files /dev/null and b/logo-full.png differ diff --git a/logo.png b/logo.png deleted file mode 100644 index c16ac726..00000000 Binary files a/logo.png and /dev/null differ 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 8d337fc8..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", @@ -51,6 +53,7 @@ "glob": "^7.0.5", "gulp": "3.9.1", "gulp-cli": "^1.2.2", + "gulp-concat": "^2.6.1", "gulp-coveralls": "^0.1.4", "gulp-istanbul": "^1.1.1", "gulp-mocha": "^3.0.1", @@ -66,17 +69,23 @@ "tslint": "^4.0.2", "typescript": "~2.4.0", "vinyl-buffer": "^1.0.0", - "vinyl-source-stream": "^1.1.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", - "dev": "nodemon -e js,ts,css --watch src --watch demo --exec npm start", + "prestart-zmodem": "npm run build", + "start-zmodem": "node build/addons/zmodem/demo/app", "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/CharWidth.ts b/src/CharWidth.ts new file mode 100644 index 00000000..d296b435 --- /dev/null +++ b/src/CharWidth.ts @@ -0,0 +1,161 @@ +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + */ + +export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number { + // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c + // combining characters + const COMBINING_BMP = [ + [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489], + [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2], + [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603], + [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670], + [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED], + [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A], + [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902], + [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D], + [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981], + [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD], + [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C], + [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D], + [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC], + [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD], + [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C], + [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D], + [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0], + [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48], + [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC], + [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD], + [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D], + [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6], + [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E], + [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC], + [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35], + [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E], + [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97], + [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030], + [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039], + [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F], + [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753], + [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD], + [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD], + [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922], + [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B], + [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34], + [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42], + [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF], + [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063], + [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F], + [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B], + [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F], + [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB], + ]; + const COMBINING_HIGH = [ + [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F], + [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169], + [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD], + [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F], + [0xE0100, 0xE01EF] + ]; + // binary search + function bisearch(ucs: number, data: number[][]): boolean { + let min = 0; + let max = data.length - 1; + let mid; + if (ucs < data[0][0] || ucs > data[max][1]) + return false; + while (max >= min) { + mid = (min + max) >> 1; + if (ucs > data[mid][1]) + min = mid + 1; + else if (ucs < data[mid][0]) + max = mid - 1; + else + return true; + } + return false; + } + function wcwidthBMP(ucs: number): number { + // test for 8-bit control characters + if (ucs === 0) + return opts.nul; + if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) + return opts.control; + // binary search in table of non-spacing characters + if (bisearch(ucs, COMBINING_BMP)) + return 0; + // if we arrive here, ucs is not a combining or C0/C1 control character + if (isWideBMP(ucs)) { + return 2; + } + return 1; + } + function isWideBMP(ucs: number): boolean { + return ( + ucs >= 0x1100 && ( + ucs <= 0x115f || // Hangul Jamo init. consonants + ucs === 0x2329 || + ucs === 0x232a || + (ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi + (ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables + (ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs + (ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms + (ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms + (ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms + (ucs >= 0xffe0 && ucs <= 0xffe6))); + } + function wcwidthHigh(ucs: number): 0 | 1 | 2 { + if (bisearch(ucs, COMBINING_HIGH)) + return 0; + if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) { + return 2; + } + return 1; + } + const control = opts.control | 0; + let table: number[] | Uint32Array = null; + function init_table(): number[] | Uint32Array { + // lookup table for BMP + const CODEPOINTS = 65536; // BMP holds 65536 codepoints + const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2 + const ITEMSIZE = 32; // using uint32_t + const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE; + const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH; + table = (typeof Uint32Array === 'undefined') + ? new Array(CONTAINERSIZE) + : new Uint32Array(CONTAINERSIZE); + for (let i = 0; i < CONTAINERSIZE; ++i) { + let num = 0; + let pos = CODEPOINTS_PER_ITEM; + while (pos--) + num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos); + table[i] = num; + } + return table; + } + // get width from lookup table + // position in container : num / CODEPOINTS_PER_ITEM + // ==> n = table[Math.floor(num / 16)] + // ==> n = table[num >> 4] + // 16 codepoints per number: FFEEDDCCBBAA99887766554433221100 + // position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH + // ==> m = (n % 16) * 2 + // ==> m = (num & 15) << 1 + // right shift to position m + // ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766 + // we are only interested in 2 LSBs, cut off higher bits + // ==> n = n & 3 e.g. 000000000000000000000000000000XX + return function (num: number): number { + num = num | 0; // get asm.js like optimization under V8 + if (num < 32) + return control | 0; + if (num < 127) + return 1; + let t = table || init_table(); + if (num < 65536) + return t[num >> 4] >> ((num & 15) << 1) & 3; + // do a full search for high codepoints + return wcwidthHigh(num); + }; +})({nul: 0, control: 0}); // configurable options diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index caf74624..eb7b70da 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -5,7 +5,6 @@ import { assert } from 'chai'; import { InputHandler } from './InputHandler'; -import { wcwidth } from './InputHandler'; import { MockInputHandlingTerminal } from './utils/TestUtils.test'; describe('InputHandler', () => { @@ -66,4 +65,18 @@ describe('InputHandler', () => { assert.equal(terminal.options['cursorBlink'], false); }); }); + describe('setMode', () => { + it('should toggle Terminal.bracketedPasteMode', () => { + let terminal = new MockInputHandlingTerminal(); + terminal.prefix = '?'; + terminal.bracketedPasteMode = false; + let inputHandler = new InputHandler(terminal); + // Set bracketed paste mode + inputHandler.setMode([2004]); + assert.equal(terminal.bracketedPasteMode, true); + // Reset bracketed paste mode + inputHandler.resetMode([2004]); + assert.equal(terminal.bracketedPasteMode, false); + }); + }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 10a31e34..1ba6dc7a 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -10,6 +10,7 @@ import { DEFAULT_CHARSET } from './Charsets'; import { CharData } from './Types'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; import { FLAGS } from './renderer/Types'; +import { wcwidth } from './CharWidth'; /** * The terminal's standard implementation of IInputHandler, this handles all @@ -133,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'); } /** @@ -931,6 +932,9 @@ export class InputHandler implements IInputHandler { this._terminal.viewport.syncScrollArea(); this._terminal.showCursor(); break; + case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) + this._terminal.bracketedPasteMode = true; + break; } } } @@ -1101,6 +1105,9 @@ export class InputHandler implements IInputHandler { this._terminal.viewport.syncScrollArea(); this._terminal.showCursor(); break; + case 2004: // bracketed paste mode (https://cirw.in/blog/bracketed-paste) + this._terminal.bracketedPasteMode = false; + break; } } } @@ -1446,160 +1453,3 @@ export class InputHandler implements IInputHandler { this._terminal.buffer.y = this._terminal.buffer.savedY || 0; } } - -export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number { - // extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c - // combining characters - const COMBINING_BMP = [ - [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489], - [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2], - [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603], - [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670], - [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED], - [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A], - [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902], - [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D], - [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981], - [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD], - [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C], - [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D], - [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC], - [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD], - [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C], - [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D], - [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0], - [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48], - [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC], - [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD], - [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D], - [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6], - [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E], - [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC], - [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35], - [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E], - [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97], - [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030], - [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039], - [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F], - [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753], - [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD], - [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD], - [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922], - [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B], - [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34], - [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42], - [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF], - [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063], - [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F], - [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B], - [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F], - [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB], - ]; - const COMBINING_HIGH = [ - [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F], - [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169], - [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD], - [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F], - [0xE0100, 0xE01EF] - ]; - // binary search - function bisearch(ucs: number, data: number[][]): boolean { - let min = 0; - let max = data.length - 1; - let mid; - if (ucs < data[0][0] || ucs > data[max][1]) - return false; - while (max >= min) { - mid = (min + max) >> 1; - if (ucs > data[mid][1]) - min = mid + 1; - else if (ucs < data[mid][0]) - max = mid - 1; - else - return true; - } - return false; - } - function wcwidthBMP(ucs: number): number { - // test for 8-bit control characters - if (ucs === 0) - return opts.nul; - if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) - return opts.control; - // binary search in table of non-spacing characters - if (bisearch(ucs, COMBINING_BMP)) - return 0; - // if we arrive here, ucs is not a combining or C0/C1 control character - if (isWideBMP(ucs)) { - return 2; - } - return 1; - } - function isWideBMP(ucs: number): boolean { - return ( - ucs >= 0x1100 && ( - ucs <= 0x115f || // Hangul Jamo init. consonants - ucs === 0x2329 || - ucs === 0x232a || - (ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi - (ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables - (ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs - (ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms - (ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms - (ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms - (ucs >= 0xffe0 && ucs <= 0xffe6))); - } - function wcwidthHigh(ucs: number): 0 | 1 | 2 { - if (bisearch(ucs, COMBINING_HIGH)) - return 0; - if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) { - return 2; - } - return 1; - } - const control = opts.control | 0; - let table: number[] | Uint32Array = null; - function init_table(): number[] | Uint32Array { - // lookup table for BMP - const CODEPOINTS = 65536; // BMP holds 65536 codepoints - const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2 - const ITEMSIZE = 32; // using uint32_t - const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE; - const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH; - table = (typeof Uint32Array === 'undefined') - ? new Array(CONTAINERSIZE) - : new Uint32Array(CONTAINERSIZE); - for (let i = 0; i < CONTAINERSIZE; ++i) { - let num = 0; - let pos = CODEPOINTS_PER_ITEM; - while (pos--) - num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos); - table[i] = num; - } - return table; - } - // get width from lookup table - // position in container : num / CODEPOINTS_PER_ITEM - // ==> n = table[Math.floor(num / 16)] - // ==> n = table[num >> 4] - // 16 codepoints per number: FFEEDDCCBBAA99887766554433221100 - // position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH - // ==> m = (n % 16) * 2 - // ==> m = (num & 15) << 1 - // right shift to position m - // ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766 - // we are only interested in 2 LSBs, cut off higher bits - // ==> n = n & 3 e.g. 000000000000000000000000000000XX - return function (num: number): number { - num = num | 0; // get asm.js like optimization under V8 - if (num < 32) - return control | 0; - if (num < 127) - return 1; - let t = table || init_table(); - if (num < 65536) - return t[num >> 4] >> ((num & 15) << 1) & 3; - // do a full search for high codepoints - return wcwidthHigh(num); - }; -})({nul: 0, control: 0}); // configurable options diff --git a/src/Interfaces.ts b/src/Interfaces.ts index 39b81baf..5e922f52 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -48,6 +48,7 @@ export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElement buffers: IBufferSet; isFocused: boolean; mouseHelper: IMouseHelper; + bracketedPasteMode: boolean; /** * Emit the 'data' event and populate the given data. @@ -82,6 +83,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { originMode: boolean; insertMode: boolean; wraparoundMode: boolean; + bracketedPasteMode: boolean; defAttr: number; curAttr: number; prefix: string; @@ -138,7 +140,6 @@ export interface ITerminalOptions { enableBold?: boolean; fontSize?: number; fontFamily?: string; - geometry?: [number, number]; handler?: (data: string) => void; letterSpacing?: number; lineHeight?: number; diff --git a/src/Parser.ts b/src/Parser.ts index b6493fea..6f4b0932 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -286,36 +286,43 @@ export class Parser { // Single Shift Select of G2 Character Set // ( SS2 is 0x8e). This affects next character only. case 'N': + this._state = ParserState.NORMAL; break; // ESC O // Single Shift Select of G3 Character Set // ( SS3 is 0x8f). This affects next character only. case 'O': + this._state = ParserState.NORMAL; break; // ESC n // Invoke the G2 Character Set as GL (LS2). case 'n': this._terminal.setgLevel(2); + this._state = ParserState.NORMAL; break; // ESC o // Invoke the G3 Character Set as GL (LS3). case 'o': this._terminal.setgLevel(3); + this._state = ParserState.NORMAL; break; // ESC | // Invoke the G3 Character Set as GR (LS3R). case '|': this._terminal.setgLevel(3); + this._state = ParserState.NORMAL; break; // ESC } // Invoke the G2 Character Set as GR (LS2R). case '}': this._terminal.setgLevel(2); + this._state = ParserState.NORMAL; break; // ESC ~ // Invoke the G1 Character Set as GR (LS1R). case '~': this._terminal.setgLevel(1); + this._state = ParserState.NORMAL; break; // ESC 7 Save Cursor (DECSC). diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index e229889e..9a9cf929 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 7e04119e..e68bb6a0 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -288,6 +288,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager public selectAll(): void { this._model.isSelectAllActive = true; this.refresh(); + this._terminal.emit('selection'); } /** @@ -560,7 +561,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]; } @@ -574,6 +575,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager */ private _onMouseUp(event: MouseEvent): void { this._removeMouseDownListeners(); + + if (this.hasSelection) + 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 7dbdeb76..f2785fbb 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; @@ -70,9 +64,10 @@ const WRITE_BUFFER_PAUSE_THRESHOLD = 5; const WRITE_BATCH_SIZE = 300; const DEFAULT_OPTIONS: ITerminalOptions = { + cols: 80, + rows: 24, convertEol: false, termName: 'xterm', - geometry: [80, 24], cursorBlink: false, cursorStyle: 'block', bellSound: BellSound, @@ -133,6 +128,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT public originMode: boolean; public insertMode: boolean; public wraparoundMode: boolean; // defaults: xterm - true, vt100 - false + public bracketedPasteMode: boolean; // charset // The current charset @@ -206,7 +202,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT public cols: number; public rows: number; - public geometry: [/*cols*/number, /*rows*/number]; /** * Creates a new `Terminal` object. @@ -242,9 +237,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // TODO: WHy not document.body? this.parent = document ? document.body : null; - this.cols = this.options.cols || this.options.geometry[0]; - this.rows = this.options.rows || this.options.geometry[1]; - this.geometry = [this.cols, this.rows]; + this.cols = this.options.cols; + this.rows = this.options.rows; if (this.options.handler) { this.on('data', this.options.handler); @@ -261,6 +255,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.originMode = false; this.insertMode = false; this.wraparoundMode = true; // defaults: xterm - true, vt100 - false + this.bracketedPasteMode = false; // charset this.charset = null; @@ -322,7 +317,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 +581,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 +602,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 +621,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 +673,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT // Listen for mouse events and translate // them into terminal mouse protocols. this.bindMouse(); + } /** @@ -687,22 +688,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 +1450,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; @@ -1866,8 +1859,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.charMeasure.measure(this.options); this.refresh(0, this.rows - 1); - - this.geometry = [this.cols, this.rows]; this.emit('resize', {cols: x, rows: y}); } @@ -2129,6 +2120,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 997d1097..00000000 --- a/src/addons/attach/attach.js +++ /dev/null @@ -1,125 +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); - } - }; - - term._getMessage = function (ev) { - if (buffered) { - term._pushToBuffer(ev.data); - } else { - term.write(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/demo/app.js b/src/addons/zmodem/demo/app.js new file mode 100644 index 00000000..31ecd7d2 --- /dev/null +++ b/src/addons/zmodem/demo/app.js @@ -0,0 +1,87 @@ +var express = require('express'); +var app = express(); +var expressWs = require('express-ws')(app); +var os = require('os'); +var pty = require('node-pty'); + +var terminals = {}, + logs = {}; + +app.use('/build', express.static(__dirname + '/../../../../build')); +app.use('/demo', express.static(__dirname + '/../../../../demo')); +app.use('/zmodemjs', express.static(__dirname + '/../../../../node_modules/zmodem.js/dist')); + +app.get('/', function(req, res){ + res.sendFile(__dirname + '/index.html'); +}); + +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.post('/terminals', function (req, res) { + var cols = parseInt(req.query.cols), + rows = parseInt(req.query.rows), + term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], { + encoding: null, + name: 'xterm-color', + cols: cols || 80, + rows: rows || 24, + cwd: process.env.PWD, + env: process.env + }); + + console.log('Created terminal with PID: ' + term.pid); + terminals[term.pid] = term; + logs[term.pid] = ''; + term.on('data', function(data) { + logs[term.pid] += data; + }); + res.send(term.pid.toString()); + res.end(); +}); + +app.post('/terminals/:pid/size', function (req, res) { + var pid = parseInt(req.params.pid), + cols = parseInt(req.query.cols), + rows = parseInt(req.query.rows), + term = terminals[pid]; + + term.resize(cols, rows); + console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.'); + res.end(); +}); + +app.ws('/terminals/:pid', function (ws, req) { + var term = terminals[parseInt(req.params.pid)]; + console.log('Connected to terminal ' + term.pid); + ws.send(logs[term.pid]); + + term.on('data', function(data) { + try { + ws.send(data); + } catch (ex) { + // The WebSocket is not open, ignore + } + }); + ws.on('message', function(msg) { + term.write(msg); + }); + ws.on('close', function () { + term.kill(); + console.log('Closed terminal ' + term.pid); + // Clean things up + delete terminals[term.pid]; + delete logs[term.pid]; + }); +}); + +var port = process.env.PORT || 3000, + host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0'; + +console.log('App listening to http://' + host + ':' + port); +app.listen(port, host); diff --git a/src/addons/zmodem/demo/index.html b/src/addons/zmodem/demo/index.html new file mode 100644 index 00000000..aee7742a --- /dev/null +++ b/src/addons/zmodem/demo/index.html @@ -0,0 +1,128 @@ + + + + xterm.js demo + + + + + + + + + + + + + + + + +

xterm.js: xterm, in the browser

+ +
+ +
+ + + + + + + + + +
+ +
+

Actions

+

+ + +

+
+
+

Options

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+
+

Size

+
+
+ + +
+
+ + +
+
+
+
+

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.

+

* ZMODEM file transfers are supported via an addon. To try it out, install lrzsz onto the remote peer, then run rz to send from your browser or sz <file> to send from the remote peer.

+ + + diff --git a/src/addons/zmodem/demo/main.js b/src/addons/zmodem/demo/main.js new file mode 100644 index 00000000..2a153fb5 --- /dev/null +++ b/src/addons/zmodem/demo/main.js @@ -0,0 +1,383 @@ +"use strict"; + +var term, + protocol, + socketURL, + socket, + pid; + +var terminalContainer = document.getElementById('terminal-container'), + actionElements = { + findNext: document.querySelector('#find-next'), + findPrevious: document.querySelector('#find-previous') + }, + optionElements = { + cursorBlink: document.querySelector('#option-cursor-blink'), + cursorStyle: document.querySelector('#option-cursor-style'), + scrollback: document.querySelector('#option-scrollback'), + tabstopwidth: document.querySelector('#option-tabstopwidth'), + bellStyle: document.querySelector('#option-bell-style') + }, + colsElement = document.getElementById('cols'), + rowsElement = document.getElementById('rows'); + +function setTerminalSize() { + var cols = parseInt(colsElement.value, 10); + 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'; + + terminalContainer.style.width = width; + terminalContainer.style.height = height; + term.resize(cols, rows); +} + +colsElement.addEventListener('change', setTerminalSize); +rowsElement.addEventListener('change', setTerminalSize); + +actionElements.findNext.addEventListener('keypress', function (e) { + if (e.key === "Enter") { + e.preventDefault(); + term.findNext(actionElements.findNext.value); + } +}); +actionElements.findPrevious.addEventListener('keypress', function (e) { + if (e.key === "Enter") { + e.preventDefault(); + term.findPrevious(actionElements.findPrevious.value); + } +}); + +optionElements.cursorBlink.addEventListener('change', function () { + term.setOption('cursorBlink', optionElements.cursorBlink.checked); +}); +optionElements.cursorStyle.addEventListener('change', function () { + term.setOption('cursorStyle', optionElements.cursorStyle.value); +}); +optionElements.bellStyle.addEventListener('change', function () { + term.setOption('bellStyle', optionElements.bellStyle.value); +}); +optionElements.scrollback.addEventListener('change', function () { + term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10)); +}); +optionElements.tabstopwidth.addEventListener('change', function () { + term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10)); +}); + +createTerminal(); + +function createTerminal() { + // Clean terminal + while (terminalContainer.children.length) { + terminalContainer.removeChild(terminalContainer.children[0]); + } + term = new Terminal({ + cursorBlink: optionElements.cursorBlink.checked, + scrollback: parseInt(optionElements.scrollback.value, 10), + tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10) + }); + term.on('resize', function (size) { + if (!pid) { + return; + } + var cols = size.cols, + rows = size.rows, + url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows; + + fetch(url, {method: 'POST'}); + }); + protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; + socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; + + term.open(terminalContainer); + term.fit(); + + // fit is called within a setTimeout, cols and rows need this. + setTimeout(function () { + colsElement.value = term.cols; + rowsElement.value = term.rows; + + // Set terminal size again to set the specific dimensions on the demo + setTerminalSize(); + + fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) { + + res.text().then(function (pid) { + window.pid = pid; + socketURL += pid; + socket = new WebSocket(socketURL); + socket.onopen = runRealTerminal; + socket.onclose = runFakeTerminal; + socket.onerror = runFakeTerminal; + + term.zmodemAttach(socket, { + noTerminalWriteOutsideSession: true, + } ); + + term.on("zmodemRetract", () => { + start_form.style.display = "none"; + start_form.onsubmit = null; + }); + + term.on("zmodemDetect", (detection) => { + function do_zmodem() { + term.detach(); + let zsession = detection.confirm(); + + var promise; + + if (zsession.type === "receive") { + promise = _handle_receive_session(zsession); + } + else { + promise = _handle_send_session(zsession); + } + + promise.catch( console.error.bind(console) ).then( () => { + term.attach(socket); + } ); + } + + if (_auto_zmodem()) { + do_zmodem(); + } + else { + start_form.style.display = ""; + start_form.onsubmit = function(e) { + start_form.style.display = "none"; + + if (document.getElementById("zmstart_yes").checked) { + do_zmodem(); + } + else { + detection.deny(); + } + }; + } + }); + }); + }); + }, 0); +} + +//---------------------------------------------------------------------- +// UI STUFF + +function _show_file_info(xfer) { + var file_info = xfer.get_details(); + + document.getElementById("name").textContent = file_info.name; + document.getElementById("size").textContent = file_info.size; + document.getElementById("mtime").textContent = file_info.mtime; + document.getElementById("files_remaining").textContent = file_info.files_remaining; + document.getElementById("bytes_remaining").textContent = file_info.bytes_remaining; + + document.getElementById("mode").textContent = "0" + file_info.mode.toString(8); + + var xfer_opts = xfer.get_options(); + ["conversion", "management", "transport", "sparse"].forEach( (lbl) => { + document.getElementById(`zfile_${lbl}`).textContent = xfer_opts[lbl]; + } ); + + document.getElementById("zm_file").style.display = ""; +} +function _hide_file_info() { + document.getElementById("zm_file").style.display = "none"; +} + +function _save_to_disk(xfer, buffer) { + return Zmodem.Browser.save_to_disk(buffer, xfer.get_details().name); +} + +var skipper_button = document.getElementById("zm_progress_skipper"); +var skipper_button_orig_text = skipper_button.textContent; + +function _show_progress() { + skipper_button.disabled = false; + skipper_button.textContent = skipper_button_orig_text; + + document.getElementById("bytes_received").textContent = 0; + document.getElementById("percent_received").textContent = 0; + + document.getElementById("zm_progress").style.display = ""; +} + +function _update_progress(xfer) { + var total_in = xfer.get_offset(); + + document.getElementById("bytes_received").textContent = total_in; + + var percent_received = 100 * total_in / xfer.get_details().size; + document.getElementById("percent_received").textContent = percent_received.toFixed(2); +} + +function _hide_progress() { + document.getElementById("zm_progress").style.display = "none"; +} + +var start_form = document.getElementById("zm_start"); + +function _auto_zmodem() { + return document.getElementById("zmodem-auto").checked; +} + +// END UI STUFF +//---------------------------------------------------------------------- + +function _handle_receive_session(zsession) { + zsession.on("offer", function(xfer) { + current_receive_xfer = xfer; + + _show_file_info(xfer); + + var offer_form = document.getElementById("zm_offer"); + + function on_form_submit() { + offer_form.style.display = "none"; + + //START + //if (offer_form.zmaccept.value) { + if (_auto_zmodem() || document.getElementById("zmaccept_yes").checked) { + _show_progress(); + + var FILE_BUFFER = []; + xfer.on("input", (payload) => { + _update_progress(xfer); + FILE_BUFFER.push( new Uint8Array(payload) ); + }); + xfer.accept().then( + () => { + _save_to_disk(xfer, FILE_BUFFER); + }, + console.error.bind(console) + ); + } + else { + xfer.skip(); + } + //END + } + + if (_auto_zmodem()) { + on_form_submit(); + } + else { + offer_form.onsubmit = on_form_submit; + offer_form.style.display = ""; + } + } ); + + var promise = new Promise( (res) => { + zsession.on("session_end", () => { + _hide_file_info(); + _hide_progress(); + res(); + } ); + } ); + + zsession.start(); + + return promise; +} + +function _handle_send_session(zsession) { + var choose_form = document.getElementById("zm_choose"); + choose_form.style.display = ""; + + var file_el = document.getElementById("zm_files"); + + var promise = new Promise( (res) => { + file_el.onchange = function(e) { + choose_form.style.display = "none"; + + var files_obj = file_el.files; + + Zmodem.Browser.send_files( + zsession, + files_obj, + { + on_offer_response(obj, xfer) { + if (xfer) _show_progress(); + //console.log("offer", xfer ? "accepted" : "skipped"); + }, + on_progress(obj, xfer) { + _update_progress(xfer); + }, + on_file_complete(obj) { + //console.log("COMPLETE", obj); + _hide_progress(); + }, + } + ).then(_hide_progress).then( + zsession.close.bind(zsession), + console.error.bind(console) + ).then( () => { + _hide_file_info(); + _hide_progress(); + res(); + } ); + }; + } ); + + return promise; +} + +//This is here to allow canceling of an in-progress ZMODEM transfer. +var current_receive_xfer; + +//Called from HTML directly. +function skip_current_file() { + current_receive_xfer.skip(); + + skipper_button.disabled = true; + skipper_button.textContent = "Waiting for server to acknowledge skip …"; +} + +function runRealTerminal() { + term.attach(socket); + + term._initialized = true; +} + +function runFakeTerminal() { + if (term._initialized) { + return; + } + + term._initialized = true; + + var shellprompt = '$ '; + + term.prompt = function () { + term.write('\r\n' + shellprompt); + }; + + term.writeln('Welcome to xterm.js'); + term.writeln('This is a local terminal emulation, without a real terminal in the back-end.'); + term.writeln('Type some keys and commands to play around.'); + term.writeln(''); + term.prompt(); + + term.on('key', function (key, ev) { + var printable = ( + !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey + ); + + if (ev.keyCode == 13) { + term.prompt(); + } else if (ev.keyCode == 8) { + // Do not delete the prompt + if (term.x > 2) { + term.write('\b \b'); + } + } else if (printable) { + term.write(key); + } + }); + + term.on('paste', function (data, ev) { + term.write(data); + }); +} 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.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/handlers/Clipboard.test.ts b/src/handlers/Clipboard.test.ts index 6901b730..d35e5fa4 100644 --- a/src/handlers/Clipboard.test.ts +++ b/src/handlers/Clipboard.test.ts @@ -16,4 +16,12 @@ describe('evaluatePastedTextProcessing', () => { assert.equal(processedText, 'foo\r\nbar\r\n'); assert.equal(windowsProcessedText, 'foo\rbar\r'); }); + it('should bracket pasted text in bracketedPasteMode', () => { + const pastedText = 'foo bar'; + const unbracketedText = Clipboard.bracketTextForPaste(pastedText, false); + const bracketedText = Clipboard.bracketTextForPaste(pastedText, true); + + assert.equal(unbracketedText, 'foo bar'); + assert.equal(bracketedText, '\x1b[200~foo bar\x1b[201~'); + }); }); diff --git a/src/handlers/Clipboard.ts b/src/handlers/Clipboard.ts index f110ed1f..b3277bd7 100644 --- a/src/handlers/Clipboard.ts +++ b/src/handlers/Clipboard.ts @@ -25,6 +25,17 @@ export function prepareTextForTerminal(text: string, isMSWindows: boolean): stri return text; } +/** + * Bracket text for paste, if necessary, as per https://cirw.in/blog/bracketed-paste + * @param text The pasted text to bracket + */ +export function bracketTextForPaste(text: string, bracketedPasteMode: boolean): string { + if (bracketedPasteMode) { + return '\x1b[200~' + text + '\x1b[201~'; + } + return text; +} + /** * Binds copy functionality to the given terminal. * @param {ClipboardEvent} ev The original copy event to be handled @@ -52,6 +63,7 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { let dispatchPaste = function(text: string): void { text = prepareTextForTerminal(text, term.browser.isMSWindows); + text = bracketTextForPaste(text, term.bracketedPasteMode); term.handler(text); term.textarea.value = ''; term.emit('paste', text); 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/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index c76266a9..80e25277 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -10,6 +10,7 @@ import * as Browser from './Browser'; import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Interfaces'; export class MockTerminal implements ITerminal { + bracketedPasteMode: boolean; mouseHelper: IMouseHelper; renderer: IRenderer; linkifier: ILinkifier; @@ -94,6 +95,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { originMode: boolean; insertMode: boolean; wraparoundMode: boolean; + bracketedPasteMode: boolean; defAttr: number; curAttr: number; prefix: string; 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 a5137797..09769dbe 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', 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' | '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. @@ -420,11 +420,6 @@ declare module 'xterm' { * @param key The option key. */ getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; - /** - * Retrieves an option's value from the terminal. - * @param key The option key. - */ - getOption(key: 'geometry'): [number, number]; /** * Retrieves an option's value from the terminal. * @param key The option key. @@ -471,13 +466,7 @@ declare module 'xterm' { * @param key The option key. * @param value The option value. */ - setOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback', value: number): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - */ - setOption(key: 'geometry', value: [number, number]): void; + setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void; /** * Sets an option on the terminal. * @param key The option key. @@ -511,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' +};