diff --git a/Dockerfile b/Dockerfile index 36e821bd..1f0db1f9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,6 @@ FROM node:6.9 MAINTAINER Paris Kasidiaris -# Install cpio, used for building -RUN apt-get update \ - && apt-get install -y --no-install-recommends cpio \ - && rm -rf /var/lib/apt/lists/* - # Set the working directory WORKDIR /usr/src/app diff --git a/README.md b/README.md index b5d30041..f68da0f1 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js - [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. +- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE. +- [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams. 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. @@ -117,6 +119,14 @@ Visit https://lair.io/sourcelair/xterm and follow the instructions. All developm [Download Visual Studio Code](http://code.visualstudio.com/Download), clone xterm.js and you are all set. +#### [Eclipse Che](http://www.eclipse.org/che) + +You can start Eclipse Che with `docker run eclipse/che start`. + +#### [Codenvy](http://www.codenvy.io) + +You can create a trial account or install an enterprise version with `docker run codenvy/cli start`. + ## License Agreement If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. diff --git a/bin/build b/bin/build deleted file mode 100755 index a01a1b9f..00000000 --- a/bin/build +++ /dev/null @@ -1,36 +0,0 @@ -#! /usr/bin/env bash - -set -e - -# $BUILD_DIR should default to "build" -BUILD_DIR=${BUILD_DIR:=build} - -# Create the build directory -mkdir -p $BUILD_DIR - - -# Clean lib/* to prevent confusion if files were deleted in src/ -rm -rf lib/* - -# Build all TypeScript files (including tests) to lib/ -tsc - -# Concat all xterm.js files into a single file and output as a UMD to $BUILD_DIR/xterm.js -browserify ./lib/xterm.js --standalone Terminal --debug --outfile ./$BUILD_DIR/xterm.js -cat ./$BUILD_DIR/xterm.js | exorcist ./$BUILD_DIR/xterm.js.map -b ./$BUILD_DIR > ./$BUILD_DIR/xterm.temp.js -rm ./$BUILD_DIR/xterm.js -mv ./$BUILD_DIR/xterm.temp.js ./$BUILD_DIR/xterm.js - -# Resolve the chain of sourcemaps so that ./$BUILD_DIR/xterm.js.map points at ./src -sorcery -i $BUILD_DIR/xterm.js - -# Copy all CSS files from src/ to $BUILD_DIR/ and lib/ -cd src -find . -name '*.css' | cpio -pdm ../$BUILD_DIR -find . -name '*.css' | cpio -pdm ../lib -cd .. - -# Copy addons from lib/ to $BUILD_DIR/ -cd lib/addons -find . -name '*.js' | cpio -pdm ../../$BUILD_DIR/addons -cd ../.. diff --git a/bin/prepare-release b/bin/prepare-release index 6bd39d3d..464e65b4 100755 --- a/bin/prepare-release +++ b/bin/prepare-release @@ -21,7 +21,7 @@ CURRENT_BOWER_JSON_VERSION=$(cat bower.json \ # Build xterm.js into `dist` export BUILD_DIR=dist -./bin/build +npm run build # Update AUTHORS file sh bin/generate-authors diff --git a/demo/index.html b/demo/index.html index 285fe002..764c8442 100644 --- a/demo/index.html +++ b/demo/index.html @@ -16,7 +16,12 @@

Options

- +

+ +

+

+ +

Size

diff --git a/demo/main.js b/demo/main.js index 86fbd314..52da2cef 100644 --- a/demo/main.js +++ b/demo/main.js @@ -8,7 +8,8 @@ var term, var terminalContainer = document.getElementById('terminal-container'), optionElements = { - cursorBlink: document.querySelector('#option-cursor-blink') + cursorBlink: document.querySelector('#option-cursor-blink'), + scrollback: document.querySelector('#option-scrollback') }, colsElement = document.getElementById('cols'), rowsElement = document.getElementById('rows'); @@ -28,6 +29,9 @@ colsElement.addEventListener('change', setTerminalSize); rowsElement.addEventListener('change', setTerminalSize); optionElements.cursorBlink.addEventListener('change', createTerminal); +optionElements.scrollback.addEventListener('change', function () { + terminal.setOption('scrollback', parseInt(optionElements.scrollback.value, 10)); +}); createTerminal(); @@ -37,7 +41,8 @@ function createTerminal() { terminalContainer.removeChild(terminalContainer.children[0]); } term = new Terminal({ - cursorBlink: optionElements.cursorBlink.checked + cursorBlink: optionElements.cursorBlink.checked, + scrollback: parseInt(optionElements.scrollback.value, 10) }); term.on('resize', function (size) { if (!pid) { @@ -78,7 +83,6 @@ function createTerminal() { }); } - function runRealTerminal() { term.attach(socket); term._initialized = true; diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 00000000..144b8831 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,87 @@ +const browserify = require('browserify'); +const buffer = require('vinyl-buffer'); +const fs = require('fs-extra'); +const gulp = require('gulp'); +const merge = require('merge-stream'); +const sorcery = require('sorcery'); +const source = require('vinyl-source-stream'); +const sourcemaps = require('gulp-sourcemaps'); +const ts = require('gulp-typescript'); +const tsify = require('tsify'); + + +let buildDir = process.env.BUILD_DIR || 'build'; + + +/** + * Compile TypeScript sources to JavaScript files and create a source map file for each TypeScript + * file compiled. + */ +gulp.task('tsc', function () { + // Remove the lib/ directory to prevent confusion if files were deleted in src/ + fs.emptyDirSync('lib'); + + // Build all TypeScript files (including tests) to lib/, based on the configuration defined in + // `tsconfig.json`. + let tsProject = ts.createProject('tsconfig.json'); + let tsResult = tsProject.src().pipe(sourcemaps.init()).pipe(tsProject()); + let tsc = tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest('lib')); + + // Copy all addons from src/ to lib/ + let copyAddons = gulp.src('src/addons/**/*').pipe(gulp.dest('lib/addons')); + + // Copy stylesheets from src/ to lib/ + let copyStylesheets = gulp.src('src/**/*.css').pipe(gulp.dest('lib')); + + return merge(tsc, copyAddons, copyStylesheets); +}); + +/** + * Bundle JavaScript files produced by the `tsc` task, into a single file named `xterm.js` with + * Browserify. + */ +gulp.task('browserify', ['tsc'], function() { + // Ensure that the build directory exists + fs.ensureDirSync(buildDir); + + let browserifyOptions = { + basedir: buildDir, + debug: true, + entries: ['../lib/xterm.js'], + standalone: 'Terminal', + cache: {}, + packageCache: {} + }; + let bundleStream = browserify(browserifyOptions) + .plugin(tsify) + .bundle() + .pipe(source('xterm.js')) + .pipe(buffer()) + .pipe(sourcemaps.init({loadMaps: true, sourceRoot: '..'})) + .pipe(sourcemaps.write('./')) + .pipe(gulp.dest(buildDir)); + + // Copy all add-ons from lib/ to buildDir + let copyAddons = gulp.src('lib/addons/**/*').pipe(gulp.dest(`${buildDir}/addons`)); + + // Copy stylesheets from src/ to lib/ + let copyStylesheets = gulp.src('lib/**/*.css').pipe(gulp.dest(buildDir)); + + return merge(bundleStream, copyAddons, copyStylesheets); +}); + + +/** + * Use `sorcery` to resolve the source map chain and point back to the TypeScript files. + * (Without this task the source maps produced for the JavaScript bundle points into the + * compiled JavaScript files in lib/). + */ +gulp.task('sorcery', ['browserify'], function () { + var chain = sorcery.loadSync(`${buildDir}/xterm.js`); + var map = chain.apply(); + chain.writeSync(); +}); + +gulp.task('build', ['sorcery']); + +gulp.task('default', ['build']); diff --git a/package.json b/package.json index 74ba3e00..67eb824d 100644 --- a/package.json +++ b/package.json @@ -38,18 +38,26 @@ "browserify": "^13.1.0", "chai": "3.5.0", "docdash": "0.4.0", - "exorcist": "^0.4.0", "express": "4.13.4", "express-ws": "2.0.0-rc.1", + "fs-extra": "^1.0.0", "glob": "^7.0.5", + "gulp": "^3.9.1", + "gulp-cli": "^1.2.2", + "gulp-sourcemaps": "1.9.1", + "gulp-typescript": "^3.1.3", "jsdoc": "3.4.3", + "merge-stream": "^1.0.1", "mocha": "2.5.3", "node-pty": "^0.4.1", "nodemon": "1.10.2", "sleep": "^3.0.1", "sorcery": "^0.10.0", + "tsify": "^3.0.0", "tslint": "^4.0.2", - "typescript": "^2.0.3" + "typescript": "^2.0.3", + "vinyl-buffer": "^1.0.0", + "vinyl-source-stream": "^1.1.0" }, "scripts": { "prestart": "npm run build", @@ -58,7 +66,7 @@ "lint": "tslint src/**/*.ts", "test": "mocha --recursive ./lib", "build:docs": "jsdoc -c jsdoc.json", - "build": "./bin/build", + "build": "gulp build", "prepublish": "npm run build" } } diff --git a/src/EscapeSequences.ts b/src/EscapeSequences.ts new file mode 100644 index 00000000..34dfde90 --- /dev/null +++ b/src/EscapeSequences.ts @@ -0,0 +1,74 @@ +/** + * C0 control codes + * See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes + */ +export namespace C0 { + /** Null (Caret = ^@, C = \0) */ + export const NUL = '\x00'; + /** Start of Heading (Caret = ^A) */ + export const SOH = '\x01'; + /** Start of Text (Caret = ^B) */ + export const STX = '\x02'; + /** End of Text (Caret = ^C) */ + export const ETX = '\x03'; + /** End of Transmission (Caret = ^D) */ + export const EOT = '\x04'; + /** Enquiry (Caret = ^E) */ + export const ENQ = '\x05'; + /** Acknowledge (Caret = ^F) */ + export const ACK = '\x06'; + /** Bell (Caret = ^G, C = \a) */ + export const BEL = '\x07'; + /** Backspace (Caret = ^H, C = \b) */ + export const BS = '\x08'; + /** Character Tabulation, Horizontal Tabulation (Caret = ^I, C = \t) */ + export const HT = '\x09'; + /** Line Feed (Caret = ^J, C = \n) */ + export const LF = '\x0a'; + /** Line Tabulation, Vertical Tabulation (Caret = ^K, C = \v) */ + export const VT = '\x0b'; + /** Form Feed (Caret = ^L, C = \f) */ + export const FF = '\x0c'; + /** Carriage Return (Caret = ^M, C = \r) */ + export const CR = '\x0d'; + /** Shift Out (Caret = ^N) */ + export const SO = '\x0e'; + /** Shift In (Caret = ^O) */ + export const SI = '\x0f'; + /** Data Link Escape (Caret = ^P) */ + export const DLE = '\x10'; + /** Device Control One (XON) (Caret = ^Q) */ + export const DC1 = '\x11'; + /** Device Control Two (Caret = ^R) */ + export const DC2 = '\x12'; + /** Device Control Three (XOFF) (Caret = ^S) */ + export const DC3 = '\x13'; + /** Device Control Four (Caret = ^T) */ + export const DC4 = '\x14'; + /** Negative Acknowledge (Caret = ^U) */ + export const NAK = '\x15'; + /** Synchronous Idle (Caret = ^V) */ + export const SYN = '\x16'; + /** End of Transmission Block (Caret = ^W) */ + export const ETB = '\x17'; + /** Cancel (Caret = ^X) */ + export const CAN = '\x18'; + /** End of Medium (Caret = ^Y) */ + export const EM = '\x19'; + /** Substitute (Caret = ^Z) */ + export const SUB = '\x1a'; + /** Escape (Caret = ^[, C = \e) */ + export const ESC = '\x1b'; + /** File Separator (Caret = ^\) */ + export const FS = '\x1c'; + /** Group Separator (Caret = ^]) */ + export const GS = '\x1d'; + /** Record Separator (Caret = ^^) */ + export const RS = '\x1e'; + /** Unit Separator (Caret = ^_) */ + export const US = '\x1f'; + /** Space */ + export const SP = '\x20'; + /** Delete (Caret = ^?) */ + export const DEL = '\x7f'; +}; diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts index d05e3863..1db86763 100644 --- a/src/EventEmitter.ts +++ b/src/EventEmitter.ts @@ -11,7 +11,9 @@ export class EventEmitter { private _events: {[type: string]: ListenerType[]}; constructor() { - this._events = {}; + // Restore the previous events if available, this will happen if the + // constructor is called multiple times on the same object (terminal reset). + this._events = this._events || {}; } public on(type, listener): void { diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts index 5b106b43..4fa77ec0 100644 --- a/src/Viewport.test.ts +++ b/src/Viewport.test.ts @@ -2,11 +2,11 @@ import { assert } from 'chai'; import { Viewport } from './Viewport'; describe('Viewport', () => { - var terminal; - var viewportElement; - var charMeasureElement; - var viewport; - var scrollAreaElement; + let terminal; + let viewportElement; + let charMeasure; + let viewport; + let scrollAreaElement; const CHARACTER_HEIGHT = 10; @@ -34,21 +34,17 @@ describe('Viewport', () => { height: 0 } }; - charMeasureElement = { - getBoundingClientRect: () => { - return { width: null, height: CHARACTER_HEIGHT }; - } + charMeasure = { + height: CHARACTER_HEIGHT }; - viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasureElement); + viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasure); }); describe('refresh', () => { it('should set the line-height of the terminal', () => { assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px'); assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px'); - charMeasureElement.getBoundingClientRect = () => { - return { width: null, height: 1 }; - }; + charMeasure.height = 1; viewport.refresh(); assert.equal(viewportElement.style.lineHeight, '1px'); assert.equal(terminal.rowContainer.style.lineHeight, '1px'); @@ -59,9 +55,7 @@ describe('Viewport', () => { terminal.rows = 1; viewport.refresh(); assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); - charMeasureElement.getBoundingClientRect = () => { - return { width: null, height: 20 }; - }; + charMeasure.height = 20; viewport.refresh(); assert.equal(viewportElement.style.height, 20 + 'px'); }); diff --git a/src/Viewport.ts b/src/Viewport.ts index 3aa1319f..aaafbc5f 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -3,6 +3,7 @@ */ import { ITerminal } from './Interfaces'; +import { CharMeasure } from './utils/CharMeasure'; /** * Represents the viewport of a terminal, the visible area within the larger buffer of output. @@ -24,7 +25,7 @@ export class Viewport { private terminal: ITerminal, private viewportElement: HTMLElement, private scrollArea: HTMLElement, - private charMeasureElement: HTMLElement + private charMeasure: CharMeasure ) { this.currentRowHeight = 0; this.lastRecordedBufferLength = 0; @@ -43,21 +44,20 @@ export class Viewport { * @param charSize A character size measurement bounding rect object, if it doesn't exist it will * be created. */ - private refresh(charSize?: ClientRect): void { - var size = charSize || this.charMeasureElement.getBoundingClientRect(); - if (size.height > 0) { - var rowHeightChanged = size.height !== this.currentRowHeight; + private refresh(): void { + if (this.charMeasure.height > 0) { + const rowHeightChanged = this.charMeasure.height !== this.currentRowHeight; if (rowHeightChanged) { - this.currentRowHeight = size.height; - this.viewportElement.style.lineHeight = size.height + 'px'; - this.terminal.rowContainer.style.lineHeight = size.height + 'px'; + this.currentRowHeight = this.charMeasure.height; + this.viewportElement.style.lineHeight = this.charMeasure.height + 'px'; + this.terminal.rowContainer.style.lineHeight = this.charMeasure.height + 'px'; } - var viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; + const viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; if (rowHeightChanged || viewportHeightChanged) { this.lastRecordedViewportHeight = this.terminal.rows; - this.viewportElement.style.height = size.height * this.terminal.rows + 'px'; + this.viewportElement.style.height = this.charMeasure.height * this.terminal.rows + 'px'; } - this.scrollArea.style.height = (size.height * this.lastRecordedBufferLength) + 'px'; + this.scrollArea.style.height = (this.charMeasure.height * this.lastRecordedBufferLength) + 'px'; } } @@ -74,14 +74,13 @@ export class Viewport { this.refresh(); } else { // If size has changed, refresh viewport - var size = this.charMeasureElement.getBoundingClientRect(); - if (size.height !== this.currentRowHeight) { - this.refresh(size); + if (this.charMeasure.height !== this.currentRowHeight) { + this.refresh(); } } // Sync scrollTop - var scrollTop = this.terminal.ydisp * this.currentRowHeight; + const scrollTop = this.terminal.ydisp * this.currentRowHeight; if (this.viewportElement.scrollTop !== scrollTop) { this.viewportElement.scrollTop = scrollTop; } @@ -93,8 +92,8 @@ export class Viewport { * @param ev The scroll event. */ private onScroll(ev: Event) { - var newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight); - var diff = newRow - this.terminal.ydisp; + const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight); + const diff = newRow - this.terminal.ydisp; this.terminal.scrollDisp(diff, true); } @@ -110,7 +109,7 @@ export class Viewport { return; } // Fallback to WheelEvent.DOM_DELTA_PIXEL - var multiplier = 1; + let multiplier = 1; if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) { multiplier = this.currentRowHeight; } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { diff --git a/src/test/escape-sequences-test.js b/src/test/escape-sequences-test.js index 7dcefd95..6c64b562 100644 --- a/src/test/escape-sequences-test.js +++ b/src/test/escape-sequences-test.js @@ -92,7 +92,11 @@ describe('xterm output comparison', function() { var from_pty = pty_write_read(in_file); // uncomment this to get log from terminal //console.log = function(){}; - xterm.write(from_pty); + + // Perform a synchronous .write(data) + xterm.writeBuffer.push(from_pty); + xterm.innerWrite(); + var from_emulator = terminalToString(xterm); console.log = CONSOLE_LOG; var expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8'); diff --git a/src/test/test.js b/src/test/test.js index a44d2066..7c716b2a 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -14,6 +14,15 @@ describe('xterm.js', function() { xterm.compositionHelper = { keydown: function(){ return true; } }; + // Force synchronous refreshes + xterm.queueRefresh = function(start, end) { + xterm.refresh(start, end); + }; + // Force synchronous writes + xterm.write = function(data) { + xterm.writeBuffer.push(data); + xterm.innerWrite(); + }; }); describe('getOption', function() { diff --git a/src/utils/Browser.js b/src/utils/Browser.js deleted file mode 100644 index cd13e027..00000000 --- a/src/utils/Browser.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Attributes and methods to help with identifying the current browser and platform. - * @module xterm/utils/Browser - * @license MIT - */ - -import { contains } from './Generic.js'; - -let isNode = (typeof navigator == 'undefined') ? true : false; -let userAgent = (isNode) ? 'node' : navigator.userAgent; -let platform = (isNode) ? 'node' : navigator.platform; - -export let isFirefox = !!~userAgent.indexOf('Firefox'); -export let isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident'); - -// Find the users platform. We use this to interpret the meta key -// and ISO third level shifts. -// http://stackoverflow.com/q/19877924/577598 -export let isMac = contains(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], platform); -export let isIpad = platform === 'iPad'; -export let isIphone = platform === 'iPhone'; -export let isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform); diff --git a/src/utils/Browser.ts b/src/utils/Browser.ts new file mode 100644 index 00000000..04da698e --- /dev/null +++ b/src/utils/Browser.ts @@ -0,0 +1,22 @@ +/** + * Attributes and methods to help with identifying the current browser and platform. + * @module xterm/utils/Browser + * @license MIT + */ + +import { contains } from './Generic'; + +const isNode = (typeof navigator === 'undefined') ? true : false; +const userAgent = (isNode) ? 'node' : navigator.userAgent; +const platform = (isNode) ? 'node' : navigator.platform; + +export const isFirefox = !!~userAgent.indexOf('Firefox'); +export const isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident'); + +// Find the users platform. We use this to interpret the meta key +// and ISO third level shifts. +// http://stackoverflow.com/q/19877924/577598 +export const isMac = contains(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], platform); +export const isIpad = platform === 'iPad'; +export const isIphone = platform === 'iPhone'; +export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform); diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts new file mode 100644 index 00000000..6ca5046a --- /dev/null +++ b/src/utils/CharMeasure.ts @@ -0,0 +1,57 @@ +/** + * @module xterm/utils/CharMeasure + * @license MIT + */ + +import { EventEmitter } from '../EventEmitter.js'; + +/** + * Utility class that measures the size of a character. + */ +export class CharMeasure extends EventEmitter { + private _parentElement: HTMLElement; + private _measureElement: HTMLElement; + private _width: number; + private _height: number; + + constructor(parentElement: HTMLElement) { + super(); + this._parentElement = parentElement; + } + + public get width(): number { + return this._width; + } + + public get height(): number { + return this._height; + } + + public measure(): void { + if (!this._measureElement) { + this._measureElement = document.createElement('span'); + this._measureElement.style.position = 'absolute'; + this._measureElement.style.top = '0'; + this._measureElement.style.left = '-9999em'; + this._measureElement.textContent = 'W'; + 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(), 0); + } else { + this._doMeasure(); + } + } + + private _doMeasure(): void { + const oldWidth = this._width; + const oldHeight = this._height; + const geometry = this._measureElement.getBoundingClientRect(); + + if (this._width !== geometry.width || this._height !== geometry.height) { + this._width = geometry.width; + this._height = geometry.height; + this.emit('charsizechanged'); + } + } +} diff --git a/src/utils/Generic.js b/src/utils/Generic.ts similarity index 88% rename from src/utils/Generic.js rename to src/utils/Generic.ts index 42f876f3..ce09c1be 100644 --- a/src/utils/Generic.js +++ b/src/utils/Generic.ts @@ -9,6 +9,6 @@ * @param {Array} array The array to search for the given element. * @param {Object} el The element to look for into the array */ -export let contains = function(arr, el) { +export function contains(arr: any[], el: any) { return arr.indexOf(el) >= 0; }; diff --git a/src/xterm.css b/src/xterm.css index 4877f86e..3f4f4bbe 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -116,6 +116,10 @@ overflow-y: scroll; } +.terminal .xterm-wide-char { + display: inline-block; +} + .terminal .xterm-rows { position: absolute; left: 0; diff --git a/src/xterm.js b/src/xterm.js index 1d220652..353b9618 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -10,11 +10,13 @@ * @license MIT */ -import { CompositionHelper } from './CompositionHelper.js'; -import { EventEmitter } from './EventEmitter.js'; -import { Viewport } from './Viewport.js'; -import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard.js'; -import { CircularList } from './utils/CircularList.js'; +import { CompositionHelper } from './CompositionHelper'; +import { EventEmitter } from './EventEmitter'; +import { Viewport } from './Viewport'; +import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard'; +import { CircularList } from './utils/CircularList'; +import { C0 } from './EscapeSequences'; +import { CharMeasure } from './utils/CharMeasure'; import * as Browser from './utils/Browser'; import * as Keyboard from './utils/Keyboard'; @@ -37,6 +39,26 @@ var document = (typeof window != 'undefined') ? window.document : null; */ var normal = 0, escaped = 1, csi = 2, osc = 3, charset = 4, dcs = 5, ignore = 6; +/** + * The amount of write requests to queue before sending an XOFF signal to the + * pty process. This number must be small in order for ^C and similar sequences + * to be responsive. + */ +var WRITE_BUFFER_PAUSE_THRESHOLD = 5; + +/** + * The number of writes to perform in a single batch before allowing the + * renderer to catch up with a 0ms setTimeout. + */ +var WRITE_BATCH_SIZE = 300; + +/** + * The maximum number of refresh frames to skip when the write buffer is non- + * empty. Note that these frames may be intermingled with frames that are + * skipped via requestAnimationFrame's mechanism. + */ +var MAX_REFRESH_FRAME_SKIP = 5; + /** * Terminal */ @@ -137,14 +159,8 @@ function Terminal(options) { */ this.y = 0; - /** - * Used to debounce the refresh function - */ - this.isRefreshing = false; - - /** - * Whether there is a full terminal refresh queued - */ + /** A queue of the rows to be refreshed */ + this.refreshRowsQueue = []; this.cursorState = 0; this.cursorHidden = false; @@ -202,6 +218,22 @@ function Terminal(options) { this.prefix = ''; this.postfix = ''; + // user input states + this.writeBuffer = []; + this.writeInProgress = false; + this.refreshFramesSkipped = 0; + + /** + * Whether _xterm.js_ sent XOFF in order to catch up with the pty process. + * This is a distinct state from writeStopped so that if the user requested + * XOFF via ^S that it will not automatically resume when the writeBuffer goes + * below threshold. + */ + this.xoffSentToCatchUp = false; + + /** Whether writing has been stopped as a result of XOFF */ + this.writeStopped = false; + // leftover surrogate high from previous write invocation this.surrogate_high = ''; @@ -326,7 +358,8 @@ Terminal.defaults = { scrollback: 1000, screenKeys: false, debug: false, - cancelEvents: false + cancelEvents: false, + disableStdin: false // programFeatures: false, // focusKeys: false, }; @@ -372,6 +405,24 @@ Terminal.prototype.setOption = function(key, value) { if (!(key in Terminal.defaults)) { throw new Error('No option with key "' + key + '"'); } + switch (key) { + case 'scrollback': + if (this.options[key] !== value) { + if (this.lines.length > value) { + const amountToTrim = this.lines.length - value; + const needsRefresh = (this.ydisp - amountToTrim < 0); + this.lines.trimStart(amountToTrim); + this.ybase = Math.max(this.ybase - amountToTrim, 0); + this.ydisp = Math.max(this.ydisp - amountToTrim, 0); + if (needsRefresh) { + this.refresh(0, this.rows - 1); + } + } + this.lines.maxLength = value; + this.viewport.syncScrollArea(); + } + break; + } this[key] = value; this.options[key] = value; }; @@ -384,7 +435,7 @@ Terminal.prototype.setOption = function(key, value) { Terminal.bindFocus = function (term) { on(term.textarea, 'focus', function (ev) { if (term.sendFocus) { - term.send('\x1b[I'); + term.send(C0.ESC + '[I'); } term.element.classList.add('focus'); term.showCursor(); @@ -407,9 +458,9 @@ Terminal.prototype.blur = function() { */ Terminal.bindBlur = function (term) { on(term.textarea, 'blur', function (ev) { - term.refresh(term.y, term.y); + term.queueRefresh(term.y, term.y); if (term.sendFocus) { - term.send('\x1b[O'); + term.send(C0.ESC + '[O'); } term.element.classList.remove('focus'); Terminal.focus = null; @@ -573,20 +624,25 @@ Terminal.prototype.open = function(parent) { this.compositionHelper = new CompositionHelper(this.textarea, this.compositionView, this); this.helperContainer.appendChild(this.compositionView); - this.charMeasureElement = document.createElement('div'); - this.charMeasureElement.classList.add('xterm-char-measure-element'); - this.charMeasureElement.innerHTML = 'W'; - this.helperContainer.appendChild(this.charMeasureElement); + this.charSizeStyleElement = document.createElement('style'); + this.helperContainer.appendChild(this.charSizeStyleElement); for (; i < this.rows; i++) { this.insertRow(); } this.parent.appendChild(this.element); - this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasureElement); + this.charMeasure = new CharMeasure(this.helperContainer); + this.charMeasure.on('charsizechanged', function () { + self.updateCharSizeCSS(); + }); + this.charMeasure.measure(); - // Draw the screen. - this.refresh(0, this.rows - 1); + this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); + + // Setup loop that draws to screen + this.queueRefresh(0, this.rows - 1); + this.refreshLoop(); // Initialize global actions that // need to be taken on the document. @@ -641,6 +697,13 @@ Terminal.loadAddon = function(addon, callback) { } }; +/** + * Updates the helper CSS class with any changes necessary after the terminal's + * character width has been changed. + */ +Terminal.prototype.updateCharSizeCSS = function() { + this.charSizeStyleElement.textContent = '.xterm-wide-char{width:' + (this.charMeasure.width * 2) + 'px;}'; +} /** * XTerm mouse events @@ -742,7 +805,7 @@ Terminal.prototype.bindMouse = function() { button &= 3; pos.x -= 32; pos.y -= 32; - var data = '\x1b[24'; + var data = C0.ESC + '[24'; if (button === 0) data += '1'; else if (button === 1) data += '3'; else if (button === 2) data += '5'; @@ -762,7 +825,7 @@ Terminal.prototype.bindMouse = function() { else if (button === 1) button = 4; else if (button === 2) button = 6; else if (button === 3) button = 3; - self.send('\x1b[' + self.send(C0.ESC + '[' + button + ';' + (button === 3 ? 4 : 0) @@ -781,14 +844,14 @@ Terminal.prototype.bindMouse = function() { pos.y -= 32; pos.x++; pos.y++; - self.send('\x1b[' + button + ';' + pos.x + ';' + pos.y + 'M'); + self.send(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M'); return; } if (self.sgrMouse) { pos.x -= 32; pos.y -= 32; - self.send('\x1b[<' + self.send(C0.ESC + '[<' + (((button & 3) === 3 ? button & ~3 : button) - 32) + ';' + pos.x @@ -804,7 +867,7 @@ Terminal.prototype.bindMouse = function() { encode(data, pos.x); encode(data, pos.y); - self.send('\x1b[M' + String.fromCharCode.apply(String, data)); + self.send(C0.ESC + '[M' + String.fromCharCode.apply(String, data)); } function getButton(ev) { @@ -997,6 +1060,57 @@ Terminal.flags = { INVISIBLE: 16 } +/** + * Queues a refresh between two rows (inclusive), to be done on next animation + * frame. + * @param {number} start The start row. + * @param {number} end The end row. + */ +Terminal.prototype.queueRefresh = function(start, end) { + this.refreshRowsQueue.push({ start: start, end: end }); +} + +/** + * Performs the refresh loop callback, calling refresh only if a refresh is + * necessary before queueing up the next one. + */ +Terminal.prototype.refreshLoop = function() { + // Don't refresh if there were no row changes + if (this.refreshRowsQueue.length > 0) { + // Skip MAX_REFRESH_FRAME_SKIP frames if the writeBuffer is non-empty as it + // will need to be immediately refreshed anyway. This saves a lot of + // rendering time as the viewport DOM does not need to be refreshed, no + // scroll events, no layouts, etc. + var skipFrame = this.writeBuffer.length > 0 && this.refreshFramesSkipped++ <= MAX_REFRESH_FRAME_SKIP; + + if (!skipFrame) { + this.refreshFramesSkipped = 0; + var start; + var end; + if (this.refreshRowsQueue.length > 4) { + // Just do a full refresh when 5+ refreshes are queued + start = 0; + end = this.rows - 1; + } else { + // Get start and end rows that need refreshing + start = this.refreshRowsQueue[0].start; + end = this.refreshRowsQueue[0].end; + for (var i = 1; i < this.refreshRowsQueue.length; i++) { + if (this.refreshRowsQueue[i].start < start) { + start = this.refreshRowsQueue[i].start; + } + if (this.refreshRowsQueue[i].end > end) { + end = this.refreshRowsQueue[i].end; + } + } + } + this.refreshRowsQueue = []; + this.refresh(start, end); + } + } + window.requestAnimationFrame(this.refreshLoop.bind(this)); +} + /** * Refreshes (re-renders) terminal content within two rows (inclusive) * @@ -1017,47 +1131,10 @@ Terminal.flags = { * * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) - * @param {boolean} queue Whether the refresh should ran right now or be queued */ -Terminal.prototype.refresh = function(start, end, queue) { +Terminal.prototype.refresh = function(start, end) { var self = this; - // queue defaults to true - queue = (typeof queue == 'undefined') ? true : queue; - - /** - * The refresh queue allows refresh to execute only approximately 30 times a second. For - * commands that pass a significant amount of output to the write function, this prevents the - * terminal from maxing out the CPU and making the UI unresponsive. While commands can still - * run beyond what they do on the terminal, it is far better with a debounce in place as - * every single terminal manipulation does not need to be constructed in the DOM. - * - * A side-effect of this is that it makes ^C to interrupt a process seem more responsive. - */ - if (queue) { - // If refresh should be queued, order the refresh and return. - if (this._refreshIsQueued) { - // If a refresh has already been queued, just order a full refresh next - this._fullRefreshNext = true; - } else { - setTimeout(function () { - self.refresh(start, end, false); - }, 34) - this._refreshIsQueued = true; - } - return; - } - - // If refresh should be run right now (not be queued), release the lock - this._refreshIsQueued = false; - - // If multiple refreshes were requested, make a full refresh. - if (this._fullRefreshNext) { - start = 0; - end = this.rows - 1; - this._fullRefreshNext = false // reset lock - } - var x, y, i, line, out, ch, ch_width, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement; // If this is a big refresh, remove the terminal rows from the DOM for faster calculations @@ -1080,6 +1157,10 @@ Terminal.prototype.refresh = function(start, end, queue) { row = y + this.ydisp; line = this.lines.get(row); + if (!line || !this.children[y]) { + // Continue if the line is not available, this means a resize is currently in progress + continue; + } out = ''; if (this.y === y - (this.ybase - this.ydisp) @@ -1094,6 +1175,10 @@ Terminal.prototype.refresh = function(start, end, queue) { i = 0; for (; i < width; i++) { + if (!line[i]) { + // Continue if the character is not available, this means a resize is currently in progress + continue; + } data = line[i][0]; ch = line[i][1]; ch_width = line[i][2]; @@ -1182,6 +1267,9 @@ Terminal.prototype.refresh = function(start, end, queue) { } } + if (ch_width === 2) { + out += ''; + } switch (ch) { case '&': out += '&'; @@ -1200,6 +1288,9 @@ Terminal.prototype.refresh = function(start, end, queue) { } break; } + if (ch_width === 2) { + out += ''; + } attr = data; } @@ -1224,7 +1315,7 @@ Terminal.prototype.refresh = function(start, end, queue) { Terminal.prototype.showCursor = function() { if (!this.cursorState) { this.cursorState = 1; - this.refresh(this.y, this.y); + this.queueRefresh(this.y, this.y); } }; @@ -1234,6 +1325,15 @@ Terminal.prototype.showCursor = function() { Terminal.prototype.scroll = function() { var row; + // Make room for the new row in lines + if (this.lines.length === this.lines.maxLength) { + this.lines.trimStart(1); + this.ybase--; + if (this.ydisp !== 0) { + this.ydisp--; + } + } + this.ybase++; // TODO: Why is this done twice? @@ -1248,13 +1348,6 @@ Terminal.prototype.scroll = function() { row -= this.rows - 1 - this.scrollBottom; if (row === this.lines.length) { - // Compensate ybase and ydisp if lines has hit the maximum buffer size - if (this.lines.length === this.lines.maxLength) { - this.ybase--; - if (this.ydisp !== 0) { - this.ydisp--; - } - } // Optimization: pushing is faster than splicing when they amount to the same behavior this.lines.push(this.blankLine()); } else { @@ -1311,7 +1404,7 @@ Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) { this.emit('scroll', this.ydisp); } - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); }; /** @@ -1341,745 +1434,780 @@ Terminal.prototype.scrollToBottom = function() { * @param {string} text The text to write to the terminal. */ Terminal.prototype.write = function(data) { - var l = data.length, i = 0, j, cs, ch, code, low, ch_width, row; + this.writeBuffer.push(data); - this.refreshStart = this.y; - this.refreshEnd = this.y; - - // apply leftover surrogate high from last write - if (this.surrogate_high) { - data = this.surrogate_high + data; - this.surrogate_high = ''; + // Send XOFF to pause the pty process if the write buffer becomes too large so + // xterm.js can catch up before more data is sent. This is necessary in order + // to keep signals such as ^C responsive. + if (!this.xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) { + // XOFF - stop pty pipe + // XON will be triggered by emulator before processing data chunk + this.send(C0.DC3); + this.xoffSentToCatchUp = true; } - for (; i < l; i++) { - ch = data[i]; + if (!this.writeInProgress && this.writeBuffer.length > 0) { + // Kick off a write which will write all data in sequence recursively + this.writeInProgress = true; + // Kick off an async innerWrite so more writes can come in while processing data + var self = this; + setTimeout(function () { + self.innerWrite(); + }); + } +} - // FIXME: higher chars than 0xa0 are not allowed in escape sequences - // --> maybe move to default - code = data.charCodeAt(i); - if (0xD800 <= code && code <= 0xDBFF) { - // we got a surrogate high - // get surrogate low (next 2 bytes) - low = data.charCodeAt(i+1); - if (isNaN(low)) { - // end of data stream, save surrogate high - this.surrogate_high = ch; - continue; - } - code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; - ch += data.charAt(i+1); +Terminal.prototype.innerWrite = function() { + var writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE); + while (writeBatch.length > 0) { + var data = writeBatch.shift(); + var l = data.length, i = 0, j, cs, ch, code, low, ch_width, row; + + // If XOFF was sent in order to catch up with the pty process, resume it if + // the writeBuffer is empty to allow more data to come in. + if (this.xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) { + this.send(C0.DC1); + this.xoffSentToCatchUp = false; } - // surrogate low - already handled above - if (0xDC00 <= code && code <= 0xDFFF) - continue; - switch (this.state) { - case normal: - switch (ch) { - case '\x07': - this.bell(); - break; - // '\n', '\v', '\f' - case '\n': - case '\x0b': - case '\x0c': - if (this.convertEol) { + this.refreshStart = this.y; + this.refreshEnd = this.y; + + // apply leftover surrogate high from last write + if (this.surrogate_high) { + data = this.surrogate_high + data; + this.surrogate_high = ''; + } + + for (; i < l; i++) { + ch = data[i]; + + // FIXME: higher chars than 0xa0 are not allowed in escape sequences + // --> maybe move to default + code = data.charCodeAt(i); + if (0xD800 <= code && code <= 0xDBFF) { + // we got a surrogate high + // get surrogate low (next 2 bytes) + low = data.charCodeAt(i+1); + if (isNaN(low)) { + // end of data stream, save surrogate high + this.surrogate_high = ch; + continue; + } + code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; + ch += data.charAt(i+1); + } + + // surrogate low - already handled above + if (0xDC00 <= code && code <= 0xDFFF) + continue; + switch (this.state) { + case normal: + switch (ch) { + case C0.BEL: + this.bell(); + break; + + // '\n', '\v', '\f' + case C0.LF: + case C0.VT: + case C0.FF: + if (this.convertEol) { + this.x = 0; + } + this.y++; + if (this.y > this.scrollBottom) { + this.y--; + this.scroll(); + } + break; + + // '\r' + case '\r': this.x = 0; - } - this.y++; - if (this.y > this.scrollBottom) { - this.y--; - this.scroll(); - } - break; + break; - // '\r' - case '\r': - this.x = 0; - break; - - // '\b' - case '\x08': - if (this.x > 0) { - this.x--; - } - break; - - // '\t' - case '\t': - this.x = this.nextStop(); - break; - - // shift out - case '\x0e': - this.setgLevel(1); - break; - - // shift in - case '\x0f': - this.setgLevel(0); - break; - - // '\e' - case '\x1b': - this.state = escaped; - break; - - default: - // ' ' - // calculate print space - // expensive call, therefore we save width in line buffer - ch_width = wcwidth(code); - - if (ch >= ' ') { - if (this.charset && this.charset[ch]) { - ch = this.charset[ch]; + // '\b' + case C0.BS: + if (this.x > 0) { + this.x--; } + break; - row = this.y + this.ybase; + // '\t' + case C0.HT: + this.x = this.nextStop(); + break; - // insert combining char in last cell - // FIXME: needs handling after cursor jumps - if (!ch_width && this.x) { - // dont overflow left - if (this.lines.get(row)[this.x-1]) { - if (!this.lines.get(row)[this.x-1][2]) { + // shift out + case C0.SO: + this.setgLevel(1); + break; - // found empty cell after fullwidth, need to go 2 cells back - if (this.lines.get(row)[this.x-2]) - this.lines.get(row)[this.x-2][1] += ch; + // shift in + case C0.SI: + this.setgLevel(0); + break; + // '\e' + case C0.ESC: + this.state = escaped; + break; + + default: + // ' ' + // calculate print space + // expensive call, therefore we save width in line buffer + ch_width = wcwidth(code); + + if (ch >= ' ') { + if (this.charset && this.charset[ch]) { + ch = this.charset[ch]; + } + + row = this.y + this.ybase; + + // insert combining char in last cell + // FIXME: needs handling after cursor jumps + if (!ch_width && this.x) { + // dont overflow left + if (this.lines.get(row)[this.x-1]) { + if (!this.lines.get(row)[this.x-1][2]) { + + // found empty cell after fullwidth, need to go 2 cells back + if (this.lines.get(row)[this.x-2]) + this.lines.get(row)[this.x-2][1] += ch; + + } else { + this.lines.get(row)[this.x-1][1] += ch; + } + this.updateRange(this.y); + } + break; + } + + // goto next line if ch would overflow + // TODO: needs a global min terminal width of 2 + if (this.x+ch_width-1 >= this.cols) { + // autowrap - DECAWM + if (this.wraparoundMode) { + this.x = 0; + this.y++; + if (this.y > this.scrollBottom) { + this.y--; + this.scroll(); + } } else { - this.lines.get(row)[this.x-1][1] += ch; + this.x = this.cols-1; + if(ch_width===2) // FIXME: check for xterm behavior + continue; } - this.updateRange(this.y); } - break; - } + row = this.y + this.ybase; - // goto next line if ch would overflow - // TODO: needs a global min terminal width of 2 - if (this.x+ch_width-1 >= this.cols) { - // autowrap - DECAWM - if (this.wraparoundMode) { - this.x = 0; - this.y++; - if (this.y > this.scrollBottom) { - this.y--; - this.scroll(); + // insert mode: move characters to right + if (this.insertMode) { + // do this twice for a fullwidth char + for (var moves=0; moves Normal Keypad (DECKPNM). + case '>': + this.log('Switching back to normal keypad.'); + this.applicationKeypad = false; + this.viewport.syncScrollArea(); + this.state = normal; + break; + + default: + this.state = normal; + this.error('Unknown ESC control: %s.', ch); + break; + } + break; + + case charset: + switch (ch) { + case '0': // DEC Special Character and Line Drawing Set. + cs = Terminal.charsets.SCLD; + break; + case 'A': // UK + cs = Terminal.charsets.UK; + break; + case 'B': // United States (USASCII). + cs = Terminal.charsets.US; + break; + case '4': // Dutch + cs = Terminal.charsets.Dutch; + break; + case 'C': // Finnish + case '5': + cs = Terminal.charsets.Finnish; + break; + case 'R': // French + cs = Terminal.charsets.French; + break; + case 'Q': // FrenchCanadian + cs = Terminal.charsets.FrenchCanadian; + break; + case 'K': // German + cs = Terminal.charsets.German; + break; + case 'Y': // Italian + cs = Terminal.charsets.Italian; + break; + case 'E': // NorwegianDanish + case '6': + cs = Terminal.charsets.NorwegianDanish; + break; + case 'Z': // Spanish + cs = Terminal.charsets.Spanish; + break; + case 'H': // Swedish + case '7': + cs = Terminal.charsets.Swedish; + break; + case '=': // Swiss + cs = Terminal.charsets.Swiss; + break; + case '/': // ISOLatin (actually /A) + cs = Terminal.charsets.ISOLatin; + i++; + break; + default: // Default + cs = Terminal.charsets.US; + break; + } + this.setgCharset(this.gcharset, cs); + this.gcharset = null; + this.state = normal; + break; + + case osc: + // OSC Ps ; Pt ST + // OSC Ps ; Pt BEL + // Set Text Parameters. + if (ch === C0.ESC || ch === C0.BEL) { + if (ch === C0.ESC) i++; + + this.params.push(this.currentParam); + + switch (this.params[0]) { + case 0: + case 1: + case 2: + if (this.params[1]) { + this.title = this.params[1]; + this.handleTitle(this.title); + } break; - case ')': - this.gcharset = 1; + case 3: + // set X property break; - case '*': - this.gcharset = 2; + case 4: + case 5: + // change dynamic colors break; - case '+': - this.gcharset = 3; + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + case 18: + case 19: + // change dynamic ui colors break; - case '-': - this.gcharset = 1; + case 46: + // change log file break; - case '.': - this.gcharset = 2; + case 50: + // dynamic font + break; + case 51: + // emacs shell + break; + case 52: + // manipulate selection data + break; + case 104: + case 105: + case 110: + case 111: + case 112: + case 113: + case 114: + case 115: + case 116: + case 117: + case 118: + // reset colors break; } - this.state = charset; - break; - // Designate G3 Character Set (VT300). - // A = ISO Latin-1 Supplemental. - // Not implemented. - case '/': - this.gcharset = 3; - this.state = charset; - i--; - break; - - // ESC N - // Single Shift Select of G2 Character Set - // ( SS2 is 0x8e). This affects next character only. - case 'N': - break; - // ESC O - // Single Shift Select of G3 Character Set - // ( SS3 is 0x8f). This affects next character only. - case 'O': - break; - // ESC n - // Invoke the G2 Character Set as GL (LS2). - case 'n': - this.setgLevel(2); - break; - // ESC o - // Invoke the G3 Character Set as GL (LS3). - case 'o': - this.setgLevel(3); - break; - // ESC | - // Invoke the G3 Character Set as GR (LS3R). - case '|': - this.setgLevel(3); - break; - // ESC } - // Invoke the G2 Character Set as GR (LS2R). - case '}': - this.setgLevel(2); - break; - // ESC ~ - // Invoke the G1 Character Set as GR (LS1R). - case '~': - this.setgLevel(1); - break; - - // ESC 7 Save Cursor (DECSC). - case '7': - this.saveCursor(); + this.params = []; + this.currentParam = 0; this.state = normal; - break; + } else { + if (!this.params.length) { + if (ch >= '0' && ch <= '9') { + this.currentParam = + this.currentParam * 10 + ch.charCodeAt(0) - 48; + } else if (ch === ';') { + this.params.push(this.currentParam); + this.currentParam = ''; + } + } else { + this.currentParam += ch; + } + } + break; - // ESC 8 Restore Cursor (DECRC). - case '8': - this.restoreCursor(); - this.state = normal; + case csi: + // '?', '>', '!' + if (ch === '?' || ch === '>' || ch === '!') { + this.prefix = ch; break; + } - // ESC # 3 DEC line height/width - case '#': - this.state = normal; - i++; + // 0 - 9 + if (ch >= '0' && ch <= '9') { + this.currentParam = this.currentParam * 10 + ch.charCodeAt(0) - 48; break; + } - // ESC H Tab Set (HTS is 0x88). - case 'H': - this.tabSet(); + // '$', '"', ' ', '\'' + if (ch === '$' || ch === '"' || ch === ' ' || ch === '\'') { + this.postfix = ch; break; - - // ESC = Application Keypad (DECKPAM). - case '=': - this.log('Serial port requested application keypad.'); - this.applicationKeypad = true; - this.viewport.syncScrollArea(); - this.state = normal; - break; - - // ESC > Normal Keypad (DECKPNM). - case '>': - this.log('Switching back to normal keypad.'); - this.applicationKeypad = false; - this.viewport.syncScrollArea(); - this.state = normal; - break; - - default: - this.state = normal; - this.error('Unknown ESC control: %s.', ch); - break; - } - break; - - case charset: - switch (ch) { - case '0': // DEC Special Character and Line Drawing Set. - cs = Terminal.charsets.SCLD; - break; - case 'A': // UK - cs = Terminal.charsets.UK; - break; - case 'B': // United States (USASCII). - cs = Terminal.charsets.US; - break; - case '4': // Dutch - cs = Terminal.charsets.Dutch; - break; - case 'C': // Finnish - case '5': - cs = Terminal.charsets.Finnish; - break; - case 'R': // French - cs = Terminal.charsets.French; - break; - case 'Q': // FrenchCanadian - cs = Terminal.charsets.FrenchCanadian; - break; - case 'K': // German - cs = Terminal.charsets.German; - break; - case 'Y': // Italian - cs = Terminal.charsets.Italian; - break; - case 'E': // NorwegianDanish - case '6': - cs = Terminal.charsets.NorwegianDanish; - break; - case 'Z': // Spanish - cs = Terminal.charsets.Spanish; - break; - case 'H': // Swedish - case '7': - cs = Terminal.charsets.Swedish; - break; - case '=': // Swiss - cs = Terminal.charsets.Swiss; - break; - case '/': // ISOLatin (actually /A) - cs = Terminal.charsets.ISOLatin; - i++; - break; - default: // Default - cs = Terminal.charsets.US; - break; - } - this.setgCharset(this.gcharset, cs); - this.gcharset = null; - this.state = normal; - break; - - case osc: - // OSC Ps ; Pt ST - // OSC Ps ; Pt BEL - // Set Text Parameters. - if (ch === '\x1b' || ch === '\x07') { - if (ch === '\x1b') i++; + } this.params.push(this.currentParam); + this.currentParam = 0; - switch (this.params[0]) { - case 0: - case 1: - case 2: - if (this.params[1]) { - this.title = this.params[1]; - this.handleTitle(this.title); + // ';' + if (ch === ';') break; + + this.state = normal; + + switch (ch) { + // CSI Ps A + // Cursor Up Ps Times (default = 1) (CUU). + case 'A': + this.cursorUp(this.params); + break; + + // CSI Ps B + // Cursor Down Ps Times (default = 1) (CUD). + case 'B': + this.cursorDown(this.params); + break; + + // CSI Ps C + // Cursor Forward Ps Times (default = 1) (CUF). + case 'C': + this.cursorForward(this.params); + break; + + // CSI Ps D + // Cursor Backward Ps Times (default = 1) (CUB). + case 'D': + this.cursorBackward(this.params); + break; + + // CSI Ps ; Ps H + // Cursor Position [row;column] (default = [1,1]) (CUP). + case 'H': + this.cursorPos(this.params); + break; + + // CSI Ps J Erase in Display (ED). + case 'J': + this.eraseInDisplay(this.params); + break; + + // CSI Ps K Erase in Line (EL). + case 'K': + this.eraseInLine(this.params); + break; + + // CSI Pm m Character Attributes (SGR). + case 'm': + if (!this.prefix) { + this.charAttributes(this.params); } break; - case 3: - // set X property + + // CSI Ps n Device Status Report (DSR). + case 'n': + if (!this.prefix) { + this.deviceStatus(this.params); + } break; - case 4: - case 5: - // change dynamic colors + + /** + * Additions + */ + + // CSI Ps @ + // Insert Ps (Blank) Character(s) (default = 1) (ICH). + case '@': + this.insertChars(this.params); break; - case 10: - case 11: - case 12: - case 13: - case 14: - case 15: - case 16: - case 17: - case 18: - case 19: - // change dynamic ui colors + + // CSI Ps E + // Cursor Next Line Ps Times (default = 1) (CNL). + case 'E': + this.cursorNextLine(this.params); break; - case 46: - // change log file + + // CSI Ps F + // Cursor Preceding Line Ps Times (default = 1) (CNL). + case 'F': + this.cursorPrecedingLine(this.params); break; - case 50: - // dynamic font + + // CSI Ps G + // Cursor Character Absolute [column] (default = [row,1]) (CHA). + case 'G': + this.cursorCharAbsolute(this.params); break; - case 51: - // emacs shell + + // CSI Ps L + // Insert Ps Line(s) (default = 1) (IL). + case 'L': + this.insertLines(this.params); break; - case 52: - // manipulate selection data + + // CSI Ps M + // Delete Ps Line(s) (default = 1) (DL). + case 'M': + this.deleteLines(this.params); break; - case 104: - case 105: - case 110: - case 111: - case 112: - case 113: - case 114: - case 115: - case 116: - case 117: - case 118: - // reset colors + + // CSI Ps P + // Delete Ps Character(s) (default = 1) (DCH). + case 'P': + this.deleteChars(this.params); break; - } - this.params = []; - this.currentParam = 0; - this.state = normal; - } else { - if (!this.params.length) { - if (ch >= '0' && ch <= '9') { - this.currentParam = - this.currentParam * 10 + ch.charCodeAt(0) - 48; - } else if (ch === ';') { - this.params.push(this.currentParam); - this.currentParam = ''; - } - } else { - this.currentParam += ch; - } - } - break; + // CSI Ps X + // Erase Ps Character(s) (default = 1) (ECH). + case 'X': + this.eraseChars(this.params); + break; - case csi: - // '?', '>', '!' - if (ch === '?' || ch === '>' || ch === '!') { - this.prefix = ch; - break; - } + // CSI Pm ` Character Position Absolute + // [column] (default = [row,1]) (HPA). + case '`': + this.charPosAbsolute(this.params); + break; - // 0 - 9 - if (ch >= '0' && ch <= '9') { - this.currentParam = this.currentParam * 10 + ch.charCodeAt(0) - 48; - break; - } + // 141 61 a * HPR - + // Horizontal Position Relative + case 'a': + this.HPositionRelative(this.params); + break; - // '$', '"', ' ', '\'' - if (ch === '$' || ch === '"' || ch === ' ' || ch === '\'') { - this.postfix = ch; - break; - } + // CSI P s c + // Send Device Attributes (Primary DA). + // CSI > P s c + // Send Device Attributes (Secondary DA) + case 'c': + this.sendDeviceAttributes(this.params); + break; - this.params.push(this.currentParam); - this.currentParam = 0; + // CSI Pm d + // Line Position Absolute [row] (default = [1,column]) (VPA). + case 'd': + this.linePosAbsolute(this.params); + break; - // ';' - if (ch === ';') break; + // 145 65 e * VPR - Vertical Position Relative + case 'e': + this.VPositionRelative(this.params); + break; - this.state = normal; + // CSI Ps ; Ps f + // Horizontal and Vertical Position [row;column] (default = + // [1,1]) (HVP). + case 'f': + this.HVPosition(this.params); + break; - switch (ch) { - // CSI Ps A - // Cursor Up Ps Times (default = 1) (CUU). - case 'A': - this.cursorUp(this.params); - break; + // CSI Pm h Set Mode (SM). + // CSI ? Pm h - mouse escape codes, cursor escape codes + case 'h': + this.setMode(this.params); + break; - // CSI Ps B - // Cursor Down Ps Times (default = 1) (CUD). - case 'B': - this.cursorDown(this.params); - break; + // CSI Pm l Reset Mode (RM). + // CSI ? Pm l + case 'l': + this.resetMode(this.params); + break; - // CSI Ps C - // Cursor Forward Ps Times (default = 1) (CUF). - case 'C': - this.cursorForward(this.params); - break; + // CSI Ps ; Ps r + // Set Scrolling Region [top;bottom] (default = full size of win- + // dow) (DECSTBM). + // CSI ? Pm r + case 'r': + this.setScrollRegion(this.params); + break; - // CSI Ps D - // Cursor Backward Ps Times (default = 1) (CUB). - case 'D': - this.cursorBackward(this.params); - break; + // CSI s + // Save cursor (ANSI.SYS). + case 's': + this.saveCursor(this.params); + break; - // CSI Ps ; Ps H - // Cursor Position [row;column] (default = [1,1]) (CUP). - case 'H': - this.cursorPos(this.params); - break; + // CSI u + // Restore cursor (ANSI.SYS). + case 'u': + this.restoreCursor(this.params); + break; - // CSI Ps J Erase in Display (ED). - case 'J': - this.eraseInDisplay(this.params); - break; + /** + * Lesser Used + */ - // CSI Ps K Erase in Line (EL). - case 'K': - this.eraseInLine(this.params); - break; + // CSI Ps I + // Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). + case 'I': + this.cursorForwardTab(this.params); + break; - // CSI Pm m Character Attributes (SGR). - case 'm': - if (!this.prefix) { - this.charAttributes(this.params); - } - break; + // CSI Ps S Scroll up Ps lines (default = 1) (SU). + case 'S': + this.scrollUp(this.params); + break; - // CSI Ps n Device Status Report (DSR). - case 'n': - if (!this.prefix) { - this.deviceStatus(this.params); - } - break; + // CSI Ps T Scroll down Ps lines (default = 1) (SD). + // CSI Ps ; Ps ; Ps ; Ps ; Ps T + // CSI > Ps; Ps T + case 'T': + // if (this.prefix === '>') { + // this.resetTitleModes(this.params); + // break; + // } + // if (this.params.length > 2) { + // this.initMouseTracking(this.params); + // break; + // } + if (this.params.length < 2 && !this.prefix) { + this.scrollDown(this.params); + } + break; - /** - * Additions - */ + // CSI Ps Z + // Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). + case 'Z': + this.cursorBackwardTab(this.params); + break; - // CSI Ps @ - // Insert Ps (Blank) Character(s) (default = 1) (ICH). - case '@': - this.insertChars(this.params); - break; + // CSI Ps b Repeat the preceding graphic character Ps times (REP). + case 'b': + this.repeatPrecedingCharacter(this.params); + break; - // CSI Ps E - // Cursor Next Line Ps Times (default = 1) (CNL). - case 'E': - this.cursorNextLine(this.params); - break; - - // CSI Ps F - // Cursor Preceding Line Ps Times (default = 1) (CNL). - case 'F': - this.cursorPrecedingLine(this.params); - break; - - // CSI Ps G - // Cursor Character Absolute [column] (default = [row,1]) (CHA). - case 'G': - this.cursorCharAbsolute(this.params); - break; - - // CSI Ps L - // Insert Ps Line(s) (default = 1) (IL). - case 'L': - this.insertLines(this.params); - break; - - // CSI Ps M - // Delete Ps Line(s) (default = 1) (DL). - case 'M': - this.deleteLines(this.params); - break; - - // CSI Ps P - // Delete Ps Character(s) (default = 1) (DCH). - case 'P': - this.deleteChars(this.params); - break; - - // CSI Ps X - // Erase Ps Character(s) (default = 1) (ECH). - case 'X': - this.eraseChars(this.params); - break; - - // CSI Pm ` Character Position Absolute - // [column] (default = [row,1]) (HPA). - case '`': - this.charPosAbsolute(this.params); - break; - - // 141 61 a * HPR - - // Horizontal Position Relative - case 'a': - this.HPositionRelative(this.params); - break; - - // CSI P s c - // Send Device Attributes (Primary DA). - // CSI > P s c - // Send Device Attributes (Secondary DA) - case 'c': - this.sendDeviceAttributes(this.params); - break; - - // CSI Pm d - // Line Position Absolute [row] (default = [1,column]) (VPA). - case 'd': - this.linePosAbsolute(this.params); - break; - - // 145 65 e * VPR - Vertical Position Relative - case 'e': - this.VPositionRelative(this.params); - break; - - // CSI Ps ; Ps f - // Horizontal and Vertical Position [row;column] (default = - // [1,1]) (HVP). - case 'f': - this.HVPosition(this.params); - break; - - // CSI Pm h Set Mode (SM). - // CSI ? Pm h - mouse escape codes, cursor escape codes - case 'h': - this.setMode(this.params); - break; - - // CSI Pm l Reset Mode (RM). - // CSI ? Pm l - case 'l': - this.resetMode(this.params); - break; - - // CSI Ps ; Ps r - // Set Scrolling Region [top;bottom] (default = full size of win- - // dow) (DECSTBM). - // CSI ? Pm r - case 'r': - this.setScrollRegion(this.params); - break; - - // CSI s - // Save cursor (ANSI.SYS). - case 's': - this.saveCursor(this.params); - break; - - // CSI u - // Restore cursor (ANSI.SYS). - case 'u': - this.restoreCursor(this.params); - break; - - /** - * Lesser Used - */ - - // CSI Ps I - // Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). - case 'I': - this.cursorForwardTab(this.params); - break; - - // CSI Ps S Scroll up Ps lines (default = 1) (SU). - case 'S': - this.scrollUp(this.params); - break; - - // CSI Ps T Scroll down Ps lines (default = 1) (SD). - // CSI Ps ; Ps ; Ps ; Ps ; Ps T - // CSI > Ps; Ps T - case 'T': - // if (this.prefix === '>') { - // this.resetTitleModes(this.params); - // break; - // } - // if (this.params.length > 2) { - // this.initMouseTracking(this.params); - // break; - // } - if (this.params.length < 2 && !this.prefix) { - this.scrollDown(this.params); - } - break; - - // CSI Ps Z - // Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). - case 'Z': - this.cursorBackwardTab(this.params); - break; - - // CSI Ps b Repeat the preceding graphic character Ps times (REP). - case 'b': - this.repeatPrecedingCharacter(this.params); - break; - - // CSI Ps g Tab Clear (TBC). - case 'g': - this.tabClear(this.params); - break; + // CSI Ps g Tab Clear (TBC). + case 'g': + this.tabClear(this.params); + break; // CSI Pm i Media Copy (MC). // CSI ? Pm i @@ -2114,272 +2242,282 @@ Terminal.prototype.write = function(data) { // CSI ? Ps$ p // Request DEC private mode (DECRQM). // CSI Ps ; Ps " p - case 'p': - switch (this.prefix) { - // case '>': - // this.setPointerMode(this.params); - // break; - case '!': - this.softReset(this.params); - break; - // case '?': - // if (this.postfix === '$') { - // this.requestPrivateMode(this.params); - // } - // break; - // default: - // if (this.postfix === '"') { - // this.setConformanceLevel(this.params); - // } else if (this.postfix === '$') { - // this.requestAnsiMode(this.params); - // } - // break; - } - break; - - // CSI Ps q Load LEDs (DECLL). - // CSI Ps SP q - // CSI Ps " q - // case 'q': - // if (this.postfix === ' ') { - // this.setCursorStyle(this.params); - // break; - // } - // if (this.postfix === '"') { - // this.setCharProtectionAttr(this.params); - // break; - // } - // this.loadLEDs(this.params); - // break; - - // CSI Ps ; Ps r - // Set Scrolling Region [top;bottom] (default = full size of win- - // dow) (DECSTBM). - // CSI ? Pm r - // CSI Pt; Pl; Pb; Pr; Ps$ r - // case 'r': // duplicate - // if (this.prefix === '?') { - // this.restorePrivateValues(this.params); - // } else if (this.postfix === '$') { - // this.setAttrInRectangle(this.params); - // } else { - // this.setScrollRegion(this.params); - // } - // break; - - // CSI s Save cursor (ANSI.SYS). - // CSI ? Pm s - // case 's': // duplicate - // if (this.prefix === '?') { - // this.savePrivateValues(this.params); - // } else { - // this.saveCursor(this.params); - // } - // break; - - // CSI Ps ; Ps ; Ps t - // CSI Pt; Pl; Pb; Pr; Ps$ t - // CSI > Ps; Ps t - // CSI Ps SP t - // case 't': - // if (this.postfix === '$') { - // this.reverseAttrInRectangle(this.params); - // } else if (this.postfix === ' ') { - // this.setWarningBellVolume(this.params); - // } else { - // if (this.prefix === '>') { - // this.setTitleModeFeature(this.params); - // } else { - // this.manipulateWindow(this.params); - // } - // } - // break; - - // CSI u Restore cursor (ANSI.SYS). - // CSI Ps SP u - // case 'u': // duplicate - // if (this.postfix === ' ') { - // this.setMarginBellVolume(this.params); - // } else { - // this.restoreCursor(this.params); - // } - // break; - - // CSI Pt; Pl; Pb; Pr; Pp; Pt; Pl; Pp$ v - // case 'v': - // if (this.postfix === '$') { - // this.copyRectagle(this.params); - // } - // break; - - // CSI Pt ; Pl ; Pb ; Pr ' w - // case 'w': - // if (this.postfix === '\'') { - // this.enableFilterRectangle(this.params); - // } - // break; - - // CSI Ps x Request Terminal Parameters (DECREQTPARM). - // CSI Ps x Select Attribute Change Extent (DECSACE). - // CSI Pc; Pt; Pl; Pb; Pr$ x - // case 'x': - // if (this.postfix === '$') { - // this.fillRectangle(this.params); - // } else { - // this.requestParameters(this.params); - // //this.__(this.params); - // } - // break; - - // CSI Ps ; Pu ' z - // CSI Pt; Pl; Pb; Pr$ z - // case 'z': - // if (this.postfix === '\'') { - // this.enableLocatorReporting(this.params); - // } else if (this.postfix === '$') { - // this.eraseRectangle(this.params); - // } - // break; - - // CSI Pm ' { - // CSI Pt; Pl; Pb; Pr$ { - // case '{': - // if (this.postfix === '\'') { - // this.setLocatorEvents(this.params); - // } else if (this.postfix === '$') { - // this.selectiveEraseRectangle(this.params); - // } - // break; - - // CSI Ps ' | - // case '|': - // if (this.postfix === '\'') { - // this.requestLocatorPosition(this.params); - // } - // break; - - // CSI P m SP } - // Insert P s Column(s) (default = 1) (DECIC), VT420 and up. - // case '}': - // if (this.postfix === ' ') { - // this.insertColumns(this.params); - // } - // break; - - // CSI P m SP ~ - // Delete P s Column(s) (default = 1) (DECDC), VT420 and up - // case '~': - // if (this.postfix === ' ') { - // this.deleteColumns(this.params); - // } - // break; - - default: - this.error('Unknown CSI code: %s.', ch); - break; - } - - this.prefix = ''; - this.postfix = ''; - break; - - case dcs: - if (ch === '\x1b' || ch === '\x07') { - if (ch === '\x1b') i++; - - switch (this.prefix) { - // User-Defined Keys (DECUDK). - case '': - break; - - // Request Status String (DECRQSS). - // test: echo -e '\eP$q"p\e\\' - case '$q': - var pt = this.currentParam - , valid = false; - - switch (pt) { - // DECSCA - case '"q': - pt = '0"q'; - break; - - // DECSCL - case '"p': - pt = '61"p'; - break; - - // DECSTBM - case 'r': - pt = '' - + (this.scrollTop + 1) - + ';' - + (this.scrollBottom + 1) - + 'r'; - break; - - // SGR - case 'm': - pt = '0m'; - break; - - default: - this.error('Unknown DCS Pt: %s.', pt); - pt = ''; + case 'p': + switch (this.prefix) { + // case '>': + // this.setPointerMode(this.params); + // break; + case '!': + this.softReset(this.params); break; + // case '?': + // if (this.postfix === '$') { + // this.requestPrivateMode(this.params); + // } + // break; + // default: + // if (this.postfix === '"') { + // this.setConformanceLevel(this.params); + // } else if (this.postfix === '$') { + // this.requestAnsiMode(this.params); + // } + // break; } - - this.send('\x1bP' + +valid + '$r' + pt + '\x1b\\'); break; - // Set Termcap/Terminfo Data (xterm, experimental). - case '+p': - break; + // CSI Ps q Load LEDs (DECLL). + // CSI Ps SP q + // CSI Ps " q + // case 'q': + // if (this.postfix === ' ') { + // this.setCursorStyle(this.params); + // break; + // } + // if (this.postfix === '"') { + // this.setCharProtectionAttr(this.params); + // break; + // } + // this.loadLEDs(this.params); + // break; - // Request Termcap/Terminfo String (xterm, experimental) - // Regular xterm does not even respond to this sequence. - // This can cause a small glitch in vim. - // test: echo -ne '\eP+q6b64\e\\' - case '+q': - var pt = this.currentParam - , valid = false; + // CSI Ps ; Ps r + // Set Scrolling Region [top;bottom] (default = full size of win- + // dow) (DECSTBM). + // CSI ? Pm r + // CSI Pt; Pl; Pb; Pr; Ps$ r + // case 'r': // duplicate + // if (this.prefix === '?') { + // this.restorePrivateValues(this.params); + // } else if (this.postfix === '$') { + // this.setAttrInRectangle(this.params); + // } else { + // this.setScrollRegion(this.params); + // } + // break; - this.send('\x1bP' + +valid + '+r' + pt + '\x1b\\'); - break; + // CSI s Save cursor (ANSI.SYS). + // CSI ? Pm s + // case 's': // duplicate + // if (this.prefix === '?') { + // this.savePrivateValues(this.params); + // } else { + // this.saveCursor(this.params); + // } + // break; + + // CSI Ps ; Ps ; Ps t + // CSI Pt; Pl; Pb; Pr; Ps$ t + // CSI > Ps; Ps t + // CSI Ps SP t + // case 't': + // if (this.postfix === '$') { + // this.reverseAttrInRectangle(this.params); + // } else if (this.postfix === ' ') { + // this.setWarningBellVolume(this.params); + // } else { + // if (this.prefix === '>') { + // this.setTitleModeFeature(this.params); + // } else { + // this.manipulateWindow(this.params); + // } + // } + // break; + + // CSI u Restore cursor (ANSI.SYS). + // CSI Ps SP u + // case 'u': // duplicate + // if (this.postfix === ' ') { + // this.setMarginBellVolume(this.params); + // } else { + // this.restoreCursor(this.params); + // } + // break; + + // CSI Pt; Pl; Pb; Pr; Pp; Pt; Pl; Pp$ v + // case 'v': + // if (this.postfix === '$') { + // this.copyRectagle(this.params); + // } + // break; + + // CSI Pt ; Pl ; Pb ; Pr ' w + // case 'w': + // if (this.postfix === '\'') { + // this.enableFilterRectangle(this.params); + // } + // break; + + // CSI Ps x Request Terminal Parameters (DECREQTPARM). + // CSI Ps x Select Attribute Change Extent (DECSACE). + // CSI Pc; Pt; Pl; Pb; Pr$ x + // case 'x': + // if (this.postfix === '$') { + // this.fillRectangle(this.params); + // } else { + // this.requestParameters(this.params); + // //this.__(this.params); + // } + // break; + + // CSI Ps ; Pu ' z + // CSI Pt; Pl; Pb; Pr$ z + // case 'z': + // if (this.postfix === '\'') { + // this.enableLocatorReporting(this.params); + // } else if (this.postfix === '$') { + // this.eraseRectangle(this.params); + // } + // break; + + // CSI Pm ' { + // CSI Pt; Pl; Pb; Pr$ { + // case '{': + // if (this.postfix === '\'') { + // this.setLocatorEvents(this.params); + // } else if (this.postfix === '$') { + // this.selectiveEraseRectangle(this.params); + // } + // break; + + // CSI Ps ' | + // case '|': + // if (this.postfix === '\'') { + // this.requestLocatorPosition(this.params); + // } + // break; + + // CSI P m SP } + // Insert P s Column(s) (default = 1) (DECIC), VT420 and up. + // case '}': + // if (this.postfix === ' ') { + // this.insertColumns(this.params); + // } + // break; + + // CSI P m SP ~ + // Delete P s Column(s) (default = 1) (DECDC), VT420 and up + // case '~': + // if (this.postfix === ' ') { + // this.deleteColumns(this.params); + // } + // break; default: - this.error('Unknown DCS prefix: %s.', this.prefix); + this.error('Unknown CSI code: %s.', ch); break; } - this.currentParam = 0; this.prefix = ''; - this.state = normal; - } else if (!this.currentParam) { - if (!this.prefix && ch !== '$' && ch !== '+') { - this.currentParam = ch; - } else if (this.prefix.length === 2) { - this.currentParam = ch; + this.postfix = ''; + break; + + case dcs: + if (ch === C0.ESC || ch === C0.BEL) { + if (ch === C0.ESC) i++; + + switch (this.prefix) { + // User-Defined Keys (DECUDK). + case '': + break; + + // Request Status String (DECRQSS). + // test: echo -e '\eP$q"p\e\\' + case '$q': + var pt = this.currentParam + , valid = false; + + switch (pt) { + // DECSCA + case '"q': + pt = '0"q'; + break; + + // DECSCL + case '"p': + pt = '61"p'; + break; + + // DECSTBM + case 'r': + pt = '' + + (this.scrollTop + 1) + + ';' + + (this.scrollBottom + 1) + + 'r'; + break; + + // SGR + case 'm': + pt = '0m'; + break; + + default: + this.error('Unknown DCS Pt: %s.', pt); + pt = ''; + break; + } + + this.send(C0.ESC + 'P' + +valid + '$r' + pt + C0.ESC + '\\'); + break; + + // Set Termcap/Terminfo Data (xterm, experimental). + case '+p': + break; + + // Request Termcap/Terminfo String (xterm, experimental) + // Regular xterm does not even respond to this sequence. + // This can cause a small glitch in vim. + // test: echo -ne '\eP+q6b64\e\\' + case '+q': + var pt = this.currentParam + , valid = false; + + this.send(C0.ESC + 'P' + +valid + '+r' + pt + C0.ESC + '\\'); + break; + + default: + this.error('Unknown DCS prefix: %s.', this.prefix); + break; + } + + this.currentParam = 0; + this.prefix = ''; + this.state = normal; + } else if (!this.currentParam) { + if (!this.prefix && ch !== '$' && ch !== '+') { + this.currentParam = ch; + } else if (this.prefix.length === 2) { + this.currentParam = ch; + } else { + this.prefix += ch; + } } else { - this.prefix += ch; + this.currentParam += ch; } - } else { - this.currentParam += ch; - } - break; + break; - case ignore: - // For PM and APC. - if (ch === '\x1b' || ch === '\x07') { - if (ch === '\x1b') i++; - this.state = normal; - } - break; + case ignore: + // For PM and APC. + if (ch === C0.ESC || ch === C0.BEL) { + if (ch === C0.ESC) i++; + this.state = normal; + } + break; + } } - } - this.updateRange(this.y); - this.refresh(this.refreshStart, this.refreshEnd); + this.updateRange(this.y); + this.queueRefresh(this.refreshStart, this.refreshEnd); + } + if (this.writeBuffer.length > 0) { + // Allow renderer to catch up before processing the next batch + var self = this; + setTimeout(function () { + self.innerWrite(); + }, 0); + } else { + this.writeInProgress = false; + } }; /** @@ -2423,6 +2561,12 @@ Terminal.prototype.keyDown = function(ev) { var self = this; var result = this.evaluateKeyEscapeSequence(ev); + if (result.key === C0.DC3) { // XOFF + this.writeStopped = true; + } else if (result.key === C0.DC1) { // XON + this.writeStopped = false; + } + if (result.scrollDisp) { this.scrollDisp(result.scrollDisp); return this.cancel(ev, true); @@ -2471,90 +2615,90 @@ Terminal.prototype.evaluateKeyEscapeSequence = function(ev) { case 8: // backspace if (ev.shiftKey) { - result.key = '\x08'; // ^H + result.key = C0.BS; // ^H break; } - result.key = '\x7f'; // ^? + result.key = C0.DEL; // ^? break; case 9: // tab if (ev.shiftKey) { - result.key = '\x1b[Z'; + result.key = C0.ESC + '[Z'; break; } - result.key = '\t'; + result.key = C0.HT; result.cancel = true; break; case 13: // return/enter - result.key = '\r'; + result.key = C0.CR; result.cancel = true; break; case 27: // escape - result.key = '\x1b'; + result.key = C0.ESC; result.cancel = true; break; case 37: // left-arrow if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'D'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D'; // HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards // http://unix.stackexchange.com/a/108106 // macOS uses different escape sequences than linux - if (result.key == '\x1b[1;3D') { - result.key = (this.browser.isMac) ? '\x1bb' : '\x1b[1;5D'; + if (result.key == C0.ESC + '[1;3D') { + result.key = (this.browser.isMac) ? C0.ESC + 'b' : C0.ESC + '[1;5D'; } } else if (this.applicationCursor) { - result.key = '\x1bOD'; + result.key = C0.ESC + 'OD'; } else { - result.key = '\x1b[D'; + result.key = C0.ESC + '[D'; } break; case 39: // right-arrow if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'C'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C'; // HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward // http://unix.stackexchange.com/a/108106 // macOS uses different escape sequences than linux - if (result.key == '\x1b[1;3C') { - result.key = (this.browser.isMac) ? '\x1bf' : '\x1b[1;5C'; + if (result.key == C0.ESC + '[1;3C') { + result.key = (this.browser.isMac) ? C0.ESC + 'f' : C0.ESC + '[1;5C'; } } else if (this.applicationCursor) { - result.key = '\x1bOC'; + result.key = C0.ESC + 'OC'; } else { - result.key = '\x1b[C'; + result.key = C0.ESC + '[C'; } break; case 38: // up-arrow if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'A'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A'; // HACK: Make Alt + up-arrow behave like Ctrl + up-arrow // http://unix.stackexchange.com/a/108106 - if (result.key == '\x1b[1;3A') { - result.key = '\x1b[1;5A'; + if (result.key == C0.ESC + '[1;3A') { + result.key = C0.ESC + '[1;5A'; } } else if (this.applicationCursor) { - result.key = '\x1bOA'; + result.key = C0.ESC + 'OA'; } else { - result.key = '\x1b[A'; + result.key = C0.ESC + '[A'; } break; case 40: // down-arrow if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'B'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B'; // HACK: Make Alt + down-arrow behave like Ctrl + down-arrow // http://unix.stackexchange.com/a/108106 - if (result.key == '\x1b[1;3B') { - result.key = '\x1b[1;5B'; + if (result.key == C0.ESC + '[1;3B') { + result.key = C0.ESC + '[1;5B'; } } else if (this.applicationCursor) { - result.key = '\x1bOB'; + result.key = C0.ESC + 'OB'; } else { - result.key = '\x1b[B'; + result.key = C0.ESC + '[B'; } break; case 45: @@ -2562,41 +2706,41 @@ Terminal.prototype.evaluateKeyEscapeSequence = function(ev) { if (!ev.shiftKey && !ev.ctrlKey) { // or + are used to // copy-paste on some systems. - result.key = '\x1b[2~'; + result.key = C0.ESC + '[2~'; } break; case 46: // delete if (modifiers) { - result.key = '\x1b[3;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[3;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[3~'; + result.key = C0.ESC + '[3~'; } break; case 36: // home if (modifiers) - result.key = '\x1b[1;' + (modifiers + 1) + 'H'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H'; else if (this.applicationCursor) - result.key = '\x1bOH'; + result.key = C0.ESC + 'OH'; else - result.key = '\x1b[H'; + result.key = C0.ESC + '[H'; break; case 35: // end if (modifiers) - result.key = '\x1b[1;' + (modifiers + 1) + 'F'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F'; else if (this.applicationCursor) - result.key = '\x1bOF'; + result.key = C0.ESC + 'OF'; else - result.key = '\x1b[F'; + result.key = C0.ESC + '[F'; break; case 33: // page up if (ev.shiftKey) { result.scrollDisp = -(this.rows - 1); } else { - result.key = '\x1b[5~'; + result.key = C0.ESC + '[5~'; } break; case 34: @@ -2604,92 +2748,92 @@ Terminal.prototype.evaluateKeyEscapeSequence = function(ev) { if (ev.shiftKey) { result.scrollDisp = this.rows - 1; } else { - result.key = '\x1b[6~'; + result.key = C0.ESC + '[6~'; } break; case 112: // F1-F12 if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'P'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'P'; } else { - result.key = '\x1bOP'; + result.key = C0.ESC + 'OP'; } break; case 113: if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'Q'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'Q'; } else { - result.key = '\x1bOQ'; + result.key = C0.ESC + 'OQ'; } break; case 114: if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'R'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'R'; } else { - result.key = '\x1bOR'; + result.key = C0.ESC + 'OR'; } break; case 115: if (modifiers) { - result.key = '\x1b[1;' + (modifiers + 1) + 'S'; + result.key = C0.ESC + '[1;' + (modifiers + 1) + 'S'; } else { - result.key = '\x1bOS'; + result.key = C0.ESC + 'OS'; } break; case 116: if (modifiers) { - result.key = '\x1b[15;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[15;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[15~'; + result.key = C0.ESC + '[15~'; } break; case 117: if (modifiers) { - result.key = '\x1b[17;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[17;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[17~'; + result.key = C0.ESC + '[17~'; } break; case 118: if (modifiers) { - result.key = '\x1b[18;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[18;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[18~'; + result.key = C0.ESC + '[18~'; } break; case 119: if (modifiers) { - result.key = '\x1b[19;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[19;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[19~'; + result.key = C0.ESC + '[19~'; } break; case 120: if (modifiers) { - result.key = '\x1b[20;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[20;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[20~'; + result.key = C0.ESC + '[20~'; } break; case 121: if (modifiers) { - result.key = '\x1b[21;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[21;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[21~'; + result.key = C0.ESC + '[21~'; } break; case 122: if (modifiers) { - result.key = '\x1b[23;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[23;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[23~'; + result.key = C0.ESC + '[23~'; } break; case 123: if (modifiers) { - result.key = '\x1b[24;' + (modifiers + 1) + '~'; + result.key = C0.ESC + '[24;' + (modifiers + 1) + '~'; } else { - result.key = '\x1b[24~'; + result.key = C0.ESC + '[24~'; } break; default: @@ -2719,15 +2863,16 @@ Terminal.prototype.evaluateKeyEscapeSequence = function(ev) { } else if (!this.browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) { // On Mac this is a third level shift. Use instead. if (ev.keyCode >= 65 && ev.keyCode <= 90) { - result.key = '\x1b' + String.fromCharCode(ev.keyCode + 32); + result.key = C0.ESC + String.fromCharCode(ev.keyCode + 32); } else if (ev.keyCode === 192) { - result.key = '\x1b`'; + result.key = C0.ESC + '`'; } else if (ev.keyCode >= 48 && ev.keyCode <= 57) { - result.key = '\x1b' + (ev.keyCode - 48); + result.key = C0.ESC + (ev.keyCode - 48); } } break; } + return result; }; @@ -2945,7 +3090,9 @@ Terminal.prototype.resize = function(x, y) { this.scrollTop = 0; this.scrollBottom = y - 1; - this.refresh(0, this.rows - 1); + this.charMeasure.measure(); + + this.queueRefresh(0, this.rows - 1); this.normal = null; @@ -3074,7 +3221,7 @@ Terminal.prototype.clear = function() { for (var i = 1; i < this.rows; i++) { this.lines.push(this.blankLine()); } - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.emit('scroll', this.ydisp); }; @@ -3134,6 +3281,11 @@ Terminal.prototype.is = function(term) { * @param {string} data The data to populate in the event. */ Terminal.prototype.handler = function(data) { + // Prevents all events to pty process if stdin is disabled + if (this.options.disableStdin) { + return; + } + // Input is being sent to the terminal, the terminal should focus the prompt. if (this.ybase !== this.ydisp) { this.scrollToBottom(); @@ -3205,7 +3357,7 @@ Terminal.prototype.reset = function() { var customKeydownHandler = this.customKeydownHandler; Terminal.call(this, this.options); this.customKeydownHandler = customKeydownHandler; - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.viewport.syncScrollArea(); }; @@ -3583,11 +3735,11 @@ Terminal.prototype.deviceStatus = function(params) { switch (params[0]) { case 5: // status report - this.send('\x1b[0n'); + this.send(C0.ESC + '[0n'); break; case 6: // cursor position - this.send('\x1b[' + this.send(C0.ESC + '[' + (this.y + 1) + ';' + (this.x + 1) @@ -3600,7 +3752,7 @@ Terminal.prototype.deviceStatus = function(params) { switch (params[0]) { case 6: // cursor position - this.send('\x1b[?' + this.send(C0.ESC + '[?' + (this.y + 1) + ';' + (this.x + 1) @@ -3608,19 +3760,19 @@ Terminal.prototype.deviceStatus = function(params) { break; case 15: // no printer - // this.send('\x1b[?11n'); + // this.send(C0.ESC + '[?11n'); break; case 25: // dont support user defined keys - // this.send('\x1b[?21n'); + // this.send(C0.ESC + '[?21n'); break; case 26: // north american keyboard - // this.send('\x1b[?27;1;0;0n'); + // this.send(C0.ESC + '[?27;1;0;0n'); break; case 53: // no dec locator/mouse - // this.send('\x1b[?50n'); + // this.send(C0.ESC + '[?50n'); break; } } @@ -3871,24 +4023,24 @@ Terminal.prototype.sendDeviceAttributes = function(params) { if (this.is('xterm') || this.is('rxvt-unicode') || this.is('screen')) { - this.send('\x1b[?1;2c'); + this.send(C0.ESC + '[?1;2c'); } else if (this.is('linux')) { - this.send('\x1b[?6c'); + this.send(C0.ESC + '[?6c'); } } else if (this.prefix === '>') { // xterm and urxvt // seem to spit this // out around ~370 times (?). if (this.is('xterm')) { - this.send('\x1b[>0;276;0c'); + this.send(C0.ESC + '[>0;276;0c'); } else if (this.is('rxvt-unicode')) { - this.send('\x1b[>85;95;0c'); + this.send(C0.ESC + '[>85;95;0c'); } else if (this.is('linux')) { // not supported by linux console. // linux console echoes parameters. this.send(params[0] + 'c'); } else if (this.is('screen')) { - this.send('\x1b[>83;40003;0c'); + this.send(C0.ESC + '[>83;40003;0c'); } } }; @@ -4324,7 +4476,7 @@ Terminal.prototype.resetMode = function(params) { // this.x = this.savedX; // this.y = this.savedY; // } - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.viewport.syncScrollArea(); this.showCursor(); } diff --git a/tsconfig.json b/tsconfig.json index f4a5a1b2..f5a7d66c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,13 +7,17 @@ "outDir": "lib", "sourceMap": true }, + "include": [ + "src/**/*" + ], "exclude": [ - "addons", + "src/addons/**/*", "build", "demo", "dist", "out", "test", - "node_modules" + "node_modules", + "docs" ] }