diff --git a/README.md b/README.md index 6790fea9..ae003783 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # [![xterm.js logo](logo-full.png)](https://xtermjs.org) -[![xterm.js build status](https://api.travis-ci.org/xtermjs/xterm.js.svg)](https://travis-ci.org/xtermjs/xterm.js) [![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master) [![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) +[![Travis CI build status](https://api.travis-ci.org/xtermjs/xterm.js.svg)](https://travis-ci.org/xtermjs/xterm.js) [![VSTS Build status](https://xtermjs.visualstudio.com/_apis/public/build/definitions/3e323cf7-5760-460d-af64-ee5675baf366/1/badge)](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1) [![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master) [![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. @@ -39,7 +39,7 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
@@ -149,6 +149,9 @@ computational environment for Jupyter, supporting interactive data science and s - [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies. - [**Hyper**](https://hyper.is): A terminal built on web technologies - [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter. +- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js. +- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator. + 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. @@ -247,7 +250,7 @@ To contribute either code, documentation or issues to xterm.js please read the [ ### Code structure -`src/` is roughly split up into areas of functionality such as `renderer/` that handles all rendering and `utils/` which provides general utility functions. The `shared/` folder contains code that can be used from either the main thread or a web worker thread, all code inside a `shared/` folder should only ever import other code from a `shared/` folder to minimize the amount of code run what launching a web worker. +`src/` is roughly split up into areas of functionality such as `renderer/` that handles all rendering and `utils/` which provides general utility functions. The `shared/` folder contains code that can be used from either the main thread or a web worker thread, all code inside a `shared/` folder should only ever import other code from a `shared/` folder to minimize the amount of code ran when launching a web worker. ## License Agreement diff --git a/demo/index.html b/demo/index.html index 168f56e4..f374d998 100644 --- a/demo/index.html +++ b/demo/index.html @@ -1,95 +1,37 @@ - - xterm.js demo - - - - - - - -

xterm.js: A terminal for the web

-
-
-

Actions

-

- - -

-
-
-

Options

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-
-

Size

-
-
- - -
-
- - -
-
- - -
-
-
-
-

Accessibility

-

- -

-
-

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

- - + + xterm.js demo + + + + + + + +

xterm.js: A terminal for the web

+
+
+

Actions

+

+ + +

+
+
+

Options

+

These options can be set in the Terminal constructor or using the Terminal.setOption function.

+
+
+
+

Style

+
+ + +
+
+
+

Attention: The demo is a barebones implementation and is designed for the development and evaluation of xterm.js 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 682fc5a0..13fa8e96 100644 --- a/demo/main.js +++ b/demo/main.js @@ -26,38 +26,13 @@ var terminalContainer = document.getElementById('terminal-container'), findNext: document.querySelector('#find-next'), findPrevious: document.querySelector('#find-previous') }, - optionElements = { - cursorBlink: document.querySelector('#option-cursor-blink'), - cursorStyle: document.querySelector('#option-cursor-style'), - macOptionIsMeta: document.querySelector('#option-mac-option-is-meta'), - scrollback: document.querySelector('#option-scrollback'), - transparency: document.querySelector('#option-transparency'), - tabstopwidth: document.querySelector('#option-tabstopwidth'), - experimentalCharAtlas: document.querySelector('#option-experimental-char-atlas'), - bellStyle: document.querySelector('#option-bell-style'), - screenReaderMode: document.querySelector('#option-screen-reader-mode') - }, - colsElement = document.getElementById('cols'), - rowsElement = document.getElementById('rows'), paddingElement = document.getElementById('padding'); -function setTerminalSize() { - var cols = parseInt(colsElement.value, 10); - var rows = parseInt(rowsElement.value, 10); - var width = (cols * term.renderer.dimensions.actualCellWidth + term.viewport.scrollBarWidth).toString() + 'px'; - var height = (rows * term.renderer.dimensions.actualCellHeight).toString() + 'px'; - terminalContainer.style.width = width; - terminalContainer.style.height = height; - term.fit(); -} - function setPadding() { term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px'; term.fit(); } -colsElement.addEventListener('change', setTerminalSize); -rowsElement.addEventListener('change', setTerminalSize); paddingElement.addEventListener('change', setPadding); actionElements.findNext.addEventListener('keypress', function (e) { @@ -73,36 +48,6 @@ actionElements.findPrevious.addEventListener('keypress', function (e) { } }); -optionElements.cursorBlink.addEventListener('change', function () { - term.setOption('cursorBlink', optionElements.cursorBlink.checked); -}); -optionElements.macOptionIsMeta.addEventListener('change', function () { - term.setOption('macOptionIsMeta', optionElements.macOptionIsMeta.checked); -}); -optionElements.transparency.addEventListener('change', function () { - var checked = optionElements.transparency.checked; - term.setOption('allowTransparency', checked); - term.setOption('theme', checked ? {background: 'rgba(0, 0, 0, .5)'} : {}); -}); -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)); -}); -optionElements.experimentalCharAtlas.addEventListener('change', function () { - term.setOption('experimentalCharAtlas', optionElements.experimentalCharAtlas.value); -}); -optionElements.screenReaderMode.addEventListener('change', function () { - term.setOption('screenReaderMode', optionElements.screenReaderMode.checked); -}); - createTerminal(); function createTerminal() { @@ -110,13 +55,7 @@ function createTerminal() { while (terminalContainer.children.length) { terminalContainer.removeChild(terminalContainer.children[0]); } - term = new Terminal({ - macOptionIsMeta: optionElements.macOptionIsMeta.enabled, - cursorBlink: optionElements.cursorBlink.checked, - scrollback: parseInt(optionElements.scrollback.value, 10), - tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10), - screenReaderMode: optionElements.screenReaderMode.checked - }); + term = new Terminal({}); window.term = term; // Expose `term` to window for debugging purposes term.on('resize', function (size) { if (!pid) { @@ -139,12 +78,13 @@ function createTerminal() { // fit is called within a setTimeout, cols and rows need this. setTimeout(function () { - colsElement.value = term.cols; - rowsElement.value = term.rows; + initOptions(term); + document.getElementById(`opt-cols`).value = term.cols; + document.getElementById(`opt-rows`).value = term.rows; paddingElement.value = 0; // Set terminal size again to set the specific dimensions on the demo - setTerminalSize(); + updateTerminalSize(); fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) { @@ -205,3 +145,105 @@ function runFakeTerminal() { term.write(data); }); } + +function initOptions(term) { + var blacklistedOptions = [ + // Internal only options + 'cancelEvents', + 'convertEol', + 'debug', + 'handler', + 'screenKeys', + 'termName', + 'useFlowControl', + // Complex option + 'theme', + // Only in constructor + 'rendererType' + ]; + var stringOptions = { + bellSound: null, + bellStyle: ['none', 'sound'], + cursorStyle: ['block', 'underline', 'bar'], + experimentalCharAtlas: ['none', 'static', 'dynamic'], + fontFamily: null, + fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], + fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'] + }; + var options = Object.keys(term.options); + var booleanOptions = []; + var numberOptions = []; + options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => { + switch (typeof term.getOption(o)) { + case 'boolean': + booleanOptions.push(o); + break; + case 'number': + numberOptions.push(o); + break; + default: + if (Object.keys(stringOptions).indexOf(o) === -1) { + console.warn(`Unrecognized option: "${o}"`); + } + } + }); + + var html = ''; + html += '
'; + booleanOptions.forEach(o => { + html += `
`; + }); + html += '
'; + numberOptions.forEach(o => { + html += `
`; + }); + html += '
'; + Object.keys(stringOptions).forEach(o => { + if (stringOptions[o]) { + html += `
`; + } else { + html += `
` + } + }); + html += '
'; + + var container = document.getElementById('options-container'); + container.innerHTML = html; + + // Attach listeners + booleanOptions.forEach(o => { + var input = document.getElementById(`opt-${o}`); + input.addEventListener('change', () => { + console.log('change', o, input.checked); + term.setOption(o, input.checked); + }); + }); + numberOptions.forEach(o => { + var input = document.getElementById(`opt-${o}`); + input.addEventListener('change', () => { + console.log('change', o, input.value); + if (o === 'cols' || o === 'rows') { + updateTerminalSize(); + } else { + term.setOption(o, parseInt(input.value, 10)); + } + }); + }); + Object.keys(stringOptions).forEach(o => { + var input = document.getElementById(`opt-${o}`); + input.addEventListener('change', () => { + console.log('change', o, input.value); + term.setOption(o, input.value); + }); + }); +} + +function updateTerminalSize() { + var cols = parseInt(document.getElementById(`opt-cols`).value, 10); + var rows = parseInt(document.getElementById(`opt-rows`).value, 10); + var width = (cols * term.renderer.dimensions.actualCellWidth + term.viewport.scrollBarWidth).toString() + 'px'; + var height = (rows * term.renderer.dimensions.actualCellHeight).toString() + 'px'; + terminalContainer.style.width = width; + terminalContainer.style.height = height; + term.fit(); +} diff --git a/demo/style.css b/demo/style.css index 1ab5f61c..b061dfcb 100644 --- a/demo/style.css +++ b/demo/style.css @@ -14,3 +14,19 @@ h1 { margin: 0 auto; padding: 2px; } + +p { + font-size: 0.9em; + font-style: italic +} + +#option-container { + display: flex; + justify-content: center; +} + +.option-group { + display: inline-block; + padding-left: 20px; + vertical-align: top; +} diff --git a/gulpfile.js b/gulpfile.js index fe88c355..7026ec43 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -18,7 +18,7 @@ const webpack = require('webpack-stream'); const buildDir = process.env.BUILD_DIR || 'build'; const tsProject = ts.createProject('tsconfig.json'); -const srcDir = tsProject.config.compilerOptions.rootDir; +let srcDir = tsProject.config.compilerOptions.rootDir; let outDir = tsProject.config.compilerOptions.outDir; const addons = fs.readdirSync(`${__dirname}/src/addons`); @@ -36,58 +36,19 @@ if (path.normalize(outDir).indexOf(__dirname) !== 0) { outDir = `${__dirname}/${path.normalize(outDir)}`; } -/** - * Compile TypeScript sources to JavaScript files and create a source map file for each TypeScript - * file compiled. - */ -gulp.task('tsc', function () { - // Remove the ${outDir}/ directory to prevent confusion if files were deleted in ${srcDir}/ - fs.emptyDirSync(`${outDir}`); +gulp.task('css', function() { + return gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir)); +}); - // Build all TypeScript files (including tests) to ${outDir}/, based on the configuration defined in - // `tsconfig.json`. - let tsResult = tsProject.src().pipe(sourcemaps.init()).pipe(tsProject()); - let tsc = merge( - tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir)), - tsResult.dts.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir)) - ); - - let addonStreams = addons.map(function(addon) { - fs.emptyDirSync(`${outDir}/addons/${addon}`); - - let tsProjectAddon = ts.createProject(`./src/addons/${addon}/tsconfig.json`); - let tsResultAddon = tsProjectAddon.src().pipe(sourcemaps.init()).pipe(tsProjectAddon()); - let tscAddon = merge( - tsResultAddon.js - .pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})) - .pipe(gulp.dest(`${outDir}/addons/${addon}`)), - tsResultAddon.dts - .pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})) - .pipe(gulp.dest(`${outDir}/addons/${addon}`)) - ) - - return tscAddon; - }); - - // Copy all addons from ${srcDir}/ to ${outDir}/ - let copyAddons = gulp.src([ - `${srcDir}/addons/**/**` - ]).pipe(gulp.dest(`${outDir}/addons`)); - - // Copy stylesheets from ${srcDir}/ to ${outDir}/ - let copyStylesheets = gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir)); - - // Join all streams into a single array - let streams = [tsc].concat(addonStreams).concat([copyAddons, copyStylesheets]); - - return merge.apply(this, streams); +gulp.task('watch-css', function() { + return gulp.watch(`${srcDir}/**/*.css`, ['css']); }); /** * Bundle JavaScript files produced by the `tsc` task, into a single file named `xterm.js` with * Browserify. */ -gulp.task('browserify', ['tsc'], function() { +gulp.task('browserify', function() { // Ensure that the build directory exists fs.ensureDirSync(buildDir); @@ -113,7 +74,7 @@ gulp.task('browserify', ['tsc'], function() { return merge(bundleStream, copyStylesheets); }); -gulp.task('browserify-addons', ['tsc'], function() { +gulp.task('browserify-addons', function() { const bundles = addons.map((addon) => { const addonOptions = { basedir: `${buildDir}/addons/${addon}`, @@ -182,8 +143,9 @@ gulp.task('webpack', ['build'], function() { .pipe(gulp.dest('demo/dist/')); }); -gulp.task('watch', ['webpack'], () => { - gulp.watch(['./src/*', './src/**/*'], ['webpack']); + +gulp.task('watch-demo', ['webpack'], () => { + gulp.watch(['./demo/*', './lib/**/*'], ['webpack']); }); gulp.task('build', ['sorcery', 'sorcery-addons']); diff --git a/package.json b/package.json index d42505e6..63899088 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,13 @@ "license": "MIT", "devDependencies": { "@types/chai": "^3.4.34", + "@types/glob": "^5.0.35", "@types/jsdom": "^11.0.1", "@types/mocha": "^2.2.33", "@types/node": "6.0.108", - "@types/text-encoding": "0.0.32", "browserify": "^13.3.0", "chai": "3.5.0", + "concurrently": "^3.5.1", "express": "4.13.4", "express-ws": "2.0.0-rc.1", "fs-extra": "^1.0.0", @@ -35,7 +36,7 @@ "sorcery": "^0.10.0", "tslint": "^5.9.1", "tslint-consistent-codestyle": "^1.13.0", - "typescript": "~2.7.1", + "typescript": "2.8.3", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", "webpack": "^3.10.0", @@ -43,7 +44,7 @@ "zmodem.js": "^0.1.5" }, "scripts": { - "start": "node demo/app", + "start": "concurrently --kill-others-on-fail --names \"demo,server\" \"gulp watch-demo\" \"node demo/app\"", "start-zmodem": "node demo/zmodem/app", "lint": "tslint 'src/**/*.ts'", "test": "npm-run-all mocha lint", @@ -53,10 +54,11 @@ "mocha": "gulp test", "build:docs": "jsdoc -c jsdoc.json", "tsc": "tsc", + "prebuild": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/attach\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/webLinks\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"", "build": "gulp build", "prepublish": "npm run build", "coveralls": "nyc report --reporter=text-lcov | coveralls", "webpack": "gulp webpack", - "watch": "gulp watch" + "watch": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc -w\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\" \"gulp watch-css\"" } } diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts index a7e205e3..ea300767 100644 --- a/src/AccessibilityManager.ts +++ b/src/AccessibilityManager.ts @@ -179,7 +179,7 @@ export class AccessibilityManager implements IDisposable { this._refreshRowsDimensions(); } - public _createAccessibilityTreeNode(): HTMLElement { + private _createAccessibilityTreeNode(): HTMLElement { const element = document.createElement('div'); element.setAttribute('role', 'listitem'); element.tabIndex = -1; diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts index 44687f0e..82aa0224 100644 --- a/src/Buffer.test.ts +++ b/src/Buffer.test.ts @@ -48,6 +48,63 @@ describe('Buffer', () => { }); }); + describe('getWrappedRangeForLine', () => { + describe('non-wrapped', () => { + it('should return a single row for the first row', () => { + buffer.fillViewportRows(); + assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 0 }); + }); + it('should return a single row for a middle row', () => { + buffer.fillViewportRows(); + assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 12 }); + }); + it('should return a single row for the last row', () => { + buffer.fillViewportRows(); + assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 23, last: 23 }); + }); + }); + describe('wrapped', () => { + it('should return a range for the first row', () => { + buffer.fillViewportRows(); + ( buffer.lines.get(1)).isWrapped = true; + assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 1 }); + }); + it('should return a range for a middle row wrapping upwards', () => { + buffer.fillViewportRows(); + ( buffer.lines.get(12)).isWrapped = true; + assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 11, last: 12 }); + }); + it('should return a range for a middle row wrapping downwards', () => { + buffer.fillViewportRows(); + ( buffer.lines.get(13)).isWrapped = true; + assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 13 }); + }); + it('should return a range for a middle row wrapping both ways', () => { + buffer.fillViewportRows(); + ( buffer.lines.get(11)).isWrapped = true; + ( buffer.lines.get(12)).isWrapped = true; + ( buffer.lines.get(13)).isWrapped = true; + ( buffer.lines.get(14)).isWrapped = true; + assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 10, last: 14 }); + }); + it('should return a range for the last row', () => { + buffer.fillViewportRows(); + ( buffer.lines.get(23)).isWrapped = true; + assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 22, last: 23 }); + }); + it('should return a range for a row that wraps upward to first row', () => { + buffer.fillViewportRows(); + ( buffer.lines.get(1)).isWrapped = true; + assert.deepEqual(buffer.getWrappedRangeForLine(1), { first: 0, last: 1 }); + }); + it('should return a range for a row that wraps downward to last row', () => { + buffer.fillViewportRows(); + ( buffer.lines.get(buffer.lines.length - 1)).isWrapped = true; + assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 2), { first: 22, last: 23 }); + }); + }); + }); + describe('resize', () => { describe('column size is reduced', () => { it('should not trim the data in the buffer', () => { diff --git a/src/Buffer.ts b/src/Buffer.ts index 1ea303b2..5ac2a4f2 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -8,6 +8,7 @@ import { LineData, CharData, ITerminal, IBuffer } from './Types'; import { EventEmitter } from './EventEmitter'; import { IDisposable, IMarker } from 'xterm'; +export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0); export const CHAR_DATA_ATTR_INDEX = 0; export const CHAR_DATA_CHAR_INDEX = 1; export const CHAR_DATA_WIDTH_INDEX = 2; @@ -116,7 +117,7 @@ export class Buffer implements IBuffer { if (this.lines.length > 0) { // Deal with columns increasing (we don't do anything when columns reduce) if (this._terminal.cols < newCols) { - const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr? + const ch: CharData = [DEFAULT_ATTR, ' ', 1, 32]; // does xterm use the default attr? for (let i = 0; i < this.lines.length; i++) { while (this.lines.get(i).length < newCols) { this.lines.get(i).push(ch); @@ -259,6 +260,20 @@ export class Buffer implements IBuffer { return lineString.substring(startIndex, endIndex); } + public getWrappedRangeForLine(y: number): { first: number, last: number } { + let first = y; + let last = y; + // Scan upwards for wrapped lines + while (first > 0 && (this.lines.get(first)).isWrapped) { + first--; + } + // Scan downwards for wrapped lines + while (last + 1 < this.lines.length && (this.lines.get(last + 1)).isWrapped) { + last++; + } + return { first, last }; + } + /** * Setup the tab stops. * @param i The index to start setting up tab stops from. @@ -323,9 +338,9 @@ export class Buffer implements IBuffer { } export class Marker extends EventEmitter implements IMarker { - private static NEXT_ID = 1; + private static _nextId = 1; - private _id: number = Marker.NEXT_ID++; + private _id: number = Marker._nextId++; public isDisposed: boolean = false; public disposables: IDisposable[] = []; diff --git a/src/CharWidth.ts b/src/CharWidth.ts index 90673b2b..6e4aa37f 100644 --- a/src/CharWidth.ts +++ b/src/CharWidth.ts @@ -121,7 +121,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu } const control = opts.control | 0; let table: number[] | Uint32Array = null; - function init_table(): number[] | Uint32Array { + function initTable(): 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 @@ -161,7 +161,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu if (num < 127) { return 1; } - let t = table || init_table(); + const t = table || initTable(); if (num < 65536) { return t[num >> 4] >> ((num & 15) << 1) & 3; } diff --git a/src/CompositionHelper.test.ts b/src/CompositionHelper.test.ts index 02231723..053a0c29 100644 --- a/src/CompositionHelper.test.ts +++ b/src/CompositionHelper.test.ts @@ -5,13 +5,14 @@ import { assert } from 'chai'; import { CompositionHelper } from './CompositionHelper'; +import { ITerminal } from './Types'; describe('CompositionHelper', () => { - let terminal; - let compositionHelper; - let compositionView; - let textarea; - let handledText; + let terminal: ITerminal; + let compositionHelper: CompositionHelper; + let compositionView: HTMLElement; + let textarea: HTMLTextAreaElement; + let handledText: string; beforeEach(() => { compositionView = { @@ -27,14 +28,14 @@ describe('CompositionHelper', () => { top: 0 }, textContent: '' - }; + } as any; textarea = { value: '', style: { left: 0, top: 0 } - }; + } as any; terminal = { element: { querySelector: () => { @@ -54,7 +55,7 @@ describe('CompositionHelper', () => { options: { lineHeight: 1 } - }; + } as any; handledText = ''; compositionHelper = new CompositionHelper(textarea, compositionView, terminal); }); @@ -63,7 +64,7 @@ describe('CompositionHelper', () => { it('Should insert simple characters', (done) => { // First character 'ㅇ' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'ㅇ' }); + compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇ'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -71,7 +72,7 @@ describe('CompositionHelper', () => { assert.equal(handledText, 'ㅇ'); // Second character 'ㅇ' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'ㅇ' }); + compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇㅇ'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -87,13 +88,13 @@ describe('CompositionHelper', () => { it('Should insert complex characters', (done) => { // First character '앙' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'ㅇ' }); + compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇ'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: '아' }); + compositionHelper.compositionupdate({ data: '아' }); textarea.value = '아'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: '앙' }); + compositionHelper.compositionupdate({ data: '앙' }); textarea.value = '앙'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -101,13 +102,13 @@ describe('CompositionHelper', () => { assert.equal(handledText, '앙'); // Second character '앙' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'ㅇ' }); + compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = '앙ㅇ'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: '아' }); + compositionHelper.compositionupdate({ data: '아' }); textarea.value = '앙아'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: '앙' }); + compositionHelper.compositionupdate({ data: '앙' }); textarea.value = '앙앙'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -127,19 +128,19 @@ describe('CompositionHelper', () => { it('Should insert complex characters that change with following character', (done) => { // First character '아' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'ㅇ' }); + compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇ'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: '아' }); + compositionHelper.compositionupdate({ data: '아' }); textarea.value = '아'; setTimeout(() => { // wait for any textarea updates // Start second character '아' in first character - compositionHelper.compositionupdate({ data: '앙' }); + compositionHelper.compositionupdate({ data: '앙' }); textarea.value = '앙'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: '아' }); + compositionHelper.compositionupdate({ data: '아' }); textarea.value = '아아'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -156,14 +157,14 @@ describe('CompositionHelper', () => { it('Should insert multi-characters compositions', (done) => { // First character 'だ' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'd' }); + compositionHelper.compositionupdate({ data: 'd' }); textarea.value = 'd'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: 'だ' }); + compositionHelper.compositionupdate({ data: 'だ' }); textarea.value = 'だ'; setTimeout(() => { // wait for any textarea updates // Second character 'あ' - compositionHelper.compositionupdate({ data: 'だあ' }); + compositionHelper.compositionupdate({ data: 'だあ' }); textarea.value = 'だあ'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -179,18 +180,18 @@ describe('CompositionHelper', () => { it('Should insert multi-character compositions that are converted to other characters with the same length', (done) => { // First character 'だ' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'd' }); + compositionHelper.compositionupdate({ data: 'd' }); textarea.value = 'd'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: 'だ' }); + compositionHelper.compositionupdate({ data: 'だ' }); textarea.value = 'だ'; setTimeout(() => { // wait for any textarea updates // Second character 'ー' - compositionHelper.compositionupdate({ data: 'だー' }); + compositionHelper.compositionupdate({ data: 'だー' }); textarea.value = 'だー'; setTimeout(() => { // wait for any textarea updates // Convert to katakana 'ダー' - compositionHelper.compositionupdate({ data: 'ダー' }); + compositionHelper.compositionupdate({ data: 'ダー' }); textarea.value = 'ダー'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -207,18 +208,18 @@ describe('CompositionHelper', () => { it('Should insert multi-character compositions that are converted to other characters with different lengths', (done) => { // First character 'い' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'い' }); + compositionHelper.compositionupdate({ data: 'い' }); textarea.value = 'い'; setTimeout(() => { // wait for any textarea updates // Second character 'ま' - compositionHelper.compositionupdate({ data: 'いm' }); + compositionHelper.compositionupdate({ data: 'いm' }); textarea.value = 'いm'; setTimeout(() => { // wait for any textarea updates - compositionHelper.compositionupdate({ data: 'いま' }); + compositionHelper.compositionupdate({ data: 'いま' }); textarea.value = 'いま'; setTimeout(() => { // wait for any textarea updates // Convert to kanji '今' - compositionHelper.compositionupdate({ data: '今' }); + compositionHelper.compositionupdate({ data: '今' }); textarea.value = '今'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); @@ -235,7 +236,7 @@ describe('CompositionHelper', () => { it('Should insert non-composition characters input immediately after composition characters', (done) => { // First character 'ㅇ' compositionHelper.compositionstart(); - compositionHelper.compositionupdate({ data: 'ㅇ' }); + compositionHelper.compositionupdate({ data: 'ㅇ' }); textarea.value = 'ㅇ'; setTimeout(() => { // wait for any textarea updates compositionHelper.compositionend(); diff --git a/src/EscapeSequenceParser.test.ts b/src/EscapeSequenceParser.test.ts new file mode 100644 index 00000000..5db6d7ff --- /dev/null +++ b/src/EscapeSequenceParser.test.ts @@ -0,0 +1,1243 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ParserState, IDcsHandler, IParsingState } from './Types'; +import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from './EscapeSequenceParser'; +import * as chai from 'chai'; + +function r(a: number, b: number): string[] { + let c = b - a; + const arr = new Array(c); + while (c--) { + arr[c] = String.fromCharCode(--b); + } + return arr; +} + +// derived parser with access to internal states +class TestEscapeSequenceParser extends EscapeSequenceParser { + public get osc(): string { + return this._osc; + } + public set osc(value: string) { + this._osc = value; + } + public get params(): number[] { + return this._params; + } + public set params(value: number[]) { + this._params = value; + } + public get collect(): string { + return this._collect; + } + public set collect(value: string) { + this._collect = value; + } + public mockActiveDcsHandler(): void { + this._activeDcsHandler = this._dcsHandlerFb; + } +} + +// test object to collect parser actions and compare them with expected values +const testTerminal: any = { + calls: [], + clear: function (): void { + this.calls = []; + }, + compare: function (value: any): void { + chai.expect(this.calls.slice()).eql(value); // weird bug w'o slicing here + }, + print: function (data: string, start: number, end: number): void { + this.calls.push(['print', data.substring(start, end)]); + }, + actionOSC: function (s: string): void { + this.calls.push(['osc', s]); + }, + actionExecute: function (flag: string): void { + this.calls.push(['exe', flag]); + }, + actionCSI: function (collect: string, params: number[], flag: string): void { + this.calls.push(['csi', collect, params, flag]); + }, + actionESC: function (collect: string, flag: string): void { + this.calls.push(['esc', collect, flag]); + }, + actionDCSHook: function (collect: string, params: number[], flag: string): void { + this.calls.push(['dcs hook', collect, params, flag]); + }, + actionDCSPrint: function (data: string, start: number, end: number): void { + this.calls.push(['dcs put', data.substring(start, end)]); + }, + actionDCSUnhook: function (): void { + this.calls.push(['dcs unhook']); + } +}; + +// dcs handler to map dcs actions into the test object `testTerminal` +class DcsTest implements IDcsHandler { + hook(collect: string, params: number[], flag: number): void { + testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag)); + } + put(data: string, start: number, end: number): void { + testTerminal.actionDCSPrint(data, start, end); + } + unhook(): void { + testTerminal.actionDCSUnhook(); + } +} + +const states: number[] = [ + ParserState.GROUND, + ParserState.ESCAPE, + ParserState.ESCAPE_INTERMEDIATE, + ParserState.CSI_ENTRY, + ParserState.CSI_PARAM, + ParserState.CSI_INTERMEDIATE, + ParserState.CSI_IGNORE, + ParserState.SOS_PM_APC_STRING, + ParserState.OSC_STRING, + ParserState.DCS_ENTRY, + ParserState.DCS_PARAM, + ParserState.DCS_IGNORE, + ParserState.DCS_INTERMEDIATE, + ParserState.DCS_PASSTHROUGH +]; +let state: any; + +// parser with Uint8Array based transition table +const parserUint = new TestEscapeSequenceParser(VT500_TRANSITION_TABLE); +parserUint.setPrintHandler(testTerminal.print.bind(testTerminal)); +parserUint.setCsiHandlerFallback((collect: string, params: number[], flag: number) => { + testTerminal.actionCSI(collect, params, String.fromCharCode(flag)); +}); +parserUint.setEscHandlerFallback((collect: string, flag: number) => { + testTerminal.actionESC(collect, String.fromCharCode(flag)); +}); +parserUint.setExecuteHandlerFallback((code: number) => { + testTerminal.actionExecute(String.fromCharCode(code)); +}); +parserUint.setOscHandlerFallback((identifier: number, data: string) => { + if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently + else testTerminal.actionOSC('' + identifier + ';' + data); +}); +parserUint.setDcsHandlerFallback(new DcsTest()); + +// array based transition table +const VT500_TRANSITION_TABLE_ARRAY = new TransitionTable(VT500_TRANSITION_TABLE.table.length); +VT500_TRANSITION_TABLE_ARRAY.table = new Array(VT500_TRANSITION_TABLE.table.length); +for (let i = 0; i < VT500_TRANSITION_TABLE.table.length; ++i) { + VT500_TRANSITION_TABLE_ARRAY.table[i] = VT500_TRANSITION_TABLE.table[i]; +} + +// parser with array based transition table +const parserArray = new TestEscapeSequenceParser(VT500_TRANSITION_TABLE_ARRAY); +parserArray.setPrintHandler(testTerminal.print.bind(testTerminal)); +parserArray.setCsiHandlerFallback((collect: string, params: number[], flag: number) => { + testTerminal.actionCSI(collect, params, String.fromCharCode(flag)); +}); +parserArray.setEscHandlerFallback((collect: string, flag: number) => { + testTerminal.actionESC(collect, String.fromCharCode(flag)); +}); +parserArray.setExecuteHandlerFallback((code: number) => { + testTerminal.actionExecute(String.fromCharCode(code)); +}); +parserArray.setOscHandlerFallback((identifier: number, data: string) => { + if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently + else testTerminal.actionOSC('' + identifier + ';' + data); +}); +parserArray.setDcsHandlerFallback(new DcsTest()); + +interface IRun { + tableType: string; + parser: TestEscapeSequenceParser; +} + + +describe('EscapeSequenceParser', function (): void { + let parser: TestEscapeSequenceParser | null = null; + const runs: IRun[] = [ + { tableType: 'Uint8Array', parser: parserUint }, + { tableType: 'Array', parser: parserArray } + ]; + runs.forEach(function (run: IRun): void { + describe('Parser init and methods / ' + run.tableType, function (): void { + before(function(): void { + parser = run.parser; + }); + it('constructor', function (): void { + let p: EscapeSequenceParser = new EscapeSequenceParser(); + chai.expect(p.TRANSITIONS).equal(VT500_TRANSITION_TABLE); + p = new EscapeSequenceParser(VT500_TRANSITION_TABLE); + chai.expect(p.TRANSITIONS).equal(VT500_TRANSITION_TABLE); + const tansitions: TransitionTable = new TransitionTable(10); + p = new EscapeSequenceParser(tansitions); + chai.expect(p.TRANSITIONS).equal(tansitions); + }); + it('inital states', function (): void { + chai.expect(parser.initialState).equal(ParserState.GROUND); + chai.expect(parser.currentState).equal(ParserState.GROUND); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + }); + it('reset states', function (): void { + parser.currentState = 124; + parser.osc = '#'; + parser.params = [123]; + parser.collect = '#'; + + parser.reset(); + chai.expect(parser.currentState).equal(ParserState.GROUND); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + }); + }); + }); + runs.forEach(function (run: IRun): void { + describe('state transitions and actions / ' + run.tableType, function (): void { + before(function(): void { + parser = run.parser; + }); + it('state GROUND execute action', function (): void { + parser.reset(); + testTerminal.clear(); + const exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.GROUND; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state GROUND print action', function (): void { + parser.reset(); + testTerminal.clear(); + const printables = r(0x20, 0x7f); // NOTE: DEL excluded + for (let i = 0; i < printables.length; ++i) { + parser.currentState = ParserState.GROUND; + parser.parse(printables[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['print', printables[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE --> GROUND with actions', function (): void { + const exes = [ + '\x18', '\x1a', + '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87', '\x88', + '\x89', '\x8a', '\x8b', '\x8c', '\x8d', '\x8e', '\x8f', + '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x99', '\x9a' + ]; + const exceptions: { [key: number]: { [key: string]: any[] } } = { + 8: { '\x18': [], '\x1a': [] } // simply abort osc state + }; + parser.reset(); + testTerminal.clear(); + for (state in states) { + for (let i = 0; i < exes.length; ++i) { + parser.currentState = state; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare((state in exceptions ? exceptions[state][exes[i]] : 0) || [['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + parser.parse('\x9c'); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE --> ESCAPE with clear', function (): void { + parser.reset(); + for (state in states) { + parser.currentState = state; + parser.osc = '#'; + parser.params = [23]; + parser.collect = '#'; + parser.parse('\x1b'); + chai.expect(parser.currentState).equal(ParserState.ESCAPE); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + parser.reset(); + } + }); + it('state ESCAPE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + const exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.ESCAPE; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.ESCAPE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state ESCAPE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.ESCAPE; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(ParserState.ESCAPE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans ESCAPE --> GROUND with ecs_dispatch action', function (): void { + parser.reset(); + testTerminal.clear(); + const dispatches = r(0x30, 0x50); + dispatches.concat(r(0x51, 0x58)); + dispatches.concat(['\x59', '\x5a', '\x5c']); + dispatches.concat(r(0x60, 0x7f)); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.ESCAPE; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['esc', '', dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ESCAPE --> ESCAPE_INTERMEDIATE with collect action', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.ESCAPE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state ESCAPE_INTERMEDIATE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + const exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.ESCAPE_INTERMEDIATE; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state ESCAPE_INTERMEDIATE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.ESCAPE_INTERMEDIATE; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('state ESCAPE_INTERMEDIATE collect action', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.ESCAPE_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.ESCAPE_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('trans ESCAPE_INTERMEDIATE --> GROUND with esc_dispatch action', function (): void { + parser.reset(); + testTerminal.clear(); + const collect = r(0x30, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.ESCAPE_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['esc', '', collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE/ESCAPE --> CSI_ENTRY with clear', function (): void { + parser.reset(); + // C0 + parser.currentState = ParserState.ESCAPE; + parser.osc = '#'; + parser.params = [123]; + parser.collect = '#'; + parser.parse('['); + chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + parser.reset(); + // C1 + for (state in states) { + parser.currentState = state; + parser.osc = '#'; + parser.params = [123]; + parser.collect = '#'; + parser.parse('\x9b'); + chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); + chai.expect(parser.osc).equal(''); + chai.expect(parser.params).eql([0]); + chai.expect(parser.collect).equal(''); + parser.reset(); + } + }); + it('state CSI_ENTRY execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + const exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_ENTRY ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.CSI_ENTRY; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(ParserState.CSI_ENTRY); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_ENTRY --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + const dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['csi', '', [0], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_PARAM with param/collect actions', function (): void { + parser.reset(); + const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + const collect = ['\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = ParserState.CSI_ENTRY; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_PARAM execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + const exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_PARAM param action', function (): void { + parser.reset(); + const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = ParserState.CSI_PARAM; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + }); + it('state CSI_PARAM ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.CSI_PARAM; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(ParserState.CSI_PARAM); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_PARAM --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + const dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_INTERMEDIATE with collect action', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.CSI_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('trans CSI_PARAM --> CSI_INTERMEDIATE with collect action', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_INTERMEDIATE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + const exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.CSI_INTERMEDIATE; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_INTERMEDIATE collect', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.CSI_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state CSI_INTERMEDIATE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.CSI_INTERMEDIATE; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(ParserState.CSI_INTERMEDIATE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('trans CSI_INTERMEDIATE --> GROUND with csi_dispatch action', function (): void { + parser.reset(); + const dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.CSI_INTERMEDIATE; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([['csi', '', [0, 1], dispatches[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_ENTRY --> CSI_IGNORE', function (): void { + parser.reset(); + parser.currentState = ParserState.CSI_ENTRY; + parser.parse('\x3a'); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + parser.reset(); + }); + it('trans CSI_PARAM --> CSI_IGNORE', function (): void { + parser.reset(); + const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.CSI_PARAM; + parser.parse('\x3b' + chars[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + } + }); + it('trans CSI_INTERMEDIATE --> CSI_IGNORE', function (): void { + parser.reset(); + const chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.CSI_INTERMEDIATE; + parser.parse(chars[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + chai.expect(parser.params).eql([0]); + parser.reset(); + } + }); + it('state CSI_IGNORE execute rules', function (): void { + parser.reset(); + testTerminal.clear(); + const exes = r(0x00, 0x18); + exes.concat(['\x19']); + exes.concat(r(0x1c, 0x20)); + for (let i = 0; i < exes.length; ++i) { + parser.currentState = ParserState.CSI_IGNORE; + parser.parse(exes[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + testTerminal.compare([['exe', exes[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state CSI_IGNORE ignore', function (): void { + parser.reset(); + testTerminal.clear(); + const ignored = r(0x20, 0x40); + ignored.concat(['\x7f']); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.CSI_IGNORE; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans CSI_IGNORE --> GROUND', function (): void { + parser.reset(); + const dispatches = r(0x40, 0x7f); + for (let i = 0; i < dispatches.length; ++i) { + parser.currentState = ParserState.CSI_IGNORE; + parser.params = [0, 1]; + parser.parse(dispatches[i]); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans ANYWHERE/ESCAPE --> SOS_PM_APC_STRING', function (): void { + parser.reset(); + // C0 + let initializers = ['\x58', '\x5e', '\x5f']; + for (let i = 0; i < initializers.length; ++i) { + parser.parse('\x1b' + initializers[i]); + chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); + parser.reset(); + } + // C1 + for (state in states) { + parser.currentState = state; + initializers = ['\x98', '\x9e', '\x9f']; + for (let i = 0; i < initializers.length; ++i) { + parser.parse(initializers[i]); + chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); + parser.reset(); + } + } + }); + it('state SOS_PM_APC_STRING ignore rules', function (): void { + parser.reset(); + const ignored = r(0x00, 0x18); + ignored.concat(['\x19']); + ignored.concat(r(0x1c, 0x20)); + ignored.concat(r(0x20, 0x80)); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.SOS_PM_APC_STRING; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(ParserState.SOS_PM_APC_STRING); + parser.reset(); + } + }); + it('trans ANYWHERE/ESCAPE --> OSC_STRING', function (): void { + parser.reset(); + // C0 + parser.parse('\x1b]'); + chai.expect(parser.currentState).equal(ParserState.OSC_STRING); + parser.reset(); + // C1 + for (state in states) { + parser.currentState = state; + parser.parse('\x9d'); + chai.expect(parser.currentState).equal(ParserState.OSC_STRING); + parser.reset(); + } + }); + it('state OSC_STRING ignore rules', function (): void { + parser.reset(); + const ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', /*'\x07',*/ '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.OSC_STRING; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(ParserState.OSC_STRING); + chai.expect(parser.osc).equal(''); + parser.reset(); + } + }); + it('state OSC_STRING put action', function (): void { + parser.reset(); + const puts = r(0x20, 0x80); + for (let i = 0; i < puts.length; ++i) { + parser.currentState = ParserState.OSC_STRING; + parser.parse(puts[i]); + chai.expect(parser.currentState).equal(ParserState.OSC_STRING); + chai.expect(parser.osc).equal(puts[i]); + parser.reset(); + } + }); + it('state DCS_ENTRY', function (): void { + parser.reset(); + // C0 + parser.parse('\x1bP'); + chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); + parser.reset(); + // C1 + for (state in states) { + parser.currentState = state; + parser.parse('\x90'); + chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); + parser.reset(); + } + }); + it('state DCS_ENTRY ignore rules', function (): void { + parser.reset(); + const ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_ENTRY); + parser.reset(); + } + }); + it('state DCS_ENTRY --> DCS_PARAM with param/collect actions', function (): void { + parser.reset(); + const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + const collect = ['\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = ParserState.DCS_ENTRY; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state DCS_PARAM ignore rules', function (): void { + parser.reset(); + const ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + parser.reset(); + } + }); + it('state DCS_PARAM param action', function (): void { + parser.reset(); + const params = ['\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', '\x38', '\x39']; + for (let i = 0; i < params.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parser.parse(params[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.params).eql([params[i].charCodeAt(0) - 48]); + parser.reset(); + } + parser.currentState = ParserState.DCS_PARAM; + parser.parse('\x3b'); + chai.expect(parser.currentState).equal(ParserState.DCS_PARAM); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + }); + it('trans DCS_ENTRY --> DCS_IGNORE', function (): void { + parser.reset(); + parser.currentState = ParserState.DCS_ENTRY; + parser.parse('\x3a'); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + parser.reset(); + }); + it('trans DCS_PARAM --> DCS_IGNORE', function (): void { + parser.reset(); + const chars = ['\x3a', '\x3c', '\x3d', '\x3e', '\x3f']; + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parser.parse('\x3b' + chars[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + chai.expect(parser.params).eql([0, 0]); + parser.reset(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { + parser.reset(); + const chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parser.parse(chars[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + parser.reset(); + } + }); + it('state DCS_IGNORE ignore rules', function (): void { + parser.reset(); + const ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + ignored.concat(r(0x20, 0x80)); + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.DCS_IGNORE; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + parser.reset(); + } + }); + it('trans DCS_ENTRY --> DCS_INTERMEDIATE with collect action', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('trans DCS_PARAM --> DCS_INTERMEDIATE with collect action', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('state DCS_INTERMEDIATE ignore rules', function (): void { + parser.reset(); + const ignored = [ + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', + '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', '\x10', '\x11', + '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x19', '\x1c', '\x1d', '\x1e', '\x1f', '\x7f']; + for (let i = 0; i < ignored.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parser.parse(ignored[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); + parser.reset(); + } + }); + it('state DCS_INTERMEDIATE collect action', function (): void { + parser.reset(); + const collect = r(0x20, 0x30); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_INTERMEDIATE); + chai.expect(parser.collect).equal(collect[i]); + parser.reset(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_IGNORE', function (): void { + parser.reset(); + const chars = r(0x30, 0x40); + for (let i = 0; i < chars.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parser.parse('\x20' + chars[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + chai.expect(parser.collect).equal('\x20'); + parser.reset(); + } + }); + it('trans DCS_ENTRY --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + const collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_ENTRY; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans DCS_PARAM --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + const collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_PARAM; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('trans DCS_INTERMEDIATE --> DCS_PASSTHROUGH with hook', function (): void { + parser.reset(); + testTerminal.clear(); + const collect = r(0x40, 0x7f); + for (let i = 0; i < collect.length; ++i) { + parser.currentState = ParserState.DCS_INTERMEDIATE; + parser.parse(collect[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '', [0], collect[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state DCS_PASSTHROUGH put action', function (): void { + parser.reset(); + testTerminal.clear(); + const puts = r(0x00, 0x18); + puts.concat(['\x19']); + puts.concat(r(0x1c, 0x20)); + puts.concat(r(0x20, 0x7f)); + for (let i = 0; i < puts.length; ++i) { + parser.currentState = ParserState.DCS_PASSTHROUGH; + parser.mockActiveDcsHandler(); + parser.parse(puts[i]); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs put', puts[i]]]); + parser.reset(); + testTerminal.clear(); + } + }); + it('state DCS_PASSTHROUGH ignore', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.DCS_PASSTHROUGH; + parser.parse('\x7f'); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + }); + }); + + runs.forEach(function (run: IRun): void { + let test: Function | null = null; + describe('escape sequence examples / ' + run.tableType, function (): void { + before(function(): void { + parser = run.parser; + test = function(s: string, value: any, noReset: any): void { + if (!noReset) { + parser.reset(); + testTerminal.clear(); + } + parser.parse(s); + testTerminal.compare(value); + }; + }); + it('CSI with print and execute', function (): void { + test('\x1b[<31;5mHello World! öäü€\nabc', + [ + ['csi', '<', [31, 5], 'm'], + ['print', 'Hello World! öäü€'], + ['exe', '\n'], + ['print', 'abc'] + ], null); + }); + it('OSC', function (): void { + test('\x1b]0;abc123€öäü\x07', [ + ['osc', '0;abc123€öäü'] + ], null); + }); + it('single DCS', function (): void { + test('\x1bP1;2;3+$abc;de\x9c', [ + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'], + ['dcs unhook'] + ], null); + }); + it('multi DCS', function (): void { + test('\x1bP1;2;3+$abc;de', [ + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'] + ], null); + testTerminal.clear(); + test('abc\x9c', [ + ['dcs put', 'abc'], + ['dcs unhook'] + ], true); + }); + it('print + DCS(C1)', function (): void { + test('abc\x901;2;3+$abc;de\x9c', [ + ['print', 'abc'], + ['dcs hook', '+$', [1, 2, 3], 'a'], + ['dcs put', 'bc;de'], + ['dcs unhook'] + ], null); + }); + it('print + PM(C1) + print', function (): void { + test('abc\x98123tzf\x9cdefg', [ + ['print', 'abc'], + ['print', 'defg'] + ], null); + }); + it('print + OSC(C1) + print', function (): void { + test('abc\x9d123tzf\x9cdefg', [ + ['print', 'abc'], + ['osc', '123tzf'], + ['print', 'defg'] + ], null); + }); + it('error recovery', function (): void { + test('\x1b[1€abcdefg\x9b<;c', [ + ['print', 'abcdefg'], + ['csi', '<', [0, 0], 'c'] + ], null); + }); + }); + }); + + describe('coverage tests', function (): void { + it('CSI_IGNORE error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.CSI_IGNORE; + parser.parse('€öäü'); + chai.expect(parser.currentState).equal(ParserState.CSI_IGNORE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('DCS_IGNORE error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.DCS_IGNORE; + parser.parse('€öäü'); + chai.expect(parser.currentState).equal(ParserState.DCS_IGNORE); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + it('DCS_PASSTHROUGH error', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.DCS_PASSTHROUGH; + parser.parse('\x901;2;3+$a€öäü'); + chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH); + testTerminal.compare([['dcs hook', '+$', [1, 2, 3], 'a'], ['dcs put', '€öäü']]); + parser.reset(); + testTerminal.clear(); + }); + it('error else of if (code > 159)', function (): void { + parser.reset(); + testTerminal.clear(); + parser.currentState = ParserState.GROUND; + parser.parse('\x1e'); + chai.expect(parser.currentState).equal(ParserState.GROUND); + testTerminal.compare([]); + parser.reset(); + testTerminal.clear(); + }); + }); + + describe('set/clear handler', function (): void { + const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1b]1;foo=bar\x1b\\'; + let parser2: TestEscapeSequenceParser = null; + let print = ''; + const esc: string[] = []; + const csi: [string, number[], string][] = []; + const exe: string[] = []; + const osc: [number, string][] = []; + const dcs: ([string] | [string, string] | [string, string, number[], number])[] = []; + function clearAccu(): void { + print = ''; + esc.length = 0; + csi.length = 0; + exe.length = 0; + osc.length = 0; + dcs.length = 0; + } + beforeEach(function (): void { + parser2 = new TestEscapeSequenceParser(); + clearAccu(); + }); + it('print handler', function (): void { + parser2.setPrintHandler(function (data: string, start: number, end: number): void { + print += data.substring(start, end); + }); + parser2.parse(INPUT); + chai.expect(print).equal('hello world!$>'); + parser2.clearPrintHandler(); + parser2.clearPrintHandler(); // should not throw + clearAccu(); + parser2.parse(INPUT); + chai.expect(print).equal(''); + }); + it('ESC handler', function (): void { + parser2.setEscHandler('%G', function (): void { + esc.push('%G'); + }); + parser2.setEscHandler('E', function (): void { + esc.push('E'); + }); + parser2.parse(INPUT); + chai.expect(esc).eql(['%G', 'E']); + parser2.clearEscHandler('%G'); + parser2.clearEscHandler('%G'); // should not throw + clearAccu(); + parser2.parse(INPUT); + chai.expect(esc).eql(['E']); + parser2.clearEscHandler('E'); + clearAccu(); + parser2.parse(INPUT); + chai.expect(esc).eql([]); + }); + it('CSI handler', function (): void { + parser2.setCsiHandler('m', function (params: number[], collect: string): void { + csi.push(['m', params, collect]); + }); + parser2.parse(INPUT); + chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]); + parser2.clearCsiHandler('m'); + parser2.clearCsiHandler('m'); // should not throw + clearAccu(); + parser2.parse(INPUT); + chai.expect(csi).eql([]); + }); + it('EXECUTE handler', function (): void { + parser2.setExecuteHandler('\n', function (): void { + exe.push('\n'); + }); + parser2.setExecuteHandler('\r', function (): void { + exe.push('\r'); + }); + parser2.parse(INPUT); + chai.expect(exe).eql(['\r', '\n']); + parser2.clearExecuteHandler('\r'); + parser2.clearExecuteHandler('\r'); // should not throw + clearAccu(); + parser2.parse(INPUT); + chai.expect(exe).eql(['\n']); + }); + it('OSC handler', function (): void { + parser2.setOscHandler(1, function (data: string): void { + osc.push([1, data]); + }); + parser2.parse(INPUT); + chai.expect(osc).eql([[1, 'foo=bar']]); + parser2.clearOscHandler(1); + parser2.clearOscHandler(1); // should not throw + clearAccu(); + parser2.parse(INPUT); + chai.expect(osc).eql([]); + }); + it('DCS handler', function (): void { + parser2.setDcsHandler('+p', { + hook: function (collect: string, params: number[], flag: number): void { + dcs.push(['hook', collect, params, flag]); + }, + put: function (data: string, start: number, end: number): void { + dcs.push(['put', data.substring(start, end)]); + }, + unhook: function (): void { + dcs.push(['unhook']); + } + }); + parser2.parse('\x1bP1;2;3+pabc'); + parser2.parse(';de\x9c'); + chai.expect(dcs).eql([ + ['hook', '+', [1, 2, 3], 'p'.charCodeAt(0)], + ['put', 'abc'], ['put', ';de'], + ['unhook'] + ]); + parser2.clearDcsHandler('+p'); + parser2.clearDcsHandler('+p'); // should not throw + clearAccu(); + parser2.parse('\x1bP1;2;3+pabc'); + parser2.parse(';de\x9c'); + chai.expect(dcs).eql([]); + }); + it('ERROR handler', function (): void { + let errorState: IParsingState = null; + parser2.setErrorHandler(function (state: IParsingState): IParsingState { + errorState = state; + return state; + }); + parser2.parse('\x1b[1;2;€;3m'); // faulty escape sequence + chai.expect(errorState).eql({ + position: 6, + code: '€'.charCodeAt(0), + currentState: ParserState.CSI_PARAM, + print: -1, + dcs: -1, + osc: '', + collect: '', + params: [1, 2, 0], // extra zero here + abort: false + }); + parser2.clearErrorHandler(); + parser2.clearErrorHandler(); // should not throw + errorState = null; + parser2.parse('\x1b[1;2;a;3m'); + chai.expect(errorState).eql(null); + }); + }); + // TODO: error conditions and error recovery (not implemented yet in parser) +}); diff --git a/src/EscapeSequenceParser.ts b/src/EscapeSequenceParser.ts new file mode 100644 index 00000000..2849dbc1 --- /dev/null +++ b/src/EscapeSequenceParser.ts @@ -0,0 +1,544 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types'; + +/** + * Returns an array filled with numbers between the low and high parameters (right exclusive). + * @param low The low number. + * @param high The high number. + */ +function r(low: number, high: number): number[] { + let c = high - low; + const arr = new Array(c); + while (c--) { + arr[c] = --high; + } + return arr; +} + +/** + * Transition table for EscapeSequenceParser. + * NOTE: data in the underlying table is packed like this: + * currentState << 8 | characterCode --> action << 4 | nextState + */ +export class TransitionTable { + public table: Uint8Array | number[]; + + constructor(length: number) { + this.table = (typeof Uint8Array === 'undefined') + ? new Array(length) + : new Uint8Array(length); + } + + /** + * Add a transition to the transition table. + * @param code input character code + * @param state current parser state + * @param action parser action to be done + * @param next next parser state + */ + add(code: number, state: number, action: number | null, next: number | null): void { + this.table[state << 8 | code] = ((action | 0) << 4) | ((next === undefined) ? state : next); + } + + /** + * Add transitions for multiple input character codes. + * @param codes input character code array + * @param state current parser state + * @param action parser action to be done + * @param next next parser state + */ + addMany(codes: number[], state: number, action: number | null, next: number | null): void { + for (let i = 0; i < codes.length; i++) { + this.add(codes[i], state, action, next); + } + } +} + + +/** + * Default definitions for the VT500_TRANSITION_TABLE. + */ +const PRINTABLES = r(0x20, 0x7f); +const EXECUTABLES = r(0x00, 0x18); +EXECUTABLES.push(0x19); +EXECUTABLES.concat(r(0x1c, 0x20)); +const DEFAULT_TRANSITION = ParserAction.ERROR << 4 | ParserState.GROUND; + +/** + * VT500 compatible transition table. + * Taken from https://vt100.net/emu/dec_ansi_parser. + */ +export const VT500_TRANSITION_TABLE = (function (): TransitionTable { + const table: TransitionTable = new TransitionTable(4095); + + const states: number[] = r(ParserState.GROUND, ParserState.DCS_PASSTHROUGH + 1); + let state: any; + + // table with default transition [any] --> DEFAULT_TRANSITION + for (state in states) { + // NOTE: table lookup is capped at 0xa0 in parse to keep the table small + for (let code = 0; code < 160; ++code) { + table.add(code, state, ParserAction.ERROR, ParserState.GROUND); + } + } + // printables + table.addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND); + // global anywhere rules + for (state in states) { + table.addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND); + table.addMany(r(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND); + table.addMany(r(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND); + table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator + table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC + table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC + table.addMany([0x98, 0x9e, 0x9f], state, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI + table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS + } + // rules for executables and 7f + table.addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND); + table.addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE); + table.add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE); + table.addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING); + table.addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY); + table.add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY); + table.addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM); + table.add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM); + table.addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE); + table.addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE); + table.add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE); + table.addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE); + table.add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE); + // osc + table.add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING); + table.addMany(PRINTABLES, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING); + table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING); + table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND); + table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING); + // sos/pm/apc does nothing + table.addMany([0x58, 0x5e, 0x5f], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING); + table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND); + // csi entries + table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY); + table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND); + table.addMany(r(0x30, 0x3a), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM); + table.add(0x3b, ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM); + table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM); + table.addMany(r(0x30, 0x3a), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM); + table.add(0x3b, ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM); + table.addMany(r(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND); + table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.addMany(r(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.addMany(r(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND); + table.add(0x3a, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.addMany(r(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE); + table.addMany(r(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE); + table.addMany(r(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE); + table.addMany(r(0x40, 0x7f), ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND); + table.addMany(r(0x20, 0x30), ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE); + // esc_intermediate + table.addMany(r(0x20, 0x30), ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE); + table.addMany(r(0x20, 0x30), ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE); + table.addMany(r(0x30, 0x7f), ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + table.addMany(r(0x30, 0x50), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + table.addMany(r(0x51, 0x58), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + table.addMany([0x59, 0x5a, 0x5c], ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + table.addMany(r(0x60, 0x7f), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND); + // dcs entry + table.add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY); + table.addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY); + table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY); + table.addMany(r(0x1c, 0x20), ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY); + table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE); + table.add(0x3a, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x30, 0x3a), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM); + table.add(0x3b, ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM); + table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM); + table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x1c, 0x20), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM); + table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM); + table.addMany(r(0x1c, 0x20), ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM); + table.addMany(r(0x30, 0x3a), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM); + table.add(0x3b, ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM); + table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE); + table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE); + table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE); + table.addMany(r(0x1c, 0x20), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE); + table.addMany(r(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE); + table.addMany(r(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE); + table.addMany(r(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH); + table.addMany(r(0x40, 0x7f), ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH); + table.addMany(r(0x40, 0x7f), ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH); + table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); + table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH); + table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH); + table.addMany([0x1b, 0x9c], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND); + return table; +})(); + +/** + * Dummy DCS handler as default fallback. + */ +class DcsDummy implements IDcsHandler { + hook(collect: string, params: number[], flag: number): void { } + put(data: string, start: number, end: number): void { } + unhook(): void { } +} + +/** + * EscapeSequenceParser. + * This class implements the ANSI/DEC compatible parser described by + * Paul Williams (https://vt100.net/emu/dec_ansi_parser). + * To implement custom ANSI compliant escape sequences it is not needed to + * alter this parser, instead consider registering a custom handler. + * For non ANSI compliant sequences change the transition table with + * the optional `transitions` contructor argument and + * reimplement the `parse` method. + * NOTE: The parameter element notation is currently not supported. + * TODO: implement error recovery hook via error handler return values + */ +export class EscapeSequenceParser implements IEscapeSequenceParser { + public initialState: number; + public currentState: number; + + // buffers over several parse calls + protected _osc: string; + protected _params: number[]; + protected _collect: string; + + // handler lookup containers + protected _printHandler: (data: string, start: number, end: number) => void; + protected _executeHandlers: any; + protected _csiHandlers: any; + protected _escHandlers: any; + protected _oscHandlers: any; + protected _dcsHandlers: any; + protected _activeDcsHandler: IDcsHandler | null; + protected _errorHandler: (state: IParsingState) => IParsingState; + + // fallback handlers + protected _printHandlerFb: (data: string, start: number, end: number) => void; + protected _executeHandlerFb: (code: number) => void; + protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void; + protected _escHandlerFb: (collect: string, flag: number) => void; + protected _oscHandlerFb: (identifier: number, data: string) => void; + protected _dcsHandlerFb: IDcsHandler; + protected _errorHandlerFb: (state: IParsingState) => IParsingState; + + constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) { + this.initialState = ParserState.GROUND; + this.currentState = this.initialState; + this._osc = ''; + this._params = [0]; + this._collect = ''; + + // set default fallback handlers and handler lookup containers + this._printHandlerFb = (data, start, end): void => { }; + this._executeHandlerFb = (code: number): void => { }; + this._csiHandlerFb = (collect: string, params: number[], flag: number): void => { }; + this._escHandlerFb = (collect: string, flag: number): void => { }; + this._oscHandlerFb = (identifier: number, data: string): void => { }; + this._dcsHandlerFb = new DcsDummy(); + this._errorHandlerFb = (state: IParsingState): IParsingState => state; + this._printHandler = this._printHandlerFb; + this._executeHandlers = Object.create(null); + this._csiHandlers = Object.create(null); + this._escHandlers = Object.create(null); + this._oscHandlers = Object.create(null); + this._dcsHandlers = Object.create(null); + this._activeDcsHandler = null; + this._errorHandler = this._errorHandlerFb; + } + + setPrintHandler(callback: (data: string, start: number, end: number) => void): void { + this._printHandler = callback; + } + clearPrintHandler(): void { + this._printHandler = this._printHandlerFb; + } + + setExecuteHandler(flag: string, callback: () => void): void { + this._executeHandlers[flag.charCodeAt(0)] = callback; + } + clearExecuteHandler(flag: string): void { + if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)]; + } + setExecuteHandlerFallback(callback: (code: number) => void): void { + this._executeHandlerFb = callback; + } + + setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void { + this._csiHandlers[flag.charCodeAt(0)] = callback; + } + clearCsiHandler(flag: string): void { + if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)]; + } + setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void { + this._csiHandlerFb = callback; + } + + setEscHandler(collectAndFlag: string, callback: () => void): void { + this._escHandlers[collectAndFlag] = callback; + } + clearEscHandler(collectAndFlag: string): void { + if (this._escHandlers[collectAndFlag]) delete this._escHandlers[collectAndFlag]; + } + setEscHandlerFallback(callback: (collect: string, flag: number) => void): void { + this._escHandlerFb = callback; + } + + setOscHandler(ident: number, callback: (data: string) => void): void { + this._oscHandlers[ident] = callback; + } + clearOscHandler(ident: number): void { + if (this._oscHandlers[ident]) delete this._oscHandlers[ident]; + } + setOscHandlerFallback(callback: (identifier: number, data: string) => void): void { + this._oscHandlerFb = callback; + } + + setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void { + this._dcsHandlers[collectAndFlag] = handler; + } + clearDcsHandler(collectAndFlag: string): void { + if (this._dcsHandlers[collectAndFlag]) delete this._dcsHandlers[collectAndFlag]; + } + setDcsHandlerFallback(handler: IDcsHandler): void { + this._dcsHandlerFb = handler; + } + + setErrorHandler(callback: (state: IParsingState) => IParsingState): void { + this._errorHandler = callback; + } + clearErrorHandler(): void { + this._errorHandler = this._errorHandlerFb; + } + + reset(): void { + this.currentState = this.initialState; + this._osc = ''; + this._params = [0]; + this._collect = ''; + this._activeDcsHandler = null; + } + + parse(data: string): void { + let code = 0; + let transition = 0; + let error = false; + let currentState = this.currentState; + let print = -1; + let dcs = -1; + let osc = this._osc; + let collect = this._collect; + let params = this._params; + const table: Uint8Array | number[] = this.TRANSITIONS.table; + let dcsHandler: IDcsHandler | null = this._activeDcsHandler; + let callback: Function | null = null; + + // process input string + const l = data.length; + for (let i = 0; i < l; ++i) { + code = data.charCodeAt(i); + + // shortcut for most chars (print action) + if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) { + print = (~print) ? print : i; + do code = data.charCodeAt(++i); + while (i < l && code > 0x1f && code < 0x80); + i--; + continue; + } + + // shortcut for CSI params + if (currentState === ParserState.CSI_PARAM && (code > 0x2f && code < 0x39)) { + params[params.length - 1] = params[params.length - 1] * 10 + code - 48; + continue; + } + + // normal transition & action lookup + transition = (code < 0xa0) ? (table[currentState << 8 | code]) : DEFAULT_TRANSITION; + switch (transition >> 4) { + case ParserAction.PRINT: + print = (~print) ? print : i; + break; + case ParserAction.EXECUTE: + if (~print) { + this._printHandler(data, print, i); + print = -1; + } + callback = this._executeHandlers[code]; + if (callback) callback(); + else this._executeHandlerFb(code); + break; + case ParserAction.IGNORE: + // handle leftover print or dcs chars + if (~print) { + this._printHandler(data, print, i); + print = -1; + } else if (~dcs) { + dcsHandler.put(data, dcs, i); + dcs = -1; + } + break; + case ParserAction.ERROR: + // chars higher than 0x9f are handled by this action + // to keep the transition table small + if (code > 0x9f) { + switch (currentState) { + case ParserState.GROUND: + print = (~print) ? print : i; + break; + case ParserState.OSC_STRING: + osc += String.fromCharCode(code); + transition |= ParserState.OSC_STRING; + break; + case ParserState.CSI_IGNORE: + transition |= ParserState.CSI_IGNORE; + break; + case ParserState.DCS_IGNORE: + transition |= ParserState.DCS_IGNORE; + break; + case ParserState.DCS_PASSTHROUGH: + dcs = (~dcs) ? dcs : i; + transition |= ParserState.DCS_PASSTHROUGH; + break; + default: + error = true; + } + } else { + error = true; + } + // if we end up here a real error happened + if (error) { + const inject: IParsingState = this._errorHandler( + { + position: i, + code, + currentState, + print, + dcs, + osc, + collect, + params, + abort: false + }); + if (inject.abort) return; + // TODO: inject return values + error = false; + } + break; + case ParserAction.CSI_DISPATCH: + callback = this._csiHandlers[code]; + if (callback) callback(params, collect); + else this._csiHandlerFb(collect, params, code); + break; + case ParserAction.PARAM: + if (code === 0x3b) params.push(0); + else params[params.length - 1] = params[params.length - 1] * 10 + code - 48; + break; + case ParserAction.COLLECT: + collect += String.fromCharCode(code); + break; + case ParserAction.ESC_DISPATCH: + callback = this._escHandlers[collect + String.fromCharCode(code)]; + if (callback) callback(collect, code); + else this._escHandlerFb(collect, code); + break; + case ParserAction.CLEAR: + if (~print) { + this._printHandler(data, print, i); + print = -1; + } + osc = ''; + params = [0]; + collect = ''; + dcs = -1; + break; + case ParserAction.DCS_HOOK: + dcsHandler = this._dcsHandlers[collect + String.fromCharCode(code)]; + if (!dcsHandler) dcsHandler = this._dcsHandlerFb; + dcsHandler.hook(collect, params, code); + break; + case ParserAction.DCS_PUT: + dcs = (~dcs) ? dcs : i; + break; + case ParserAction.DCS_UNHOOK: + if (dcsHandler) { + if (~dcs) dcsHandler.put(data, dcs, i); + dcsHandler.unhook(); + dcsHandler = null; + } + if (code === 0x1b) transition |= ParserState.ESCAPE; + osc = ''; + params = [0]; + collect = ''; + dcs = -1; + break; + case ParserAction.OSC_START: + if (~print) { + this._printHandler(data, print, i); + print = -1; + } + osc = ''; + break; + case ParserAction.OSC_PUT: + osc += data.charAt(i); + break; + case ParserAction.OSC_END: + if (osc && code !== 0x18 && code !== 0x1a) { + // NOTE: OSC subparsing is not part of the original parser + // we do basic identifier parsing here to offer a jump table for OSC as well + const idx = osc.indexOf(';'); + if (idx === -1) { + this._oscHandlerFb(-1, osc); // this is an error (malformed OSC) + } else { + // Note: NaN is not handled here + // either catch it with the fallback handler + // or with an explicit NaN OSC handler + const identifier = parseInt(osc.substring(0, idx)); + const content = osc.substring(idx + 1); + callback = this._oscHandlers[identifier]; + if (callback) callback(content); + else this._oscHandlerFb(identifier, content); + } + } + if (code === 0x1b) transition |= ParserState.ESCAPE; + osc = ''; + params = [0]; + collect = ''; + dcs = -1; + break; + } + currentState = transition & 15; + } + + // push leftover pushable buffers to terminal + if (currentState === ParserState.GROUND && ~print) { + this._printHandler(data, print, data.length); + } else if (currentState === ParserState.DCS_PASSTHROUGH && ~dcs && dcsHandler) { + dcsHandler.put(data, dcs, data.length); + } + + // save non pushable buffers + this._osc = osc; + this._collect = collect; + this._params = params; + + // save active dcs handler reference + this._activeDcsHandler = dcsHandler; + + // save state + this.currentState = currentState; + } +} diff --git a/src/EscapeSequences.ts b/src/EscapeSequences.ts index 11cbd84e..e35f01dd 100644 --- a/src/EscapeSequences.ts +++ b/src/EscapeSequences.ts @@ -77,3 +77,74 @@ export namespace C0 { /** Delete (Caret = ^?) */ export const DEL = '\x7f'; } + +/** + * C1 control codes + * See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes + */ +export namespace C1 { + /** padding character */ + export const PAD = '\x80'; + /** High Octet Preset */ + export const HOP = '\x81'; + /** Break Permitted Here */ + export const BPH = '\x82'; + /** No Break Here */ + export const NBH = '\x83'; + /** Index */ + export const IND = '\x84'; + /** Next Line */ + export const NEL = '\x85'; + /** Start of Selected Area */ + export const SSA = '\x86'; + /** End of Selected Area */ + export const ESA = '\x87'; + /** Horizontal Tabulation Set */ + export const HTS = '\x88'; + /** Horizontal Tabulation With Justification */ + export const HTJ = '\x89'; + /** Vertical Tabulation Set */ + export const VTS = '\x8a'; + /** Partial Line Down */ + export const PLD = '\x8b'; + /** Partial Line Up */ + export const PLU = '\x8c'; + /** Reverse Index */ + export const RI = '\x8d'; + /** Single-Shift 2 */ + export const SS2 = '\x8e'; + /** Single-Shift 3 */ + export const SS3 = '\x8f'; + /** Device Control String */ + export const DCS = '\x90'; + /** Private Use 1 */ + export const PU1 = '\x91'; + /** Private Use 2 */ + export const PU2 = '\x92'; + /** Set Transmit State */ + export const STS = '\x93'; + /** Destructive backspace, intended to eliminate ambiguity about meaning of BS. */ + export const CCH = '\x94'; + /** Message Waiting */ + export const MW = '\x95'; + /** Start of Protected Area */ + export const SPA = '\x96'; + /** End of Protected Area */ + export const EPA = '\x97'; + /** Start of String */ + export const SOS = '\x98'; + /** Single Graphic Character Introducer */ + export const SGCI = '\x99'; + /** Single Character Introducer */ + export const SCI = '\x9a'; + /** Control Sequence Introducer */ + export const CSI = '\x9b'; + /** String Terminator */ + export const ST = '\x9c'; + /** Operating System Command */ + export const OSC = '\x9d'; + /** Privacy Message */ + export const PM = '\x9e'; + /** Application Program Command */ + export const APC = '\x9f'; +} diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts index 0698386e..9ce31bb5 100644 --- a/src/EventEmitter.ts +++ b/src/EventEmitter.ts @@ -44,7 +44,7 @@ export class EventEmitter implements IEventEmitter, IDisposable { return; } - let obj = this._events[type]; + const obj = this._events[type]; let i = obj.length; while (i--) { @@ -65,7 +65,7 @@ export class EventEmitter implements IEventEmitter, IDisposable { if (!this._events[type]) { return; } - let obj = this._events[type]; + const obj = this._events[type]; for (let i = 0; i < obj.length; i++) { obj[i].apply(this, args); } diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts index eb7b70da..6dc043db 100644 --- a/src/InputHandler.test.ts +++ b/src/InputHandler.test.ts @@ -9,10 +9,10 @@ import { MockInputHandlingTerminal } from './utils/TestUtils.test'; describe('InputHandler', () => { describe('save and restore cursor', () => { - let terminal = new MockInputHandlingTerminal(); + const terminal = new MockInputHandlingTerminal(); terminal.buffer.x = 1; terminal.buffer.y = 2; - let inputHandler = new InputHandler(terminal); + const inputHandler = new InputHandler(terminal); // Save cursor position inputHandler.saveCursor([]); assert.equal(terminal.buffer.x, 1); @@ -27,55 +27,56 @@ describe('InputHandler', () => { }); describe('setCursorStyle', () => { it('should call Terminal.setOption with correct params', () => { - let terminal = new MockInputHandlingTerminal(); - let inputHandler = new InputHandler(terminal); + const terminal = new MockInputHandlingTerminal(); + const inputHandler = new InputHandler(terminal); + const collect = ' '; - inputHandler.setCursorStyle([0]); + inputHandler.setCursorStyle([0], collect); assert.equal(terminal.options['cursorStyle'], 'block'); assert.equal(terminal.options['cursorBlink'], true); terminal.options = {}; - inputHandler.setCursorStyle([1]); + inputHandler.setCursorStyle([1], collect); assert.equal(terminal.options['cursorStyle'], 'block'); assert.equal(terminal.options['cursorBlink'], true); terminal.options = {}; - inputHandler.setCursorStyle([2]); + inputHandler.setCursorStyle([2], collect); assert.equal(terminal.options['cursorStyle'], 'block'); assert.equal(terminal.options['cursorBlink'], false); terminal.options = {}; - inputHandler.setCursorStyle([3]); + inputHandler.setCursorStyle([3], collect); assert.equal(terminal.options['cursorStyle'], 'underline'); assert.equal(terminal.options['cursorBlink'], true); terminal.options = {}; - inputHandler.setCursorStyle([4]); + inputHandler.setCursorStyle([4], collect); assert.equal(terminal.options['cursorStyle'], 'underline'); assert.equal(terminal.options['cursorBlink'], false); terminal.options = {}; - inputHandler.setCursorStyle([5]); + inputHandler.setCursorStyle([5], collect); assert.equal(terminal.options['cursorStyle'], 'bar'); assert.equal(terminal.options['cursorBlink'], true); terminal.options = {}; - inputHandler.setCursorStyle([6]); + inputHandler.setCursorStyle([6], collect); assert.equal(terminal.options['cursorStyle'], 'bar'); assert.equal(terminal.options['cursorBlink'], false); }); }); describe('setMode', () => { it('should toggle Terminal.bracketedPasteMode', () => { - let terminal = new MockInputHandlingTerminal(); - terminal.prefix = '?'; + const terminal = new MockInputHandlingTerminal(); + const collect = '?'; terminal.bracketedPasteMode = false; - let inputHandler = new InputHandler(terminal); + const inputHandler = new InputHandler(terminal); // Set bracketed paste mode - inputHandler.setMode([2004]); + inputHandler.setMode([2004], collect); assert.equal(terminal.bracketedPasteMode, true); // Reset bracketed paste mode - inputHandler.resetMode([2004]); + inputHandler.resetMode([2004], collect); assert.equal(terminal.bracketedPasteMode, false); }); }); diff --git a/src/InputHandler.ts b/src/InputHandler.ts index 7696324e..dbf6dfb2 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -4,12 +4,104 @@ * @license MIT */ -import { CharData, IInputHandler, IInputHandlingTerminal } from './Types'; -import { C0 } from './EscapeSequences'; -import { DEFAULT_CHARSET } from './Charsets'; -import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer'; +import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, ICharset } from './Types'; +import { C0, C1 } from './EscapeSequences'; +import { CHARSETS, DEFAULT_CHARSET } from './Charsets'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR } from './Buffer'; import { FLAGS } from './renderer/Types'; import { wcwidth } from './CharWidth'; +import { EscapeSequenceParser } from './EscapeSequenceParser'; + +/** + * Map collect to glevel. Used in `selectCharset`. + */ +const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2}; + + +/** + * DCS subparser implementations + */ + + /** + * DCS + q Pt ST (xterm) + * Request Terminfo String + * not supported + */ +class RequestTerminfo implements IDcsHandler { + private _data: string; + constructor(private _terminal: any) { } + hook(collect: string, params: number[], flag: number): void { + this._data = ''; + } + put(data: string, start: number, end: number): void { + this._data += data.substring(start, end); + } + unhook(): void { + // invalid: DCS 0 + r Pt ST + this._terminal.send(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`); + } +} + +/** + * DCS $ q Pt ST + * DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html) + * Request Status String (DECRQSS), VT420 and up. + * Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html) + */ +class DECRQSS implements IDcsHandler { + private _data: string; + + constructor(private _terminal: any) { } + + hook(collect: string, params: number[], flag: number): void { + // reset data + this._data = ''; + } + + put(data: string, start: number, end: number): void { + this._data += data.substring(start, end); + } + + unhook(): void { + switch (this._data) { + // valid: DCS 1 $ r Pt ST (xterm) + case '"q': // DECSCA + return this._terminal.send(`${C0.ESC}P1$r0"q${C0.ESC}\\`); + case '"p': // DECSCL + return this._terminal.send(`${C0.ESC}P1$r61"p${C0.ESC}\\`); + case 'r': // DECSTBM + const pt = '' + (this._terminal.buffer.scrollTop + 1) + + ';' + (this._terminal.buffer.scrollBottom + 1) + 'r'; + return this._terminal.send(`${C0.ESC}P1$r${pt}${C0.ESC}\\`); + case 'm': // SGR + // TODO: report real settings instead of 0m + return this._terminal.send(`${C0.ESC}P1$r0m${C0.ESC}\\`); + case ' q': // DECSCUSR + const STYLES: {[key: string]: number} = {'block': 2, 'underline': 4, 'bar': 6}; + let style = STYLES[this._terminal.getOption('cursorStyle')]; + style -= this._terminal.getOption('cursorBlink'); + return this._terminal.send(`${C0.ESC}P1$r${style} q${C0.ESC}\\`); + default: + // invalid: DCS 0 $ r Pt ST (xterm) + this._terminal.error('Unknown DCS $q %s', this._data); + this._terminal.send(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`); + } + } +} + +/** + * DCS Ps; Ps| Pt ST + * DECUDK (https://vt100.net/docs/vt510-rm/DECUDK.html) + * not supported + */ + + /** + * DCS + p Pt ST (xterm) + * Set Terminfo Data + * not supported + */ + + /** * The terminal's standard implementation of IInputHandler, this handles all @@ -19,53 +111,281 @@ import { wcwidth } from './CharWidth'; * each function's header comment. */ export class InputHandler implements IInputHandler { - constructor(private _terminal: IInputHandlingTerminal) { } + private _surrogateHigh: string; - public addChar(char: string, code: number): void { - if (char >= ' ') { + constructor( + private _terminal: any, // TODO: reestablish IInputHandlingTerminal here + private _parser: IEscapeSequenceParser = new EscapeSequenceParser()) + { + this._surrogateHigh = ''; - // make buffer local for faster access - const buffer = this._terminal.buffer; + /** + * custom fallback handlers + */ + this._parser.setCsiHandlerFallback((collect: string, params: number[], flag: number) => { + this._terminal.error('Unknown CSI code: ', collect, params, String.fromCharCode(flag)); + }); + this._parser.setEscHandlerFallback((collect: string, flag: number) => { + this._terminal.error('Unknown ESC code: ', collect, String.fromCharCode(flag)); + }); + this._parser.setExecuteHandlerFallback((code: number) => { + this._terminal.error('Unknown EXECUTE code: ', code); + }); + this._parser.setOscHandlerFallback((identifier: number, data: string) => { + this._terminal.error('Unknown OSC code: ', identifier, data); + }); + + /** + * print handler + */ + this._parser.setPrintHandler((data, start, end): void => this.print(data, start, end)); + + /** + * CSI handler + */ + this._parser.setCsiHandler('@', (params, collect) => this.insertChars(params)); + this._parser.setCsiHandler('A', (params, collect) => this.cursorUp(params)); + this._parser.setCsiHandler('B', (params, collect) => this.cursorDown(params)); + this._parser.setCsiHandler('C', (params, collect) => this.cursorForward(params)); + this._parser.setCsiHandler('D', (params, collect) => this.cursorBackward(params)); + this._parser.setCsiHandler('E', (params, collect) => this.cursorNextLine(params)); + this._parser.setCsiHandler('F', (params, collect) => this.cursorPrecedingLine(params)); + this._parser.setCsiHandler('G', (params, collect) => this.cursorCharAbsolute(params)); + this._parser.setCsiHandler('H', (params, collect) => this.cursorPosition(params)); + this._parser.setCsiHandler('I', (params, collect) => this.cursorForwardTab(params)); + this._parser.setCsiHandler('J', (params, collect) => this.eraseInDisplay(params)); + this._parser.setCsiHandler('K', (params, collect) => this.eraseInLine(params)); + this._parser.setCsiHandler('L', (params, collect) => this.insertLines(params)); + this._parser.setCsiHandler('M', (params, collect) => this.deleteLines(params)); + this._parser.setCsiHandler('P', (params, collect) => this.deleteChars(params)); + this._parser.setCsiHandler('S', (params, collect) => this.scrollUp(params)); + this._parser.setCsiHandler('T', (params, collect) => this.scrollDown(params, collect)); + this._parser.setCsiHandler('X', (params, collect) => this.eraseChars(params)); + this._parser.setCsiHandler('Z', (params, collect) => this.cursorBackwardTab(params)); + this._parser.setCsiHandler('`', (params, collect) => this.charPosAbsolute(params)); + this._parser.setCsiHandler('a', (params, collect) => this.hPositionRelative(params)); + this._parser.setCsiHandler('b', (params, collect) => this.repeatPrecedingCharacter(params)); + this._parser.setCsiHandler('c', (params, collect) => this.sendDeviceAttributes(params, collect)); + this._parser.setCsiHandler('d', (params, collect) => this.linePosAbsolute(params)); + this._parser.setCsiHandler('e', (params, collect) => this.vPositionRelative(params)); + this._parser.setCsiHandler('f', (params, collect) => this.hVPosition(params)); + this._parser.setCsiHandler('g', (params, collect) => this.tabClear(params)); + this._parser.setCsiHandler('h', (params, collect) => this.setMode(params, collect)); + this._parser.setCsiHandler('l', (params, collect) => this.resetMode(params, collect)); + this._parser.setCsiHandler('m', (params, collect) => this.charAttributes(params)); + this._parser.setCsiHandler('n', (params, collect) => this.deviceStatus(params, collect)); + this._parser.setCsiHandler('p', (params, collect) => this.softReset(params, collect)); + this._parser.setCsiHandler('q', (params, collect) => this.setCursorStyle(params, collect)); + this._parser.setCsiHandler('r', (params, collect) => this.setScrollRegion(params, collect)); + this._parser.setCsiHandler('s', (params, collect) => this.saveCursor(params)); + this._parser.setCsiHandler('u', (params, collect) => this.restoreCursor(params)); + + /** + * execute handler + */ + this._parser.setExecuteHandler(C0.BEL, () => this.bell()); + this._parser.setExecuteHandler(C0.LF, () => this.lineFeed()); + this._parser.setExecuteHandler(C0.VT, () => this.lineFeed()); + this._parser.setExecuteHandler(C0.FF, () => this.lineFeed()); + this._parser.setExecuteHandler(C0.CR, () => this.carriageReturn()); + this._parser.setExecuteHandler(C0.BS, () => this.backspace()); + this._parser.setExecuteHandler(C0.HT, () => this.tab()); + this._parser.setExecuteHandler(C0.SO, () => this.shiftOut()); + this._parser.setExecuteHandler(C0.SI, () => this.shiftIn()); + // FIXME: What do to with missing? Old code just added those to print. + + // some C1 control codes - FIXME: should those be enabled by default? + this._parser.setExecuteHandler(C1.IND, () => this.index()); + this._parser.setExecuteHandler(C1.NEL, () => this.nextLine()); + this._parser.setExecuteHandler(C1.HTS, () => this.tabSet()); + + /** + * OSC handler + */ + // 0 - icon name + title + this._parser.setOscHandler(0, (data) => this.setTitle(data)); + // 1 - icon name + // 2 - title + this._parser.setOscHandler(2, (data) => this.setTitle(data)); + // 3 - set property X in the form "prop=value" + // 4 - Change Color Number + // 5 - Change Special Color Number + // 6 - Enable/disable Special Color Number c + // 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939) + // 10 - Change VT100 text foreground color to Pt. + // 11 - Change VT100 text background color to Pt. + // 12 - Change text cursor color to Pt. + // 13 - Change mouse foreground color to Pt. + // 14 - Change mouse background color to Pt. + // 15 - Change Tektronix foreground color to Pt. + // 16 - Change Tektronix background color to Pt. + // 17 - Change highlight background color to Pt. + // 18 - Change Tektronix cursor color to Pt. + // 19 - Change highlight foreground color to Pt. + // 46 - Change Log File to Pt. + // 50 - Set Font to Pt. + // 51 - reserved for Emacs shell. + // 52 - Manipulate Selection Data. + // 104 ; c - Reset Color Number c. + // 105 ; c - Reset Special Color Number c. + // 106 ; c; f - Enable/disable Special Color Number c. + // 110 - Reset VT100 text foreground color. + // 111 - Reset VT100 text background color. + // 112 - Reset text cursor color. + // 113 - Reset mouse foreground color. + // 114 - Reset mouse background color. + // 115 - Reset Tektronix foreground color. + // 116 - Reset Tektronix background color. + // 117 - Reset highlight color. + // 118 - Reset Tektronix cursor color. + // 119 - Reset highlight foreground color. + + /** + * ESC handlers + */ + this._parser.setEscHandler('7', () => this.saveCursor([])); + this._parser.setEscHandler('8', () => this.restoreCursor([])); + this._parser.setEscHandler('D', () => this.index()); + this._parser.setEscHandler('E', () => this.nextLine()); + this._parser.setEscHandler('H', () => this.tabSet()); + this._parser.setEscHandler('M', () => this.reverseIndex()); + this._parser.setEscHandler('=', () => this.keypadApplicationMode()); + this._parser.setEscHandler('>', () => this.keypadNumericMode()); + this._parser.setEscHandler('c', () => this.reset()); + this._parser.setEscHandler('n', () => this.setgLevel(2)); + this._parser.setEscHandler('o', () => this.setgLevel(3)); + this._parser.setEscHandler('|', () => this.setgLevel(3)); + this._parser.setEscHandler('}', () => this.setgLevel(2)); + this._parser.setEscHandler('~', () => this.setgLevel(1)); + this._parser.setEscHandler('%@', () => this.selectDefaultCharset()); + this._parser.setEscHandler('%G', () => this.selectDefaultCharset()); + for (const flag in CHARSETS) { + this._parser.setEscHandler('(' + flag, () => this.selectCharset('(' + flag)); + this._parser.setEscHandler(')' + flag, () => this.selectCharset(')' + flag)); + this._parser.setEscHandler('*' + flag, () => this.selectCharset('*' + flag)); + this._parser.setEscHandler('+' + flag, () => this.selectCharset('+' + flag)); + this._parser.setEscHandler('-' + flag, () => this.selectCharset('-' + flag)); + this._parser.setEscHandler('.' + flag, () => this.selectCharset('.' + flag)); + this._parser.setEscHandler('/' + flag, () => this.selectCharset('/' + flag)); // TODO: supported? + } + + /** + * error handler + */ + this._parser.setErrorHandler((state) => { + this._terminal.error('Parsing error: ', state); + return state; + }); + + /** + * DCS handler + */ + this._parser.setDcsHandler('$q', new DECRQSS(this._terminal)); + this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal)); + } + + public parse(data: string): void { + let buffer = this._terminal.buffer; + const cursorStartX = buffer.x; + const cursorStartY = buffer.y; + if (this._terminal.debug) { + this._terminal.log('data: ' + data); + } + + // apply leftover surrogate high from last write + if (this._surrogateHigh) { + data = this._surrogateHigh + data; + this._surrogateHigh = ''; + } + + this._parser.parse(data); + + buffer = this._terminal.buffer; + if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { + this._terminal.emit('cursormove'); + } + } + + public print(data: string, start: number, end: number): void { + let char: string; + let code: number; + let low: number; + let chWidth: number; + const buffer: IBuffer = this._terminal.buffer; + const charset: ICharset = this._terminal.charset; + const screenReaderMode: boolean = this._terminal.options.screenReaderMode; + const cols: number = this._terminal.cols; + const wraparoundMode: boolean = this._terminal.wraparoundMode; + const insertMode: boolean = this._terminal.insertMode; + const curAttr: number = this._terminal.curAttr; + let bufferRow = buffer.lines.get(buffer.y + buffer.ybase); + + this._terminal.updateRange(buffer.y); + for (let stringPosition = start; stringPosition < end; ++stringPosition) { + char = data.charAt(stringPosition); + code = data.charCodeAt(stringPosition); + + // surrogate pair handling + if (0xD800 <= code && code <= 0xDBFF) { + // we got a surrogate high + // get surrogate low (next 2 bytes) + low = data.charCodeAt(stringPosition + 1); + if (isNaN(low)) { + // end of data stream, save surrogate high + this._surrogateHigh = char; + continue; + } + code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; + char += data.charAt(stringPosition + 1); + } + // surrogate low - already handled above + if (0xDC00 <= code && code <= 0xDFFF) { + continue; + } // calculate print space // expensive call, therefore we save width in line buffer - const chWidth = wcwidth(code); + chWidth = wcwidth(code); - if (this._terminal.charset && this._terminal.charset[char]) { - char = this._terminal.charset[char]; + // get charset replacement character + // FIXME: Should code be replaced as well? + if (charset) { + char = charset[char] || char; } - if (this._terminal.options.screenReaderMode) { + if (screenReaderMode) { this._terminal.emit('a11y.char', char); } - let row = buffer.y + buffer.ybase; - - // insert combining char in last cell + // insert combining char at last cursor position // FIXME: needs handling after cursor jumps + // buffer.x should never be 0 for a combining char + // since they always follow a cell consuming char + // therefore we can test for buffer.x to avoid overflow left if (!chWidth && buffer.x) { - // dont overflow left - if (buffer.lines.get(row)[buffer.x - 1]) { - if (!buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { + if (bufferRow[buffer.x - 1]) { + if (!bufferRow[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) { // found empty cell after fullwidth, need to go 2 cells back - if (buffer.lines.get(row)[buffer.x - 2]) { - buffer.lines.get(row)[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; - buffer.lines.get(row)[buffer.x - 2][3] = char.charCodeAt(0); + // it is save to step 2 cells back here + // since an empty cell is only set by fullwidth chars + if (bufferRow[buffer.x - 2]) { + bufferRow[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char; + bufferRow[buffer.x - 2][CHAR_DATA_CODE_INDEX] = code; } } else { - buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; - buffer.lines.get(row)[buffer.x - 1][3] = char.charCodeAt(0); + bufferRow[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char; + bufferRow[buffer.x - 1][CHAR_DATA_CODE_INDEX] = code; } - this._terminal.updateRange(buffer.y); } - return; + continue; } // goto next line if ch would overflow // TODO: needs a global min terminal width of 2 - if (buffer.x + chWidth - 1 >= this._terminal.cols) { + if (buffer.x + chWidth - 1 >= cols) { // autowrap - DECAWM - if (this._terminal.wraparoundMode) { + // automatically wraps to the beginning of the next line + if (wraparoundMode) { buffer.x = 0; buffer.y++; if (buffer.y > buffer.scrollBottom) { @@ -76,42 +396,47 @@ export class InputHandler implements IInputHandler { // wrapped line (buffer.lines.get(buffer.y)).isWrapped = true; } + // row changed, get it again + bufferRow = buffer.lines.get(buffer.y + buffer.ybase); } else { - if (chWidth === 2) { // FIXME: check for xterm behavior - return; + if (chWidth === 2) { + // FIXME: check for xterm behavior + // What to do here? We got a wide char that does not fit into last cell + continue; } + // FIXME: Do we have to set buffer.x to cols - 1, if not wrapping? } } - row = buffer.y + buffer.ybase; // insert mode: move characters to right - if (this._terminal.insertMode) { + // To achieve insert, we remove cells from the right + // and insert empty ones at cursor position + if (insertMode) { // do this twice for a fullwidth char for (let moves = 0; moves < chWidth; ++moves) { - // remove last cell, if it's width is 0 - // we have to adjust the second last cell as well - const removed = buffer.lines.get(buffer.y + buffer.ybase).pop(); + // remove last cell + // if it's width is 0, we have to adjust the second last cell as well + const removed = bufferRow.pop(); if (removed[CHAR_DATA_WIDTH_INDEX] === 0 - && buffer.lines.get(row)[this._terminal.cols - 2] - && buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { - buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]; + && bufferRow[this._terminal.cols - 2] + && bufferRow[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) { + bufferRow[this._terminal.cols - 2] = [curAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */ ]; } // insert empty cell at cursor - buffer.lines.get(row).splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]); + bufferRow.splice(buffer.x, 0, [curAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */ ]); } } - buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)]; - buffer.x++; - this._terminal.updateRange(buffer.y); + // write current char to buffer and advance cursor + bufferRow[buffer.x++] = [curAttr, char, chWidth, code]; - // fullwidth char - set next cell width to zero and advance cursor + // fullwidth char - also set next cell to placeholder stub and advance cursor if (chWidth === 2) { - buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, '', 0, undefined]; - buffer.x++; + bufferRow[buffer.x++] = [curAttr, '', 0, undefined]; } } + this._terminal.updateRange(buffer.y); } /** @@ -461,10 +786,10 @@ export class InputHandler implements IInputHandler { // make buffer local for faster access const buffer = this._terminal.buffer; - let row: number = buffer.y + buffer.ybase; + const row: number = buffer.y + buffer.ybase; - let scrollBottomRowsOffset = this._terminal.rows - 1 - buffer.scrollBottom; - let scrollBottomAbsolute = this._terminal.rows - 1 + buffer.ybase - scrollBottomRowsOffset + 1; + const scrollBottomRowsOffset = this._terminal.rows - 1 - buffer.scrollBottom; + const scrollBottomAbsolute = this._terminal.rows - 1 + buffer.ybase - scrollBottomRowsOffset + 1; while (param--) { // test: echo -e '\e[44m\e[1L\e[0m' // blankLine(true) - xterm/linux behavior @@ -551,19 +876,21 @@ export class InputHandler implements IInputHandler { /** * CSI Ps T Scroll down Ps lines (default = 1) (SD). */ - public scrollDown(params: number[]): void { - let param = params[0] || 1; + public scrollDown(params: number[], collect?: string): void { + if (params.length < 2 && !collect) { + let param = params[0] || 1; - // make buffer local for faster access - const buffer = this._terminal.buffer; + // make buffer local for faster access + const buffer = this._terminal.buffer; - while (param--) { - buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); - buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine()); + while (param--) { + buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); + buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine()); + } + // this.maxRange(); + this._terminal.updateRange(buffer.scrollTop); + this._terminal.updateRange(buffer.scrollBottom); } - // this.maxRange(); - this._terminal.updateRange(buffer.scrollTop); - this._terminal.updateRange(buffer.scrollBottom); } /** @@ -622,7 +949,7 @@ export class InputHandler implements IInputHandler { * [columns] (default = [row,col+1]) (HPR) * reuse CSI Ps C ? */ - public HPositionRelative(params: number[]): void { + public hPositionRelative(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -643,7 +970,7 @@ export class InputHandler implements IInputHandler { const buffer = this._terminal.buffer; const line = buffer.lines.get(buffer.ybase + buffer.y); - const ch = line[buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32]; + const ch = line[buffer.x - 1] || [DEFAULT_ATTR, ' ', 1, 32]; while (param--) { line[buffer.x++] = ch; @@ -687,18 +1014,18 @@ export class InputHandler implements IInputHandler { * xterm/charproc.c - line 2012, for more information. * vim responds with ^[[?0c or ^[[?1c after the terminal's response (?) */ - public sendDeviceAttributes(params: number[]): void { + public sendDeviceAttributes(params: number[], collect?: string): void { if (params[0] > 0) { return; } - if (!this._terminal.prefix) { + if (!collect) { if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) { this._terminal.send(C0.ESC + '[?1;2c'); } else if (this._terminal.is('linux')) { this._terminal.send(C0.ESC + '[?6c'); } - } else if (this._terminal.prefix === '>') { + } else if (collect === '>') { // xterm and urxvt // seem to spit this // out around ~370 times (?). @@ -736,7 +1063,7 @@ export class InputHandler implements IInputHandler { * [rows] (default = [row+1,column]) * reuse CSI Ps B ? */ - public VPositionRelative(params: number[]): void { + public vPositionRelative(params: number[]): void { let param = params[0]; if (param < 1) { param = 1; @@ -756,7 +1083,7 @@ export class InputHandler implements IInputHandler { * Horizontal and Vertical Position [row;column] (default = * [1,1]) (HVP). */ - public HVPosition(params: number[]): void { + public hVPosition(params: number[]): void { if (params[0] < 1) params[0] = 1; if (params[1] < 1) params[1] = 1; @@ -780,7 +1107,7 @@ export class InputHandler implements IInputHandler { * http://vt100.net/annarbor/aaa-ug/section6.html */ public tabClear(params: number[]): void { - let param = params[0]; + const param = params[0]; if (param <= 0) { delete this._terminal.buffer.tabs[this._terminal.buffer.x]; } else if (param === 3) { @@ -874,7 +1201,7 @@ export class InputHandler implements IInputHandler { * Modes: * http: *vt100.net/docs/vt220-rm/chapter4.html */ - public setMode(params: number[]): void { + public setMode(params: number[], collect?: string): void { if (params.length > 1) { for (let i = 0; i < params.length; i++) { this.setMode([params[i]]); @@ -883,7 +1210,7 @@ export class InputHandler implements IInputHandler { return; } - if (!this._terminal.prefix) { + if (!collect) { switch (params[0]) { case 4: this._terminal.insertMode = true; @@ -892,7 +1219,7 @@ export class InputHandler implements IInputHandler { // this._t.convertEol = true; break; } - } else if (this._terminal.prefix === '?') { + } else if (collect === '?') { switch (params[0]) { case 1: this._terminal.applicationCursor = true; @@ -1068,7 +1395,7 @@ export class InputHandler implements IInputHandler { * Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style. * Ps = 2 0 0 4 -> Reset bracketed paste mode. */ - public resetMode(params: number[]): void { + public resetMode(params: number[], collect?: string): void { if (params.length > 1) { for (let i = 0; i < params.length; i++) { this.resetMode([params[i]]); @@ -1077,7 +1404,7 @@ export class InputHandler implements IInputHandler { return; } - if (!this._terminal.prefix) { + if (!collect) { switch (params[0]) { case 4: this._terminal.insertMode = false; @@ -1086,7 +1413,7 @@ export class InputHandler implements IInputHandler { // this._t.convertEol = false; break; } - } else if (this._terminal.prefix === '?') { + } else if (collect === '?') { switch (params[0]) { case 1: this._terminal.applicationCursor = false; @@ -1226,7 +1553,7 @@ export class InputHandler implements IInputHandler { public charAttributes(params: number[]): void { // Optimize a single SGR0. if (params.length === 1 && params[0] === 0) { - this._terminal.curAttr = this._terminal.defAttr; + this._terminal.curAttr = DEFAULT_ATTR; return; } @@ -1254,9 +1581,9 @@ export class InputHandler implements IInputHandler { bg = p - 100; } else if (p === 0) { // default - flags = this._terminal.defAttr >> 18; - fg = (this._terminal.defAttr >> 9) & 0x1ff; - bg = this._terminal.defAttr & 0x1ff; + flags = DEFAULT_ATTR >> 18; + fg = (DEFAULT_ATTR >> 9) & 0x1ff; + bg = DEFAULT_ATTR & 0x1ff; // flags = 0; // fg = 0x1ff; // bg = 0x1ff; @@ -1300,10 +1627,10 @@ export class InputHandler implements IInputHandler { flags &= ~FLAGS.INVISIBLE; } else if (p === 39) { // reset fg - fg = (this._terminal.defAttr >> 9) & 0x1ff; + fg = (DEFAULT_ATTR >> 9) & 0x1ff; } else if (p === 49) { // reset bg - bg = this._terminal.defAttr & 0x1ff; + bg = DEFAULT_ATTR & 0x1ff; } else if (p === 38) { // fg color 256 if (params[i + 1] === 2) { @@ -1336,8 +1663,8 @@ export class InputHandler implements IInputHandler { } } else if (p === 100) { // reset fg/bg - fg = (this._terminal.defAttr >> 9) & 0x1ff; - bg = this._terminal.defAttr & 0x1ff; + fg = (DEFAULT_ATTR >> 9) & 0x1ff; + bg = DEFAULT_ATTR & 0x1ff; } else { this._terminal.error('Unknown SGR attribute: %d.', p); } @@ -1369,8 +1696,8 @@ export class InputHandler implements IInputHandler { * CSI ? 5 3 n Locator available, if compiled-in, or * CSI ? 5 0 n No Locator, if not. */ - public deviceStatus(params: number[]): void { - if (!this._terminal.prefix) { + public deviceStatus(params: number[], collect?: string): void { + if (!collect) { switch (params[0]) { case 5: // status report @@ -1385,7 +1712,7 @@ export class InputHandler implements IInputHandler { + 'R'); break; } - } else if (this._terminal.prefix === '?') { + } else if (collect === '?') { // modern xterm doesnt seem to // respond to any of these except ?6, 6, and 5 switch (params[0]) { @@ -1421,21 +1748,23 @@ export class InputHandler implements IInputHandler { * CSI ! p Soft terminal reset (DECSTR). * http://vt100.net/docs/vt220-rm/table4-10.html */ - public softReset(params: number[]): void { - this._terminal.cursorHidden = false; - this._terminal.insertMode = false; - this._terminal.originMode = false; - this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false - this._terminal.applicationKeypad = false; // ? - this._terminal.viewport.syncScrollArea(); - this._terminal.applicationCursor = false; - this._terminal.buffer.scrollTop = 0; - this._terminal.buffer.scrollBottom = this._terminal.rows - 1; - this._terminal.curAttr = this._terminal.defAttr; - this._terminal.buffer.x = this._terminal.buffer.y = 0; // ? - this._terminal.charset = null; - this._terminal.glevel = 0; // ?? - this._terminal.charsets = [null]; // ?? + public softReset(params: number[], collect?: string): void { + if (collect === '!') { + this._terminal.cursorHidden = false; + this._terminal.insertMode = false; + this._terminal.originMode = false; + this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false + this._terminal.applicationKeypad = false; // ? + this._terminal.viewport.syncScrollArea(); + this._terminal.applicationCursor = false; + this._terminal.buffer.scrollTop = 0; + this._terminal.buffer.scrollBottom = this._terminal.rows - 1; + this._terminal.curAttr = DEFAULT_ATTR; + this._terminal.buffer.x = this._terminal.buffer.y = 0; // ? + this._terminal.charset = null; + this._terminal.glevel = 0; // ?? + this._terminal.charsets = [null]; // ?? + } } /** @@ -1448,24 +1777,26 @@ export class InputHandler implements IInputHandler { * Ps = 5 -> blinking bar (xterm). * Ps = 6 -> steady bar (xterm). */ - public setCursorStyle(params?: number[]): void { - const param = params[0] < 1 ? 1 : params[0]; - switch (param) { - case 1: - case 2: - this._terminal.setOption('cursorStyle', 'block'); - break; - case 3: - case 4: - this._terminal.setOption('cursorStyle', 'underline'); - break; - case 5: - case 6: - this._terminal.setOption('cursorStyle', 'bar'); - break; + public setCursorStyle(params?: number[], collect?: string): void { + if (collect === ' ') { + const param = params[0] < 1 ? 1 : params[0]; + switch (param) { + case 1: + case 2: + this._terminal.setOption('cursorStyle', 'block'); + break; + case 3: + case 4: + this._terminal.setOption('cursorStyle', 'underline'); + break; + case 5: + case 6: + this._terminal.setOption('cursorStyle', 'bar'); + break; + } + const isBlinking = param % 2 === 1; + this._terminal.setOption('cursorBlink', isBlinking); } - const isBlinking = param % 2 === 1; - this._terminal.setOption('cursorBlink', isBlinking); } /** @@ -1474,8 +1805,8 @@ export class InputHandler implements IInputHandler { * dow) (DECSTBM). * CSI ? Pm r */ - public setScrollRegion(params: number[]): void { - if (this._terminal.prefix) return; + public setScrollRegion(params: number[], collect?: string): void { + if (collect) return; this._terminal.buffer.scrollTop = (params[0] || 1) - 1; this._terminal.buffer.scrollBottom = (params[1] && params[1] <= this._terminal.rows ? params[1] : this._terminal.rows) - 1; this._terminal.buffer.x = 0; @@ -1485,6 +1816,7 @@ export class InputHandler implements IInputHandler { /** * CSI s + * ESC 7 * Save cursor (ANSI.SYS). */ public saveCursor(params: number[]): void { @@ -1495,10 +1827,147 @@ export class InputHandler implements IInputHandler { /** * CSI u + * ESC 8 * Restore cursor (ANSI.SYS). */ public restoreCursor(params: number[]): void { this._terminal.buffer.x = this._terminal.buffer.savedX || 0; this._terminal.buffer.y = this._terminal.buffer.savedY || 0; } + + + /** + * OSC 0; ST (set icon name + window title) + * OSC 2; ST (set window title) + * Proxy to set window title. Icon name is not supported. + */ + public setTitle(data: string): void { + this._terminal.handleTitle(data); + } + + /** + * ESC E + * C1.NEL + * DEC mnemonic: NEL (https://vt100.net/docs/vt510-rm/NEL) + * Moves cursor to first position on next line. + */ + public nextLine(): void { + this._terminal.buffer.x = 0; + this.index(); + } + + /** + * ESC = + * DEC mnemonic: DECKPAM (https://vt100.net/docs/vt510-rm/DECKPAM.html) + * Enables the numeric keypad to send application sequences to the host. + */ + public keypadApplicationMode(): void { + this._terminal.log('Serial port requested application keypad.'); + this._terminal.applicationKeypad = true; + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } + } + + /** + * ESC > + * DEC mnemonic: DECKPNM (https://vt100.net/docs/vt510-rm/DECKPNM.html) + * Enables the keypad to send numeric characters to the host. + */ + public keypadNumericMode(): void { + this._terminal.log('Switching back to normal keypad.'); + this._terminal.applicationKeypad = false; + if (this._terminal.viewport) { + this._terminal.viewport.syncScrollArea(); + } + } + + /** + * ESC % @ + * ESC % G + * Select default character set. UTF-8 is not supported (string are unicode anyways) + * therefore ESC % G does the same. + */ + public selectDefaultCharset(): void { + this._terminal.setgLevel(0); + this._terminal.setgCharset(0, DEFAULT_CHARSET); // US (default) + } + + /** + * ESC ( C + * Designate G0 Character Set, VT100, ISO 2022. + * ESC ) C + * Designate G1 Character Set (ISO 2022, VT100). + * ESC * C + * Designate G2 Character Set (ISO 2022, VT220). + * ESC + C + * Designate G3 Character Set (ISO 2022, VT220). + * ESC - C + * Designate G1 Character Set (VT300). + * ESC . C + * Designate G2 Character Set (VT300). + * ESC / C + * Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported? + */ + public selectCharset(collectAndFlag: string): void { + if (collectAndFlag.length !== 2) return this.selectDefaultCharset(); + if (collectAndFlag[0] === '/') return; // TODO: Is this supported? + this._terminal.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET); + } + + /** + * ESC D + * C1.IND + * DEC mnemonic: IND (https://vt100.net/docs/vt510-rm/IND.html) + * Moves the cursor down one line in the same column. + */ + public index(): void { + this._terminal.index(); // TODO: save to move from terminal? + } + + /** + * ESC H + * C1.HTS + * DEC mnemonic: HTS (https://vt100.net/docs/vt510-rm/HTS.html) + * Sets a horizontal tab stop at the column position indicated by + * the value of the active column when the terminal receives an HTS. + */ + public tabSet(): void { + this._terminal.tabSet(); // TODO: save to move from terminal? + } + + /** + * ESC M + * C1.RI + * DEC mnemonic: HTS + * Moves the cursor up one line in the same column. If the cursor is at the top margin, + * the page scrolls down. + */ + public reverseIndex(): void { + this._terminal.reverseIndex(); // TODO: save to move from terminal? + } + + /** + * ESC c + * DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html) + * Reset to initial state. + */ + public reset(): void { + this._parser.reset(); + this._terminal.reset(); // TODO: save to move from terminal? + } + + /** + * ESC n + * ESC o + * ESC | + * ESC } + * ESC ~ + * DEC mnemonic: LS (https://vt100.net/docs/vt510-rm/LS.html) + * When you use a locking shift, the character set remains in GL or GR until + * you use another locking shift. (partly supported) + */ + public setgLevel(level: number): void { + this._terminal.setgLevel(level); // TODO: save to move from terminal? + } } diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts index 61245296..4c0415de 100644 --- a/src/Linkifier.test.ts +++ b/src/Linkifier.test.ts @@ -11,9 +11,9 @@ import { MockBuffer, MockTerminal } from './utils/TestUtils.test'; import { CircularList } from './utils/CircularList'; class TestLinkifier extends Linkifier { - constructor(_terminal: ITerminal) { - super(_terminal); - Linkifier.TIME_BEFORE_LINKIFY = 0; + constructor(terminal: ITerminal) { + super(terminal); + (Linkifier).TIME_BEFORE_LINKIFY = 0; } public get linkMatchers(): ILinkMatcher[] { return this._linkMatchers; } @@ -47,7 +47,7 @@ describe('Linkifier', () => { }); function stringToRow(text: string): LineData { - let result: LineData = []; + const result: LineData = []; for (let i = 0; i < text.length; i++) { result.push([0, text.charAt(i), 1, text.charCodeAt(i)]); } diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 2a19c8d8..e548551a 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -17,7 +17,7 @@ export class Linkifier extends EventEmitter implements ILinkifier { * the costly operation of searching every row multiple times, potentially a * huge amount of times. */ - protected static TIME_BEFORE_LINKIFY = 200; + protected static readonly TIME_BEFORE_LINKIFY = 200; protected _linkMatchers: ILinkMatcher[] = []; @@ -199,11 +199,11 @@ export class Linkifier extends EventEmitter implements ILinkifier { */ private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void { // Find the first match - let match = text.match(matcher.regex); + const match = text.match(matcher.regex); if (!match || match.length === 0) { return; } - let uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex]; + const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex]; // Get index, match.index is for the outer match which includes negated chars const index = text.indexOf(uri); diff --git a/src/Parser.ts b/src/Parser.ts index 372d8443..519b9609 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -124,12 +124,12 @@ csiStateHandler['T'] = (handler, params, prefix) => { csiStateHandler['X'] = (handler, params, prefix) => handler.eraseChars(params); csiStateHandler['Z'] = (handler, params, prefix) => handler.cursorBackwardTab(params); csiStateHandler['`'] = (handler, params, prefix) => handler.charPosAbsolute(params); -csiStateHandler['a'] = (handler, params, prefix) => handler.HPositionRelative(params); +csiStateHandler['a'] = (handler, params, prefix) => handler.hPositionRelative(params); csiStateHandler['b'] = (handler, params, prefix) => handler.repeatPrecedingCharacter(params); csiStateHandler['c'] = (handler, params, prefix) => handler.sendDeviceAttributes(params); csiStateHandler['d'] = (handler, params, prefix) => handler.linePosAbsolute(params); -csiStateHandler['e'] = (handler, params, prefix) => handler.VPositionRelative(params); -csiStateHandler['f'] = (handler, params, prefix) => handler.HVPosition(params); +csiStateHandler['e'] = (handler, params, prefix) => handler.vPositionRelative(params); +csiStateHandler['f'] = (handler, params, prefix) => handler.hVPosition(params); csiStateHandler['g'] = (handler, params, prefix) => handler.tabClear(params); csiStateHandler['h'] = (handler, params, prefix) => handler.setMode(params); csiStateHandler['l'] = (handler, params, prefix) => handler.resetMode(params); @@ -232,7 +232,7 @@ export class Parser { if (ch in normalStateHandler) { normalStateHandler[ch](this, this._inputHandler); } else { - this._inputHandler.addChar(ch, code); + // this._inputHandler.addChar(ch, code); } break; case ParserState.ESCAPED: diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts index 8d0b04aa..8e89ea3f 100644 --- a/src/SelectionManager.test.ts +++ b/src/SelectionManager.test.ts @@ -51,7 +51,7 @@ describe('SelectionManager', () => { }); function stringToRow(text: string): LineData { - let result: LineData = []; + const result: LineData = []; for (let i = 0; i < text.length; i++) { result.push([0, text.charAt(i), 1, text.charCodeAt(i)]); } @@ -298,6 +298,16 @@ describe('SelectionManager', () => { assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0], 'The actual selection spans the entire column'); }); + it('should select the entire wrapped line', () => { + buffer.lines.set(0, stringToRow('foo')); + const line2 = stringToRow('bar'); + (line2).isWrapped = true; + buffer.lines.set(1, line2); + selectionManager.selectLineAt(0); + assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct'); + assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]); + assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1], 'The actual selection spans the entire column'); + }); }); describe('selectAll', () => { diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts index 93da887a..1b056e83 100644 --- a/src/SelectionManager.ts +++ b/src/SelectionManager.ts @@ -179,7 +179,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager // Get first row const startRowEndCol = start[1] === end[1] ? end[0] : null; - let result: string[] = []; + const result: string[] = []; result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol)); // Get middle rows @@ -586,7 +586,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param event The mouseup event. */ private _onMouseUp(event: MouseEvent): void { - let timeElapsed = event.timeStamp - this._mouseDownTimeStamp; + const timeElapsed = event.timeStamp - this._mouseDownTimeStamp; this._removeMouseDownListeners(); @@ -803,7 +803,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager * @param line The line index. */ protected _selectLineAt(line: number): void { - this._model.selectionStart = [0, line]; - this._model.selectionStartLength = this._terminal.cols; + const wrappedRange = this._buffer.getWrappedRangeForLine(line); + this._model.selectionStart = [0, wrappedRange.first]; + this._model.selectionEnd = [this._terminal.cols, wrappedRange.last]; + this._model.selectionStartLength = 0; } } diff --git a/src/Terminal.integration.ts b/src/Terminal.integration.ts index fdf27acf..7b7cdbdd 100644 --- a/src/Terminal.integration.ts +++ b/src/Terminal.integration.ts @@ -14,6 +14,11 @@ import * as pty from 'node-pty'; import { assert } from 'chai'; import { Terminal } from './Terminal'; import { CHAR_DATA_CHAR_INDEX } from './Buffer'; +import { IViewport } from './Types'; + +class TestTerminal extends Terminal { + innerWrite(): void { this._innerWrite(); } +} let primitivePty: any; @@ -24,8 +29,8 @@ let primitivePty: any; function ptyWriteRead(data: string, cb: (result: string) => void): void { fs.writeSync(primitivePty.slave, data); setTimeout(() => { - let b = new Buffer(64000); - let bytes = fs.readSync(primitivePty.master, b, 0, 64000, null); + const b = new Buffer(64000); + const bytes = fs.readSync(primitivePty.master, b, 0, 64000, null); cb(b.toString('utf8', 0, bytes)); }); } @@ -45,7 +50,7 @@ function formatError(input: string, output: string, expected: string): string { return '\x1b[33m' + (' ' + counter).slice(-2) + color + s; }; } - let line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890'; + const line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890'; let s = ''; s += '\n\x1b[34m' + JSON.stringify(input); s += '\n\x1b[33m ' + line80 + '\n'; @@ -74,34 +79,34 @@ function terminalToString(term: Terminal): string { // Skip tests on Windows since pty.open isn't supported if (os.platform() !== 'win32') { - let CONSOLE_LOG = console.log; + const consoleLog = console.log; // expect files need terminal at 80x25! - let COLS = 80; - let ROWS = 25; + const cols = 80; + const rows = 25; /** some helpers for pty interaction */ // we need a pty in between to get the termios decorations // for the basic test cases a raw pty device is enough - primitivePty = pty.native.open(COLS, ROWS); + primitivePty = pty.native.open(cols, rows); /** tests */ describe('xterm output comparison', () => { - let xterm; + let xterm: TestTerminal; beforeEach(() => { - xterm = new Terminal({ cols: COLS, rows: ROWS }); + xterm = new TestTerminal({ cols: cols, rows: rows }); xterm.refresh = () => {}; - xterm.viewport = { + xterm.viewport = { syncScrollArea: () => {} }; }); // omit stack trace for escape sequence files Error.stackTraceLimit = 0; - let files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')}); + const files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')}); // only successful tests for now - let skip = [ + const skip = [ 10, 16, 17, 19, 32, 33, 34, 35, 36, 39, 40, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 54, 55, 56, 57, 58, 59, 60, 61, @@ -118,21 +123,22 @@ if (os.platform() !== 'win32') { ((filename: string) => { it(filename.split('/').slice(-1)[0], done => { ptyReset(() => { - let inFile = fs.readFileSync(filename, 'utf8'); + const inFile = fs.readFileSync(filename, 'utf8'); ptyWriteRead(inFile, fromPty => { // uncomment this to get log from terminal // console.log = function(){}; // Perform a synchronous .write(data) xterm.writeBuffer.push(fromPty); - xterm._innerWrite(); + xterm.innerWrite(); + + const fromEmulator = terminalToString(xterm); + console.log = consoleLog; + const expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8'); - let fromEmulator = terminalToString(xterm); - console.log = CONSOLE_LOG; - let expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8'); // Some of the tests have whitespace on the right of lines, we trim all the linex // from xterm.js so ignore this for now at least. - let expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n'); + const expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n'); if (fromEmulator !== expectedRightTrimmed) { // uncomment to get noisy output throw new Error(formatError(inFile, fromEmulator, expected)); @@ -155,7 +161,7 @@ describe('typings', () => { tsc += '.cmd'; } const fixtureDir = path.join(__dirname, '..', 'fixtures', 'typings-test'); - let result = cp.spawnSync(tsc, { cwd: fixtureDir }); + const result = cp.spawnSync(tsc, { cwd: fixtureDir }); assert.equal(result.status, 0, `build did not succeed:\nstdout: ${result.stdout.toString()}\nstderr: ${result.stderr.toString()}\n`); // Clean up fs.unlinkSync(path.join(fixtureDir, 'typings-test.js')); diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 5a604379..f6cbad52 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -77,13 +77,13 @@ describe('term.js addons', () => { }); describe('attachCustomKeyEventHandler', () => { - let evKeyDown = { + const evKeyDown = { preventDefault: () => {}, stopPropagation: () => {}, type: 'keydown', keyCode: 77 }; - let evKeyPress = { + const evKeyPress = { preventDefault: () => {}, stopPropagation: () => {}, type: 'keypress', @@ -131,7 +131,7 @@ describe('term.js addons', () => { describe('clear', () => { it('should clear a buffer equal to rows', () => { - let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); + const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); term.clear(); assert.equal(term.buffer.y, 0); assert.equal(term.buffer.ybase, 0); @@ -148,7 +148,7 @@ describe('term.js addons', () => { term.write('test\n'); } - let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); + const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); term.clear(); assert.equal(term.buffer.y, 0); assert.equal(term.buffer.ybase, 0); @@ -160,7 +160,7 @@ describe('term.js addons', () => { } }); it('should not break the prompt when cleared twice', () => { - let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); + const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); term.clear(); term.clear(); assert.equal(term.buffer.y, 0); @@ -176,7 +176,7 @@ describe('term.js addons', () => { describe('scroll', () => { describe('scrollLines', () => { - let startYDisp; + let startYDisp: number; beforeEach(() => { for (let i = 0; i < term.rows * 2; i++) { term.writeln('test'); @@ -211,7 +211,7 @@ describe('term.js addons', () => { }); describe('scrollPages', () => { - let startYDisp; + let startYDisp: number; beforeEach(() => { for (let i = 0; i < term.rows * 3; i++) { term.writeln('test'); @@ -248,7 +248,7 @@ describe('term.js addons', () => { }); describe('scrollToBottom', () => { - let startYDisp; + let startYDisp: number; beforeEach(() => { for (let i = 0; i < term.rows * 3; i++) { term.writeln('test'); @@ -269,7 +269,7 @@ describe('term.js addons', () => { }); describe('scrollToLine', () => { - let startYDisp; + let startYDisp: number; beforeEach(() => { for (let i = 0; i < term.rows * 3; i++) { term.writeln('test'); @@ -302,7 +302,7 @@ describe('term.js addons', () => { (term)._evaluateKeyEscapeSequence = () => { return { key: 'a' }; }; - let event = { + const event = { type: 'keydown', keyCode: 0, preventDefault: () => {}, @@ -322,7 +322,7 @@ describe('term.js addons', () => { for (let i = 0; i < term.rows * 3; i++) { term.writeln('test'); } - let startYDisp = (term.rows * 2) + 1; + const startYDisp = (term.rows * 2) + 1; term.attachCustomKeyEventHandler(() => { return false; }); @@ -712,8 +712,8 @@ describe('term.js addons', () => { }); describe('Third level shift', () => { - let evKeyDown; - let evKeyPress; + let evKeyDown: any; + let evKeyPress: any; beforeEach(() => { term.handler = () => {}; @@ -784,11 +784,11 @@ describe('term.js addons', () => { }); it('should emit key with alt + key on keyPress', (done) => { - let keys = ['@', '@', '\\', '\\', '|', '|']; + const keys = ['@', '@', '\\', '\\', '|', '|']; term.on('keypress', (key) => { if (key) { - let index = keys.indexOf(key); + const index = keys.indexOf(key); assert(index !== -1, 'Emitted wrong key: ' + key); keys.splice(index, 1); } @@ -850,11 +850,11 @@ describe('term.js addons', () => { }); it('should emit key with alt + ctrl + key on keyPress', (done) => { - let keys = ['@', '@', '\\', '\\', '|', '|']; + const keys = ['@', '@', '\\', '\\', '|', '|']; term.on('keypress', (key) => { if (key) { - let index = keys.indexOf(key); + const index = keys.indexOf(key); assert(index !== -1, 'Emitted wrong key: ' + key); keys.splice(index, 1); } @@ -895,10 +895,10 @@ describe('term.js addons', () => { describe('unicode - surrogates', () => { it('2 characters per cell', function (): void { this.timeout(10000); // This is needed because istanbul patches code and slows it down - let high = String.fromCharCode(0xD800); + const high = String.fromCharCode(0xD800); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.write(high + String.fromCharCode(i)); - let tchar = term.buffer.lines.get(0)[0]; + const tchar = term.buffer.lines.get(0)[0]; expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); @@ -907,7 +907,7 @@ describe('term.js addons', () => { } }); it('2 characters at last cell', () => { - let high = String.fromCharCode(0xD800); + const high = String.fromCharCode(0xD800); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; term.write(high + String.fromCharCode(i)); @@ -918,7 +918,7 @@ describe('term.js addons', () => { } }); it('2 characters per cell over line end with autowrap', () => { - let high = String.fromCharCode(0xD800); + const high = String.fromCharCode(0xD800); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; term.wraparoundMode = true; @@ -931,7 +931,7 @@ describe('term.js addons', () => { } }); it('2 characters per cell over line end without autowrap', () => { - let high = String.fromCharCode(0xD800); + const high = String.fromCharCode(0xD800); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.buffer.x = term.cols - 1; term.wraparoundMode = false; @@ -944,11 +944,11 @@ describe('term.js addons', () => { } }); it('splitted surrogates', () => { - let high = String.fromCharCode(0xD800); + const high = String.fromCharCode(0xD800); for (let i = 0xDC00; i <= 0xDCFF; ++i) { term.write(high); term.write(String.fromCharCode(i)); - let tchar = term.buffer.lines.get(0)[0]; + const tchar = term.buffer.lines.get(0)[0]; expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i)); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); @@ -979,12 +979,12 @@ describe('term.js addons', () => { term.wraparoundMode = true; term.write(Array(100).join('e\u0301')); for (let i = 0; i < term.cols; ++i) { - let tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0)[i]; expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); } - let tchar = term.buffer.lines.get(1)[0]; + const tchar = term.buffer.lines.get(1)[0]; expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); @@ -993,12 +993,12 @@ describe('term.js addons', () => { term.wraparoundMode = true; term.write(Array(100).join('\uD800\uDC00\u0301')); for (let i = 0; i < term.cols; ++i) { - let tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0)[i]; expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); } - let tchar = term.buffer.lines.get(1)[0]; + const tchar = term.buffer.lines.get(1)[0]; expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1); @@ -1021,7 +1021,7 @@ describe('term.js addons', () => { term.wraparoundMode = true; term.write(Array(50).join('¥')); for (let i = 0; i < term.cols; ++i) { - let tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0)[i]; if (i % 2) { expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); @@ -1032,7 +1032,7 @@ describe('term.js addons', () => { expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); } } - let tchar = term.buffer.lines.get(1)[0]; + const tchar = term.buffer.lines.get(1)[0]; expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); @@ -1042,7 +1042,7 @@ describe('term.js addons', () => { term.buffer.x = 1; term.write(Array(50).join('¥')); for (let i = 1; i < term.cols - 1; ++i) { - let tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0)[i]; if (!(i % 2)) { expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); @@ -1067,7 +1067,7 @@ describe('term.js addons', () => { term.buffer.x = 1; term.write(Array(50).join('¥\u0301')); for (let i = 1; i < term.cols - 1; ++i) { - let tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0)[i]; if (!(i % 2)) { expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); @@ -1091,7 +1091,7 @@ describe('term.js addons', () => { term.wraparoundMode = true; term.write(Array(50).join('¥\u0301')); for (let i = 0; i < term.cols; ++i) { - let tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0)[i]; if (i % 2) { expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); @@ -1102,7 +1102,7 @@ describe('term.js addons', () => { expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); } } - let tchar = term.buffer.lines.get(1)[0]; + const tchar = term.buffer.lines.get(1)[0]; expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); @@ -1112,7 +1112,7 @@ describe('term.js addons', () => { term.buffer.x = 1; term.write(Array(50).join('\ud843\ude6d\u0301')); for (let i = 1; i < term.cols - 1; ++i) { - let tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0)[i]; if (!(i % 2)) { expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); @@ -1136,7 +1136,7 @@ describe('term.js addons', () => { term.wraparoundMode = true; term.write(Array(50).join('\ud843\ude6d\u0301')); for (let i = 0; i < term.cols; ++i) { - let tchar = term.buffer.lines.get(0)[i]; + const tchar = term.buffer.lines.get(0)[i]; if (i % 2) { expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(''); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0); @@ -1147,7 +1147,7 @@ describe('term.js addons', () => { expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); } } - let tchar = term.buffer.lines.get(1)[0]; + const tchar = term.buffer.lines.get(1)[0]; expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301'); expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3); expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2); diff --git a/src/Terminal.ts b/src/Terminal.ts index fc3998d8..af8fd21e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -25,14 +25,14 @@ import { ICharset, IInputHandlingTerminal, IViewport, ICompositionHelper, ITermi import { IMouseZoneManager } from './input/Types'; import { IRenderer } from './renderer/Types'; import { BufferSet } from './BufferSet'; -import { Buffer, MAX_BUFFER_SIZE } from './Buffer'; +import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR } from './Buffer'; import { CompositionHelper } from './CompositionHelper'; import { EventEmitter } from './EventEmitter'; import { Viewport } from './Viewport'; import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from './handlers/Clipboard'; import { C0 } from './EscapeSequences'; import { InputHandler } from './InputHandler'; -import { Parser } from './Parser'; +// import { Parser } from './Parser'; import { Renderer } from './renderer/Renderer'; import { Linkifier } from './Linkifier'; import { SelectionManager } from './SelectionManager'; @@ -49,9 +49,10 @@ import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './utils/ScreenDprMonitor'; import { ITheme, ILocalizableStrings, IMarker, IDisposable } from 'xterm'; import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache'; +import { DomRenderer } from './renderer/dom/DomRenderer'; // reg + shift key mappings for digits and special chars -const KEYCODE_KEY_MAPPINGS = { +const KEYCODE_KEY_MAPPINGS: { [key: number]: [string, string]} = { // digits 0-9 48: ['0', ')'], 49: ['1', '!'], @@ -94,6 +95,11 @@ const WRITE_BUFFER_PAUSE_THRESHOLD = 5; */ const WRITE_BATCH_SIZE = 300; +/** + * The set of options that only have an effect when set in the Terminal constructor. + */ +const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows', 'rendererType']; + const DEFAULT_OPTIONS: ITerminalOptions = { cols: 80, rows: 24, @@ -123,7 +129,8 @@ const DEFAULT_OPTIONS: ITerminalOptions = { allowTransparency: false, tabStopWidth: 8, theme: null, - rightClickSelectsWord: Browser.isMac + rightClickSelectsWord: Browser.isMac, + rendererType: 'canvas' }; export class Terminal extends EventEmitter implements ITerminal, IDisposable, IInputHandlingTerminal { @@ -190,13 +197,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _refreshEnd: number; public savedCols: number; - public defAttr: number; public curAttr: number; public params: (string | number)[]; public currentParam: string | number; - public prefix: string; - public postfix: string; // user input states public writeBuffer: string[]; @@ -218,7 +222,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _inputHandler: InputHandler; public soundManager: SoundManager; - private _parser: Parser; public renderer: IRenderer; public selectionManager: SelectionManager; public linkifier: ILinkifier; @@ -280,8 +283,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (this.options[key] == null) { this.options[key] = DEFAULT_OPTIONS[key]; } - // TODO: We should move away from duplicate options on the Terminal object - this[key] = this.options[key]; }); // this.context = options.context || window; @@ -316,13 +317,10 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // TODO: Can this be just []? this.charsets = [null]; - this.defAttr = (0 << 18) | (257 << 9) | (256 << 0); - this.curAttr = (0 << 18) | (257 << 9) | (256 << 0); + this.curAttr = DEFAULT_ATTR; this.params = []; this.currentParam = 0; - this.prefix = ''; - this.postfix = ''; // user input states this.writeBuffer = []; @@ -333,7 +331,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this._userScrolling = false; this._inputHandler = new InputHandler(this); - this._parser = new Parser(this._inputHandler, this); // Reuse renderer if the Terminal is being recreated via a reset call. this.renderer = this.renderer || null; this.selectionManager = this.selectionManager || null; @@ -364,8 +361,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * back_color_erase feature for xterm. */ public eraseAttr(): number { - // if (this.is('screen')) return this.defAttr; - return (this.defAttr & ~0x1ff) | (this.curAttr & 0x1ff); + // if (this.is('screen')) return DEFAULT_ATTR; + return (DEFAULT_ATTR & ~0x1ff) | (this.curAttr & 0x1ff); } /** @@ -390,11 +387,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II throw new Error('No option with key "' + key + '"'); } - if (typeof this.options[key] !== 'undefined') { - return this.options[key]; - } - - return this[key]; + return this.options[key]; } /** @@ -406,6 +399,9 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II if (!(key in DEFAULT_OPTIONS)) { throw new Error('No option with key "' + key + '"'); } + if (CONSTRUCTOR_ONLY_OPTIONS.indexOf(key) !== -1) { + console.error(`Option "${key}" can only be set in the constructor`); + } switch (key) { case 'bellStyle': if (!value) { @@ -468,7 +464,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } break; } - this[key] = value; this.options[key] = value; switch (key) { case 'fontFamily': @@ -567,7 +562,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } copyHandler(event, this, this.selectionManager); }); - const pasteHandlerWrapper = event => pasteHandler(event, this); + const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this); on(this.textarea, 'paste', pasteHandlerWrapper); on(this.element, 'paste', pasteHandlerWrapper); @@ -706,7 +701,11 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); - this.renderer = new Renderer(this, this.options.theme); + switch (this.options.rendererType) { + case 'canvas': this.renderer = new Renderer(this, this.options.theme); break; + case 'dom': this.renderer = new DomRenderer(this, this.options.theme); break; + default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); + } this.options.theme = null; this.viewport = new Viewport(this, this._viewportElement, this._viewportScrollArea, this.charMeasure); this.viewport.onThemeChanged(this.renderer.colorManager.colors); @@ -719,7 +718,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // dprchange should handle this case, we need this as well for browsers that don't support the // matchMedia query. this._disposables.push(Dom.addDisposableListener(window, 'resize', () => this.renderer.onWindowResize(window.devicePixelRatio))); - this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows)); + this.charMeasure.on('charsizechanged', () => this.renderer.onCharSizeChanged()); this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea()); this.selectionManager = new SelectionManager(this, this.charMeasure); @@ -833,7 +832,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< function sendMove(ev: MouseEvent): void { let button = pressed; - let pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows); + const pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows); if (!pos) return; // buttons marked as motions @@ -944,7 +943,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II return; } - let data: number[] = []; + const data: number[] = []; encode(data, button); encode(data, pos.x); @@ -1151,7 +1150,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II public scroll(isWrapped?: boolean): void { const newLine = this.blankLine(undefined, isWrapped); const topRow = this.buffer.ybase + this.buffer.scrollTop; - let bottomRow = this.buffer.ybase + this.buffer.scrollBottom; + const bottomRow = this.buffer.ybase + this.buffer.scrollBottom; if (this.buffer.scrollTop === 0) { // Determine whether the buffer is going to be trimmed after insertion. @@ -1298,7 +1297,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } } - private _innerWrite(): void { + protected _innerWrite(): void { const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE); while (writeBatch.length > 0) { const data = writeBatch.shift(); @@ -1318,8 +1317,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // middle of parsing escape sequence in two chunks. For some reason the // state of the parser resets to 0 after exiting parser.parse. This change // just sets the state back based on the correct return statement. - const state = this._parser.parse(data); - this._parser.setState(state); + + this._inputHandler.parse(data); this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); @@ -2063,7 +2062,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * set, the terminal's current column count would be used. */ public blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData { - const attr = cur ? this.eraseAttr() : this.defAttr; + const attr = cur ? this.eraseAttr() : DEFAULT_ATTR; const ch: CharData = [attr, ' ', 1, 32 /* ' '.charCodeAt(0) */]; // width defaults to 1 halfwidth character const line: LineData = []; @@ -2083,14 +2082,14 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } /** - * If cur return the back color xterm feature attribute. Else return defAttr. + * If cur return the back color xterm feature attribute. Else return default attribute. * @param cur */ public ch(cur?: boolean): CharData { if (cur) { return [this.eraseAttr(), ' ', 1, 32 /* ' '.charCodeAt(0) */]; } - return [this.defAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */]; + return [DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */]; } /** @@ -2213,7 +2212,42 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // TODO: Remove when true color is implemented public matchColor(r1: number, g1: number, b1: number): number { - return matchColor_(r1, g1, b1); + const hash = (r1 << 16) | (g1 << 8) | b1; + + if (matchColorCache[hash] != null) { + return matchColorCache[hash]; + } + + let ldiff = Infinity; + let li = -1; + let i = 0; + let c: number; + let r2: number; + let g2: number; + let b2: number; + let diff: number; + + for (; i < DEFAULT_ANSI_COLORS.length; i++) { + c = DEFAULT_ANSI_COLORS[i].rgba; + r2 = c >>> 24; + g2 = c >>> 16 & 0xFF; + b2 = c >>> 8 & 0xFF; + // assume that alpha is 0xFF + + diff = matchColorDistance(r1, g1, b1, r2, g2, b2); + + if (diff === 0) { + li = i; + break; + } + + if (diff < ldiff) { + ldiff = diff; + li = i; + } + } + + return matchColorCache[hash] = li; } private _visualBell(): boolean { @@ -2269,43 +2303,3 @@ function matchColorDistance(r1: number, g1: number, b1: number, r2: number, g2: + Math.pow(59 * (g1 - g2), 2) + Math.pow(11 * (b1 - b2), 2); } - - -function matchColor_(r1: number, g1: number, b1: number): number { - const hash = (r1 << 16) | (g1 << 8) | b1; - - if (matchColorCache[hash] != null) { - return matchColorCache[hash]; - } - - let ldiff = Infinity; - let li = -1; - let i = 0; - let c: number; - let r2: number; - let g2: number; - let b2: number; - let diff: number; - - for (; i < DEFAULT_ANSI_COLORS.length; i++) { - c = DEFAULT_ANSI_COLORS[i].rgba; - r2 = c >>> 24; - g2 = c >>> 16 & 0xFF; - b2 = c >>> 8 & 0xFF; - // assume that alpha is 0xFF - - diff = matchColorDistance(r1, g1, b1, r2, g2, b2); - - if (diff === 0) { - li = i; - break; - } - - if (diff < ldiff) { - ldiff = diff; - li = i; - } - } - - return matchColorCache[hash] = li; -} diff --git a/src/Types.ts b/src/Types.ts index 15498ce1..96eec3c4 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -43,9 +43,7 @@ export interface IInputHandlingTerminal extends IEventEmitter { insertMode: boolean; wraparoundMode: boolean; bracketedPasteMode: boolean; - defAttr: number; curAttr: number; - prefix: string; savedCols: number; x10Mouse: boolean; vt200Mouse: boolean; @@ -106,10 +104,11 @@ export interface ICompositionHelper { } /** - * Handles actions generated by the parser. + * Calls the parser and handles actions generated by the parser. */ export interface IInputHandler { - addChar(char: string, code: number): void; + parse(data: string): void; + print(data: string, start: number, end: number): void; /** C0 BEL */ bell(): void; /** C0 LF */ lineFeed(): void; @@ -135,26 +134,49 @@ export interface IInputHandler { /** CSI M */ deleteLines(params?: number[]): void; /** CSI P */ deleteChars(params?: number[]): void; /** CSI S */ scrollUp(params?: number[]): void; - /** CSI T */ scrollDown(params?: number[]): void; + /** CSI T */ scrollDown(params?: number[], collect?: string): void; /** CSI X */ eraseChars(params?: number[]): void; /** CSI Z */ cursorBackwardTab(params?: number[]): void; /** CSI ` */ charPosAbsolute(params?: number[]): void; - /** CSI a */ HPositionRelative(params?: number[]): void; + /** CSI a */ hPositionRelative(params?: number[]): void; /** CSI b */ repeatPrecedingCharacter(params?: number[]): void; - /** CSI c */ sendDeviceAttributes(params?: number[]): void; + /** CSI c */ sendDeviceAttributes(params?: number[], collect?: string): void; /** CSI d */ linePosAbsolute(params?: number[]): void; - /** CSI e */ VPositionRelative(params?: number[]): void; - /** CSI f */ HVPosition(params?: number[]): void; + /** CSI e */ vPositionRelative(params?: number[]): void; + /** CSI f */ hVPosition(params?: number[]): void; /** CSI g */ tabClear(params?: number[]): void; - /** CSI h */ setMode(params?: number[]): void; - /** CSI l */ resetMode(params?: number[]): void; + /** CSI h */ setMode(params?: number[], collect?: string): void; + /** CSI l */ resetMode(params?: number[], collect?: string): void; /** CSI m */ charAttributes(params?: number[]): void; - /** CSI n */ deviceStatus(params?: number[]): void; - /** CSI p */ softReset(params?: number[]): void; - /** CSI q */ setCursorStyle(params?: number[]): void; - /** CSI r */ setScrollRegion(params?: number[]): void; + /** CSI n */ deviceStatus(params?: number[], collect?: string): void; + /** CSI p */ softReset(params?: number[], collect?: string): void; + /** CSI q */ setCursorStyle(params?: number[], collect?: string): void; + /** CSI r */ setScrollRegion(params?: number[], collect?: string): void; /** CSI s */ saveCursor(params?: number[]): void; /** CSI u */ restoreCursor(params?: number[]): void; + /** OSC 0 + OSC 2 */ setTitle(data: string): void; + /** ESC E */ nextLine(): void; + /** ESC = */ keypadApplicationMode(): void; + /** ESC > */ keypadNumericMode(): void; + /** ESC % G + ESC % @ */ selectDefaultCharset(): void; + /** ESC ( C + ESC ) C + ESC * C + ESC + C + ESC - C + ESC . C + ESC / C */ selectCharset(collectAndFlag: string): void; + /** ESC D */ index(): void; + /** ESC H */ tabSet(): void; + /** ESC M */ reverseIndex(): void; + /** ESC c */ reset(): void; + /** ESC n + ESC o + ESC | + ESC } + ESC ~ */ setgLevel(level: number): void; } export interface ILinkMatcher { @@ -190,7 +212,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce writeBuffer: string[]; cursorHidden: boolean; cursorState: number; - defAttr: number; options: ITerminalOptions; buffer: IBuffer; buffers: IBufferSet; @@ -226,7 +247,7 @@ export interface ILinkifierAccessor { } export interface IMouseHelper { - getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number]; + getCoords(event: { pageX: number, pageY: number }, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number]; getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number }; } @@ -238,6 +259,7 @@ export interface ICharMeasure { // TODO: The options that are not in the public API should be reviewed export interface ITerminalOptions extends IPublicTerminalOptions { + [key: string]: any; cancelEvents?: boolean; convertEol?: boolean; debug?: boolean; @@ -261,6 +283,7 @@ export interface IBuffer { savedX: number; isCursorInViewport: boolean; translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string; + getWrappedRangeForLine(y: number): { first: number, last: number }; nextStop(x?: number): number; prevStop(x?: number): number; } @@ -355,3 +378,134 @@ export interface IBrowser { export interface ISoundManager { playBellSound(): void; } + +/** + * Internal states of EscapeSequenceParser. + */ +export const enum ParserState { + GROUND = 0, + ESCAPE = 1, + ESCAPE_INTERMEDIATE = 2, + CSI_ENTRY = 3, + CSI_PARAM = 4, + CSI_INTERMEDIATE = 5, + CSI_IGNORE = 6, + SOS_PM_APC_STRING = 7, + OSC_STRING = 8, + DCS_ENTRY = 9, + DCS_PARAM = 10, + DCS_IGNORE = 11, + DCS_INTERMEDIATE = 12, + DCS_PASSTHROUGH = 13 +} + +/** +* Internal actions of EscapeSequenceParser. +*/ +export const enum ParserAction { + IGNORE = 0, + ERROR = 1, + PRINT = 2, + EXECUTE = 3, + OSC_START = 4, + OSC_PUT = 5, + OSC_END = 6, + CSI_DISPATCH = 7, + PARAM = 8, + COLLECT = 9, + ESC_DISPATCH = 10, + CLEAR = 11, + DCS_HOOK = 12, + DCS_PUT = 13, + DCS_UNHOOK = 14 +} + +/** + * Internal state of EscapeSequenceParser. + * Used as argument of the error handler to allow + * introspection at runtime on parse errors. + * Return it with altered values to recover from + * faulty states (not yet supported). + * Set `abort` to `true` to abort the current parsing. + */ +export interface IParsingState { + // position in parse string + position: number; + // actual character code + code: number; + // current parser state + currentState: ParserState; + // print buffer start index (-1 for not set) + print: number; + // dcs buffer start index (-1 for not set) + dcs: number; + // osc string buffer + osc: string; + // collect buffer with intermediate characters + collect: string; + // params buffer + params: number[]; + // should abort (default: false) + abort: boolean; +} + +/** +* DCS handler signature for EscapeSequenceParser. +* EscapeSequenceParser handles DCS commands via separate +* subparsers that get hook/unhooked and can handle +* arbitrary amount of print data. +* On entering a DSC sequence `hook` is called by +* `EscapeSequenceParser`. Use it to initialize or reset +* states needed to handle the current DCS sequence. +* EscapeSequenceParser will call `put` several times if the +* parsed string got splitted, therefore you might have to collect +* `data` until `unhook` is called. `unhook` marks the end +* of the current DCS sequence. +*/ +export interface IDcsHandler { + hook(collect: string, params: number[], flag: number): void; + put(data: string, start: number, end: number): void; + unhook(): void; +} + +/** +* EscapeSequenceParser interface. +*/ +export interface IEscapeSequenceParser { + /** + * Reset the parser to its initial state (handlers are kept). + */ + reset(): void; + + /** + * Parse string `data`. + * @param data The data to parse. + */ + parse(data: string): void; + + setPrintHandler(callback: (data: string, start: number, end: number) => void): void; + clearPrintHandler(): void; + + setExecuteHandler(flag: string, callback: () => void): void; + clearExecuteHandler(flag: string): void; + setExecuteHandlerFallback(callback: (code: number) => void): void; + + setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void; + clearCsiHandler(flag: string): void; + setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void; + + setEscHandler(collectAndFlag: string, callback: () => void): void; + clearEscHandler(collectAndFlag: string): void; + setEscHandlerFallback(callback: (collect: string, flag: number) => void): void; + + setOscHandler(ident: number, callback: (data: string) => void): void; + clearOscHandler(ident: number): void; + setOscHandlerFallback(callback: (identifier: number, data: string) => void): void; + + setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void; + clearDcsHandler(collectAndFlag: string): void; + setDcsHandlerFallback(handler: IDcsHandler): void; + + setErrorHandler(callback: (state: IParsingState) => IParsingState): void; + clearErrorHandler(): void; +} diff --git a/src/Viewport.ts b/src/Viewport.ts index c951d6b5..f690c348 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -180,7 +180,7 @@ export class Viewport implements IViewport { * @param ev The touch event. */ public onTouchMove(ev: TouchEvent): void { - let deltaY = this._lastTouchY - ev.touches[0].pageY; + const deltaY = this._lastTouchY - ev.touches[0].pageY; this._lastTouchY = ev.touches[0].pageY; if (deltaY === 0) { return; diff --git a/src/addons/attach/attach.ts b/src/addons/attach/attach.ts index 4a9668de..e6d92b56 100644 --- a/src/addons/attach/attach.ts +++ b/src/addons/attach/attach.ts @@ -5,8 +5,6 @@ * Implements the attach method, that attaches the terminal to a WebSocket stream. */ -/// - import { Terminal } from 'xterm'; import { IAttachAddonTerminal } from './Interfaces'; @@ -38,10 +36,11 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean } }; - let myTextDecoder; + // TODO: This should be typed but there seem to be issues importing the type + let myTextDecoder: any; addonTerminal.__getMessage = function(ev: MessageEvent): void { - let str; + let str: string; if (typeof ev.data === 'object') { if (!myTextDecoder) { @@ -51,7 +50,7 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean str = myTextDecoder.decode(ev.data); displayData(str); } else { - let fileReader = new FileReader(); + const fileReader = new FileReader(); fileReader.addEventListener('load', () => { str = myTextDecoder.decode(this.result); diff --git a/src/addons/attach/tsconfig.json b/src/addons/attach/tsconfig.json index a4e8e78b..12e75810 100644 --- a/src/addons/attach/tsconfig.json +++ b/src/addons/attach/tsconfig.json @@ -6,6 +6,11 @@ "outDir": "../../../lib/addons/attach/", "sourceMap": true, "removeComments": true, - "declaration": true - } + "declaration": true, + "preserveWatchOutput": true + }, + "include": [ + "**/*.ts", + "../../../typings/xterm.d.ts" + ] } diff --git a/src/addons/fit/README.md b/src/addons/fit/README.md new file mode 100644 index 00000000..68f01589 --- /dev/null +++ b/src/addons/fit/README.md @@ -0,0 +1,3 @@ +## fit addon + +The fit addon adjusts the dimensions of the terminal to match best fit its parent element container. `fit` will only work when the element is visible. diff --git a/src/addons/fit/fit.ts b/src/addons/fit/fit.ts index f6f593dc..68e4cfd5 100644 --- a/src/addons/fit/fit.ts +++ b/src/addons/fit/fit.ts @@ -13,8 +13,6 @@ * row and truncate its width with the current number of columns). */ -/// - import { Terminal } from 'xterm'; export interface IGeometry { diff --git a/src/addons/fit/tsconfig.json b/src/addons/fit/tsconfig.json index 78b3c410..f806af18 100644 --- a/src/addons/fit/tsconfig.json +++ b/src/addons/fit/tsconfig.json @@ -6,6 +6,11 @@ "outDir": "../../../lib/addons/fit/", "sourceMap": true, "removeComments": true, - "declaration": true - } + "declaration": true, + "preserveWatchOutput": true + }, + "include": [ + "**/*.ts", + "../../../typings/xterm.d.ts" + ] } diff --git a/src/addons/fullscreen/fullscreen.ts b/src/addons/fullscreen/fullscreen.ts index 47440667..297a7f5b 100644 --- a/src/addons/fullscreen/fullscreen.ts +++ b/src/addons/fullscreen/fullscreen.ts @@ -3,8 +3,6 @@ * @license MIT */ -/// - import { Terminal } from 'xterm'; /** diff --git a/src/addons/fullscreen/tsconfig.json b/src/addons/fullscreen/tsconfig.json index 35ecb7ce..2fbb295f 100644 --- a/src/addons/fullscreen/tsconfig.json +++ b/src/addons/fullscreen/tsconfig.json @@ -6,6 +6,11 @@ "outDir": "../../../lib/addons/fullscreen/", "sourceMap": true, "removeComments": true, - "declaration": true - } + "declaration": true, + "preserveWatchOutput": true + }, + "include": [ + "**/*.ts", + "../../../typings/xterm.d.ts" + ] } diff --git a/src/addons/search/search.ts b/src/addons/search/search.ts index 5c0b2b96..a1dd766f 100644 --- a/src/addons/search/search.ts +++ b/src/addons/search/search.ts @@ -3,8 +3,6 @@ * @license MIT */ -/// - import { SearchHelper } from './SearchHelper'; import { Terminal } from 'xterm'; import { ISearchAddonTerminal } from './Interfaces'; diff --git a/src/addons/search/tsconfig.json b/src/addons/search/tsconfig.json index d1c3d7e0..47876a00 100644 --- a/src/addons/search/tsconfig.json +++ b/src/addons/search/tsconfig.json @@ -6,6 +6,11 @@ "outDir": "../../../lib/addons/search/", "sourceMap": true, "removeComments": true, - "declaration": true - } + "declaration": true, + "preserveWatchOutput": true + }, + "include": [ + "**/*.ts", + "../../../typings/xterm.d.ts" + ] } diff --git a/src/addons/terminado/terminado.ts b/src/addons/terminado/terminado.ts index 4acf9dd8..136eea8e 100644 --- a/src/addons/terminado/terminado.ts +++ b/src/addons/terminado/terminado.ts @@ -6,8 +6,6 @@ * WebSocket stream. */ -/// - import { Terminal } from 'xterm'; import { ITerminadoAddonTerminal } from './Interfaces'; diff --git a/src/addons/terminado/tsconfig.json b/src/addons/terminado/tsconfig.json index 84e3ac32..b664a5e0 100644 --- a/src/addons/terminado/tsconfig.json +++ b/src/addons/terminado/tsconfig.json @@ -6,6 +6,11 @@ "outDir": "../../../lib/addons/terminado/", "sourceMap": true, "removeComments": true, - "declaration": true - } + "declaration": true, + "preserveWatchOutput": true + }, + "include": [ + "**/*.ts", + "../../../typings/xterm.d.ts" + ] } diff --git a/src/addons/webLinks/tsconfig.json b/src/addons/webLinks/tsconfig.json index 7549370b..7dd999f6 100644 --- a/src/addons/webLinks/tsconfig.json +++ b/src/addons/webLinks/tsconfig.json @@ -6,6 +6,11 @@ "outDir": "../../../lib/addons/webLinks/", "sourceMap": true, "removeComments": true, - "declaration": true - } + "declaration": true, + "preserveWatchOutput": true + }, + "include": [ + "**/*.ts", + "../../../typings/xterm.d.ts" + ] } diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts index b3c76703..c84ee1a5 100644 --- a/src/addons/webLinks/webLinks.test.ts +++ b/src/addons/webLinks/webLinks.test.ts @@ -34,8 +34,8 @@ describe('webLinks addon', () => { const row = ' http://foo.com/a~b#c~d?e~f '; - let match = row.match(term.regex); - let uri = match[term.options.matchIndex]; + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; assert.equal(uri, 'http://foo.com/a~b#c~d?e~f'); }); diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index 7ff87c60..a007bbd6 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -3,8 +3,6 @@ * @license MIT */ -/// - import { Terminal, ILinkMatcherOptions } from 'xterm'; const protocolClause = '(https?:\\/\\/)'; diff --git a/src/addons/winptyCompat/tsconfig.json b/src/addons/winptyCompat/tsconfig.json index a51f7332..ecad2563 100644 --- a/src/addons/winptyCompat/tsconfig.json +++ b/src/addons/winptyCompat/tsconfig.json @@ -6,6 +6,11 @@ "outDir": "../../../lib/addons/winptyCompat/", "sourceMap": true, "removeComments": true, - "declaration": true - } + "declaration": true, + "preserveWatchOutput": true + }, + "include": [ + "**/*.ts", + "../../../typings/xterm.d.ts" + ] } diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index 61524e12..9cde62e0 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -3,8 +3,6 @@ * @license MIT */ -/// - import { Terminal } from 'xterm'; import { IWinptyCompatAddonTerminal } from './Interfaces'; diff --git a/src/addons/zmodem/tsconfig.json b/src/addons/zmodem/tsconfig.json index abae1238..d4577108 100644 --- a/src/addons/zmodem/tsconfig.json +++ b/src/addons/zmodem/tsconfig.json @@ -6,6 +6,11 @@ "outDir": "../../../lib/addons/zmodem/", "sourceMap": true, "removeComments": true, - "declaration": true - } + "declaration": true, + "preserveWatchOutput": true + }, + "include": [ + "**/*.ts", + "../../../typings/xterm.d.ts" + ] } diff --git a/src/addons/zmodem/zmodem.ts b/src/addons/zmodem/zmodem.ts index 4f2f4a91..23204f0f 100644 --- a/src/addons/zmodem/zmodem.ts +++ b/src/addons/zmodem/zmodem.ts @@ -3,8 +3,6 @@ * @license MIT */ -/// - import { Terminal } from 'xterm'; /** @@ -48,13 +46,13 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void { let zsentry; - function _shouldWrite(): boolean { + function shouldWrite(): boolean { return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession; } zsentry = new zmodem.Sentry({ to_terminal: (octets: ArrayLike) => { - if (_shouldWrite()) { + if (shouldWrite()) { term.write( String.fromCharCode.apply(String, octets) ); @@ -72,7 +70,7 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void { // may be specific to xterm.js’s demo, ultimately we // should reject anything that isn’t binary. if (typeof evt.data === 'string') { - if (_shouldWrite()) { + if (shouldWrite()) { term.write(evt.data); } } diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts index 7f2e4f05..8f1cf2df 100644 --- a/src/handlers/AltClickHandler.ts +++ b/src/handlers/AltClickHandler.ts @@ -28,7 +28,7 @@ export class AltClickHandler { this._startCol = this._terminal.buffer.x; this._startRow = this._terminal.buffer.y; - let coordinates = this._terminal.mouseHelper.getCoords( + const coordinates = this._terminal.mouseHelper.getCoords( this._mouseEvent, this._terminal.element, this._terminal.charMeasure, @@ -89,10 +89,10 @@ export class AltClickHandler { * ignoring wrapped rows */ private _moveToRequestedRow(): string { - let startRow = this._startRow - this._wrappedRowsForRow(this._startRow); - let endRow = this._endRow - this._wrappedRowsForRow(this._endRow); + const startRow = this._startRow - this._wrappedRowsForRow(this._startRow); + const endRow = this._endRow - this._wrappedRowsForRow(this._endRow); - let rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount(); + const rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount(); return repeat(rowsToMove, this._sequence(this._verticalDirection())); } @@ -108,8 +108,8 @@ export class AltClickHandler { startRow = this._startRow; } - let endRow = this._endRow; - let direction = this._horizontalDirection(); + const endRow = this._endRow; + const direction = this._horizontalDirection(); return repeat(this._bufferLine( this._startCol, startRow, this._endCol, endRow, @@ -118,7 +118,7 @@ export class AltClickHandler { } private _moveHorizontallyOnly(): string { - let direction = this._horizontalDirection(); + const direction = this._horizontalDirection(); return repeat(Math.abs(this._startCol - this._endCol), this._sequence(direction)); } @@ -132,11 +132,11 @@ export class AltClickHandler { */ private _wrappedRowsCount(): number { let wrappedRows = 0; - let startRow = this._startRow - this._wrappedRowsForRow(this._startRow); - let endRow = this._endRow - this._wrappedRowsForRow(this._endRow); + const startRow = this._startRow - this._wrappedRowsForRow(this._startRow); + const endRow = this._endRow - this._wrappedRowsForRow(this._endRow); for (let i = 0; i < Math.abs(startRow - endRow); i++) { - let direction = this._verticalDirection() === Direction.UP ? -1 : 1; + const direction = this._verticalDirection() === Direction.UP ? -1 : 1; if ((this._lines.get(startRow + (direction * i))).isWrapped) { wrappedRows++; diff --git a/src/handlers/Clipboard.ts b/src/handlers/Clipboard.ts index 23925007..bbb8302c 100644 --- a/src/handlers/Clipboard.ts +++ b/src/handlers/Clipboard.ts @@ -58,7 +58,7 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void { let text: string; - let dispatchPaste = function(text: string): void { + const dispatchPaste = function(text: string): void { text = prepareTextForTerminal(text); text = bracketTextForPaste(text, term.bracketedPasteMode); term.handler(text); diff --git a/src/renderer/ColorManager.test.ts b/src/renderer/ColorManager.test.ts index 724bc758..26fb2895 100644 --- a/src/renderer/ColorManager.test.ts +++ b/src/renderer/ColorManager.test.ts @@ -33,10 +33,10 @@ describe('ColorManager', () => { describe('constructor', () => { it('should fill all colors with values', () => { - for (let key of Object.keys(cm.colors)) { + for (const key of Object.keys(cm.colors)) { if (key !== 'ansi') { // A #rrggbb or rgba(...) - assert.ok(cm.colors[key].css.length >= 7); + assert.ok((cm.colors)[key].css.length >= 7); } } assert.equal(cm.colors.ansi.length, 256); diff --git a/src/renderer/ColorManager.ts b/src/renderer/ColorManager.ts index 9ab60750..d169f266 100644 --- a/src/renderer/ColorManager.ts +++ b/src/renderer/ColorManager.ts @@ -42,7 +42,7 @@ export const DEFAULT_ANSI_COLORS = (() => { // Fill in the remaining 240 ANSI colors. // Generate colors (16-231) - let v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; + const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; for (let i = 0; i < 216; i++) { const r = v[(i / 36) % 6 | 0]; const g = v[(i / 6) % 6 | 0]; @@ -75,7 +75,7 @@ function fromHex(css: string): IColor { } function toPaddedHex(c: number): string { - let s = c.toString(16); + const s = c.toString(16); return s.length < 2 ? '0' + s : s; } diff --git a/src/renderer/Types.ts b/src/renderer/Types.ts index edecf8b0..bafc9ad4 100644 --- a/src/renderer/Types.ts +++ b/src/renderer/Types.ts @@ -20,6 +20,10 @@ export const enum FLAGS { ITALIC = 64 } +/** + * Note that IRenderer implementations should emit the refresh event after + * rendering rows to the screen. + */ export interface IRenderer extends IEventEmitter { dimensions: IRenderDimensions; colorManager: IColorManager; diff --git a/src/renderer/atlas/CharAtlasUtils.ts b/src/renderer/atlas/CharAtlasUtils.ts index 39284d32..59ac07df 100644 --- a/src/renderer/atlas/CharAtlasUtils.ts +++ b/src/renderer/atlas/CharAtlasUtils.ts @@ -9,7 +9,7 @@ import { ICharAtlasConfig } from '../../shared/atlas/Types'; export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig { // null out some fields that don't matter - const clonedColors = { + const clonedColors = { foreground: colors.foreground, background: colors.background, cursor: null, diff --git a/src/renderer/atlas/LRUMap.ts b/src/renderer/atlas/LRUMap.ts index 4a03f2aa..eccfbfea 100644 --- a/src/renderer/atlas/LRUMap.ts +++ b/src/renderer/atlas/LRUMap.ts @@ -11,7 +11,7 @@ interface ILinkedListNode { } export default class LRUMap { - private _map = {}; + private _map: { [key: string]: ILinkedListNode } = {}; private _head: ILinkedListNode = null; private _tail: ILinkedListNode = null; private _nodePool: ILinkedListNode[] = []; diff --git a/src/renderer/atlas/StaticCharAtlas.ts b/src/renderer/atlas/StaticCharAtlas.ts index b19074b8..b6de82fc 100644 --- a/src/renderer/atlas/StaticCharAtlas.ts +++ b/src/renderer/atlas/StaticCharAtlas.ts @@ -19,10 +19,14 @@ export default class StaticCharAtlas extends BaseCharAtlas { const canvas = this._document.createElement('canvas'); canvas.width = width; canvas.height = height; + + // This is useful for debugging + // document.body.appendChild(canvas); + return canvas; } - public _doWarmUp(): void { + protected _doWarmUp(): void { const result = generateStaticCharAtlasTexture(window, this._canvasFactory, this._config); if (result instanceof HTMLCanvasElement) { this._texture = result; @@ -65,6 +69,9 @@ export default class StaticCharAtlas extends BaseCharAtlas { if (!this._isCached(glyph, colorIndex)) { return false; } + + ctx.save(); + // ImageBitmap's draw about twice as fast as from a canvas const charAtlasCellWidth = this._config.scaledCharWidth + CHAR_ATLAS_CELL_SPACING; const charAtlasCellHeight = this._config.scaledCharHeight + CHAR_ATLAS_CELL_SPACING; @@ -86,6 +93,8 @@ export default class StaticCharAtlas extends BaseCharAtlas { this._config.scaledCharHeight ); + ctx.restore(); + return true; } } diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts new file mode 100644 index 00000000..5b1813b5 --- /dev/null +++ b/src/renderer/dom/DomRenderer.ts @@ -0,0 +1,315 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IRenderer, IRenderDimensions, IColorSet } from '../Types'; +import { ITerminal } from '../../Types'; +import { ITheme } from 'xterm'; +import { EventEmitter } from '../../EventEmitter'; +import { ColorManager } from '../ColorManager'; +import { RenderDebouncer } from '../../utils/RenderDebouncer'; +import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, DomRendererRowFactory } from './DomRendererRowFactory'; + +const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; +const ROW_CONTAINER_CLASS = 'xterm-rows'; +const FG_CLASS_PREFIX = 'xterm-fg-'; +const BG_CLASS_PREFIX = 'xterm-bg-'; +const FOCUS_CLASS = 'xterm-focus'; +const SELECTION_CLASS = 'xterm-selection'; + +let nextTerminalId = 1; + +// TODO: Pull into an addon when TS composite projects allow easier sharing of code (not just +// interfaces) between core and addons + +/** + * A fallback renderer for when canvas is slow. This is not meant to be + * particularly fast or feature complete, more just stable and usable for when + * canvas is not an option. + */ +export class DomRenderer extends EventEmitter implements IRenderer { + private _renderDebouncer: RenderDebouncer; + private _rowFactory: DomRendererRowFactory; + private _terminalClass: number = nextTerminalId++; + + private _themeStyleElement: HTMLStyleElement; + private _dimensionsStyleElement: HTMLStyleElement; + private _rowContainer: HTMLElement; + private _rowElements: HTMLElement[] = []; + private _selectionContainer: HTMLElement; + + public dimensions: IRenderDimensions; + public colorManager: ColorManager; + + constructor(private _terminal: ITerminal, theme: ITheme | undefined) { + super(); + const allowTransparency = this._terminal.options.allowTransparency; + this.colorManager = new ColorManager(document, allowTransparency); + this.setTheme(theme); + + this._rowContainer = document.createElement('div'); + this._rowContainer.classList.add(ROW_CONTAINER_CLASS); + this._rowContainer.style.lineHeight = 'normal'; + this._rowContainer.setAttribute('aria-hidden', 'true'); + this._refreshRowElements(this._terminal.rows, this._terminal.cols); + this._selectionContainer = document.createElement('div'); + this._selectionContainer.classList.add(SELECTION_CLASS); + this._selectionContainer.setAttribute('aria-hidden', 'true'); + + this.dimensions = { + scaledCharWidth: null, + scaledCharHeight: null, + scaledCellWidth: null, + scaledCellHeight: null, + scaledCharLeft: null, + scaledCharTop: null, + scaledCanvasWidth: null, + scaledCanvasHeight: null, + canvasWidth: null, + canvasHeight: null, + actualCellWidth: null, + actualCellHeight: null + }; + this._updateDimensions(); + + this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this)); + this._rowFactory = new DomRendererRowFactory(document); + + this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); + this._terminal.screenElement.appendChild(this._rowContainer); + this._terminal.screenElement.appendChild(this._selectionContainer); + } + + private _updateDimensions(): void { + this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio; + this.dimensions.scaledCharHeight = this._terminal.charMeasure.height * window.devicePixelRatio; + this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth; + this.dimensions.scaledCellHeight = this.dimensions.scaledCharHeight; + this.dimensions.scaledCharLeft = 0; + this.dimensions.scaledCharTop = 0; + this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._terminal.cols; + this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._terminal.rows; + this.dimensions.canvasWidth = this._terminal.charMeasure.width * this._terminal.cols; + this.dimensions.canvasHeight = this._terminal.charMeasure.height * this._terminal.rows; + this.dimensions.actualCellWidth = this._terminal.charMeasure.width; + this.dimensions.actualCellHeight = this._terminal.charMeasure.height; + + this._rowElements.forEach(element => { + element.style.width = `${this.dimensions.canvasWidth}px`; + element.style.height = `${this._terminal.charMeasure.height}px`; + }); + + if (!this._dimensionsStyleElement) { + this._dimensionsStyleElement = document.createElement('style'); + this._terminal.screenElement.appendChild(this._dimensionsStyleElement); + } + + const styles = + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` + + ` display: inline-block;` + + ` height: 100%;` + + ` vertical-align: top;` + + ` width: ${this._terminal.charMeasure.width}px` + + `}`; + + this._dimensionsStyleElement.innerHTML = styles; + + this._selectionContainer.style.height = (this._terminal)._viewportElement.style.height; + this._rowContainer.style.width = `${this.dimensions.canvasWidth}px`; + this._rowContainer.style.height = `${this.dimensions.canvasHeight}px`; + } + + public setTheme(theme: ITheme | undefined): IColorSet { + if (theme) { + this.colorManager.setTheme(theme); + } + + if (!this._themeStyleElement) { + this._themeStyleElement = document.createElement('style'); + this._terminal.screenElement.appendChild(this._themeStyleElement); + } + + // Base CSS + let styles = + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` + + ` color: ${this.colorManager.colors.foreground.css};` + + ` background-color: ${this.colorManager.colors.background.css};` + + ` font-family: ${this._terminal.getOption('fontFamily')};` + + ` font-size: ${this._terminal.getOption('fontSize')}px;` + + `}`; + // Text styles + styles += + `${this._terminalSelector} span:not(.${BOLD_CLASS}) {` + + ` font-weight: ${this._terminal.options.fontWeight};` + + `}` + + `${this._terminalSelector} span.${BOLD_CLASS} {` + + ` font-weight: ${this._terminal.options.fontWeightBold};` + + `}` + + `${this._terminalSelector} span.${ITALIC_CLASS} {` + + ` font-style: italic;` + + `}`; + // Cursor + styles += + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS} {` + + ` background-color: ${this.colorManager.colors.cursor.css};` + + ` color: ${this.colorManager.colors.cursorAccent.css};` + + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` + + ` outline: 1px solid #fff;` + + ` outline-offset: -1px;` + + `}`; + // Selection + styles += + `${this._terminalSelector} .${SELECTION_CLASS} {` + + ` position: absolute;` + + ` top: 0;` + + ` left: 0;` + + ` z-index: 1;` + + ` pointer-events: none;` + + `}` + + `${this._terminalSelector} .${SELECTION_CLASS} div {` + + ` position: absolute;` + + ` background-color: ${this.colorManager.colors.selection.css};` + + `}`; + // Colors + this.colorManager.colors.ansi.forEach((c, i) => { + styles += + `${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` + + `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`; + }); + + this._themeStyleElement.innerHTML = styles; + return this.colorManager.colors; + } + + public onWindowResize(devicePixelRatio: number): void { + this._updateDimensions(); + } + + private _refreshRowElements(cols: number, rows: number): void { + // Add missing elements + for (let i = this._rowElements.length; i <= rows; i++) { + const row = document.createElement('div'); + this._rowContainer.appendChild(row); + this._rowElements.push(row); + } + // Remove excess elements + while (this._rowElements.length > rows) { + this._rowContainer.removeChild(this._rowElements.pop()); + } + } + + public onResize(cols: number, rows: number): void { + this._refreshRowElements(cols, rows); + this._updateDimensions(); + } + + public onCharSizeChanged(): void { + this._updateDimensions(); + } + + public onBlur(): void { + this._rowContainer.classList.remove(FOCUS_CLASS); + } + + public onFocus(): void { + this._rowContainer.classList.add(FOCUS_CLASS); + } + + public onSelectionChanged(start: [number, number], end: [number, number]): void { + // Remove all selections + while (this._selectionContainer.children.length) { + this._selectionContainer.removeChild(this._selectionContainer.children[0]); + } + + // Selection does not exist + if (!start || !end) { + return; + } + + // Translate from buffer position to viewport position + const viewportStartRow = start[1] - this._terminal.buffer.ydisp; + const viewportEndRow = end[1] - this._terminal.buffer.ydisp; + const viewportCappedStartRow = Math.max(viewportStartRow, 0); + const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1); + + // No need to draw the selection + if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) { + return; + } + + // Create the selections + const documentFragment = document.createDocumentFragment(); + // Draw first row + const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; + const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols; + documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol)); + // Draw middle rows + const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1; + documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount)); + // Draw final row + if (viewportCappedStartRow !== viewportCappedEndRow) { + // Only draw viewportEndRow if it's not the same as viewporttartRow + const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols; + documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol)); + } + this._selectionContainer.appendChild(documentFragment); + } + + /** + * Creates a selection element at the specified position. + * @param row The row of the selection. + * @param colStart The start column. + * @param colEnd The end columns. + */ + private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement { + const element = document.createElement('div'); + element.style.height = `${rowCount * this._terminal.charMeasure.height}px`; + element.style.top = `${row * this._terminal.charMeasure.height}px`; + element.style.left = `${colStart * this._terminal.charMeasure.width}px`; + element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`; + return element; + } + + public onCursorMove(): void { + // No-op, the cursor is drawn when rows are drawn + } + + public onOptionsChanged(): void { + // Force a refresh + this._updateDimensions(); + this.setTheme(undefined); + this._terminal.refresh(0, this._terminal.rows - 1); + } + + public clear(): void { + this._rowElements.forEach(e => e.innerHTML = ''); + } + + public refreshRows(start: number, end: number): void { + this._renderDebouncer.refresh(start, end); + } + + private _renderRows(start: number, end: number): void { + const terminal = this._terminal; + + const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y; + const cursorX = this._terminal.buffer.x; + + for (let y = start; y <= end; y++) { + const rowElement = this._rowElements[y]; + rowElement.innerHTML = ''; + + const row = y + terminal.buffer.ydisp; + const lineData = terminal.buffer.lines.get(row); + rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorX, terminal.charMeasure.width)); + } + + this._terminal.emit('refresh', {start, end}); + } + + private get _terminalSelector(): string { + return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`; + } +} diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts new file mode 100644 index 00000000..f28830ce --- /dev/null +++ b/src/renderer/dom/DomRendererRowFactory.test.ts @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import jsdom = require('jsdom'); +import { assert } from 'chai'; +import { DomRendererRowFactory } from './DomRendererRowFactory'; +import { LineData } from '../../Types'; +import { DEFAULT_ATTR } from '../../Buffer'; +import { FLAGS } from '../Types'; + +describe('DomRendererRowFactory', () => { + let dom: jsdom.JSDOM; + let rowFactory: DomRendererRowFactory; + let lineData: LineData; + + beforeEach(() => { + dom = new jsdom.JSDOM(''); + rowFactory = new DomRendererRowFactory(dom.window.document); + lineData = createEmptyLineData(2); + }); + + describe('createRow', () => { + it('should create an element for every character in the row', () => { + const fragment = rowFactory.createRow(lineData, false, 0, 5); + assert.equal(getFragmentHtml(fragment), + ' ' + + ' ' + ); + }); + + it('should set correct attributes for double width characters', () => { + lineData[0] = [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]; + // There should be no element for the following "empty" cell + lineData[1] = [DEFAULT_ATTR, '', 0, undefined]; + const fragment = rowFactory.createRow(lineData, false, 0, 5); + assert.equal(getFragmentHtml(fragment), + '' + ); + }); + + it('should add class for cursor', () => { + const fragment = rowFactory.createRow(lineData, true, 0, 5); + assert.equal(getFragmentHtml(fragment), + ' ' + + ' ' + ); + }); + + describe('attributes', () => { + it('should add class for bold', () => { + lineData[0] = [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)]; + const fragment = rowFactory.createRow(lineData, false, 0, 5); + assert.equal(getFragmentHtml(fragment), + 'a' + + ' ' + ); + }); + + it('should add class for italic', () => { + lineData[0] = [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)]; + const fragment = rowFactory.createRow(lineData, false, 0, 5); + assert.equal(getFragmentHtml(fragment), + 'a' + + ' ' + ); + }); + + it('should add classes for 256 foreground colors', () => { + const defaultAttrNoFgColor = (0 << 9) | (256 << 0); + for (let i = 0; i < 256; i++) { + lineData[0] = [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)]; + const fragment = rowFactory.createRow(lineData, false, 0, 5); + assert.equal(getFragmentHtml(fragment), + `a` + + ' ' + ); + } + }); + + it('should add classes for 256 background colors', () => { + const defaultAttrNoBgColor = (257 << 9) | (0 << 0); + for (let i = 0; i < 256; i++) { + lineData[0] = [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)]; + const fragment = rowFactory.createRow(lineData, false, 0, 5); + assert.equal(getFragmentHtml(fragment), + `a` + + ' ' + ); + } + }); + + it('should correctly invert colors', () => { + lineData[0] = [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]; + const fragment = rowFactory.createRow(lineData, false, 0, 5); + assert.equal(getFragmentHtml(fragment), + 'a' + + ' ' + ); + }); + + it('should correctly invert default fg color', () => { + lineData[0] = [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)]; + const fragment = rowFactory.createRow(lineData, false, 0, 5); + assert.equal(getFragmentHtml(fragment), + 'a' + + ' ' + ); + }); + + it('should correctly invert default bg color', () => { + lineData[0] = [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]; + const fragment = rowFactory.createRow(lineData, false, 0, 5); + assert.equal(getFragmentHtml(fragment), + 'a' + + ' ' + ); + }); + + it('should turn bold fg text bright', () => { + for (let i = 0; i < 8; i++) { + lineData[0] = [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)]; + const fragment = rowFactory.createRow(lineData, false, 0, 5); + assert.equal(getFragmentHtml(fragment), + `a` + + ' ' + ); + } + }); + }); + }); + + function getFragmentHtml(fragment: DocumentFragment): string { + const element = dom.window.document.createElement('div'); + element.appendChild(fragment); + return element.innerHTML; + } + + function createEmptyLineData(cols: number): LineData { + const lineData: LineData = []; + for (let i = 0; i < cols; i++) { + lineData.push([DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */]); + } + return lineData; + } +}); diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts new file mode 100644 index 00000000..d72629a3 --- /dev/null +++ b/src/renderer/dom/DomRendererRowFactory.ts @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { LineData } from '../../Types'; +import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer'; +import { FLAGS } from '../Types'; + +export const BOLD_CLASS = 'xterm-bold'; +export const ITALIC_CLASS = 'xterm-italic'; +export const CURSOR_CLASS = 'xterm-cursor'; + +export class DomRendererRowFactory { + constructor( + private _document: Document + ) { + } + + public createRow(lineData: LineData, isCursorRow: boolean, cursorX: number, cellWidth: number): DocumentFragment { + const fragment = this._document.createDocumentFragment(); + for (let x = 0; x < lineData.length; x++) { + const charData = lineData[x]; + const char: string = charData[CHAR_DATA_CHAR_INDEX]; + const attr: number = charData[CHAR_DATA_ATTR_INDEX]; + const width: number = charData[CHAR_DATA_WIDTH_INDEX]; + + // The character to the left is a wide character, drawing is owned by the char at x-1 + if (width === 0) { + continue; + } + + const charElement = this._document.createElement('span'); + if (width > 1) { + charElement.style.width = `${cellWidth * width}px`; + } + + const flags = attr >> 18; + let bg = attr & 0x1ff; + let fg = (attr >> 9) & 0x1ff; + + if (isCursorRow && x === cursorX) { + charElement.classList.add(CURSOR_CLASS); + } + + // If inverse flag is on, the foreground should become the background. + if (flags & FLAGS.INVERSE) { + const temp = bg; + bg = fg; + fg = temp; + if (fg === 256) { + fg = 0; + } + if (bg === 257) { + bg = 15; + } + } + + if (flags & FLAGS.BOLD) { + // Convert the FG color to the bold variant + if (fg < 8) { + fg += 8; + } + charElement.classList.add(BOLD_CLASS); + } + + if (flags & FLAGS.ITALIC) { + charElement.classList.add(ITALIC_CLASS); + } + + charElement.textContent = char; + if (fg !== 257) { + charElement.classList.add(`xterm-fg-${fg}`); + } + if (bg !== 256) { + charElement.classList.add(`xterm-bg-${bg}`); + } + fragment.appendChild(charElement); + } + return fragment; + } +} diff --git a/src/shared/atlas/CharAtlasGenerator.ts b/src/shared/atlas/CharAtlasGenerator.ts index 276da78d..f78a6d41 100644 --- a/src/shared/atlas/CharAtlasGenerator.ts +++ b/src/shared/atlas/CharAtlasGenerator.ts @@ -6,7 +6,7 @@ import { FontWeight } from 'xterm'; import { CHAR_ATLAS_CELL_SPACING, ICharAtlasConfig } from './Types'; import { IColor } from '../Types'; -import { isFirefox } from '../utils/Browser'; +import { isFirefox, isSafari } from '../utils/Browser'; declare const Promise: any; @@ -99,14 +99,14 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: ( // if support is lacking as drawImage works there too. Firefox is also // included here as ImageBitmap appears both buggy and has horrible // performance (tested on v55). - if (!('createImageBitmap' in context) || isFirefox) { + if (!('createImageBitmap' in context) || isFirefox || isSafari) { // Don't attempt to clear background colors if createImageBitmap is not supported if (canvas instanceof HTMLCanvasElement) { // Just return the HTMLCanvas if it's a HTMLCanvasElement return canvas; } // Transfer to an ImageBitmap is this is an OffscreenCanvas - return new Promise(r => r(canvas.transferToImageBitmap())); + return new Promise((r: (bitmap: ImageBitmap) => void) => r(canvas.transferToImageBitmap())); } const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height); diff --git a/src/shared/utils/Browser.ts b/src/shared/utils/Browser.ts index be71d875..a023031c 100644 --- a/src/shared/utils/Browser.ts +++ b/src/shared/utils/Browser.ts @@ -8,6 +8,7 @@ const userAgent = (isNode) ? 'node' : navigator.userAgent; const platform = (isNode) ? 'node' : navigator.platform; export const isFirefox = !!~userAgent.indexOf('Firefox'); +export const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent); export const isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident'); // Find the users platform. We use this to interpret the meta key diff --git a/src/utils/CircularList.ts b/src/utils/CircularList.ts index 1f00ea09..ca83d6e1 100644 --- a/src/utils/CircularList.ts +++ b/src/utils/CircularList.ts @@ -36,7 +36,7 @@ export class CircularList extends EventEmitter implements ICircularList { // Reconstruct array, starting at index 0. Only transfer values from the // indexes 0 to length. - let newArray = new Array(newMaxLength); + const newArray = new Array(newMaxLength); for (let i = 0; i < Math.min(newMaxLength, this.length); i++) { newArray[i] = this._array[this._getCyclicIndex(i)]; } diff --git a/src/utils/Clone.test.ts b/src/utils/Clone.test.ts index f5708a9e..b24452c8 100644 --- a/src/utils/Clone.test.ts +++ b/src/utils/Clone.test.ts @@ -39,7 +39,7 @@ describe('clone', () => { }); it('should clone null values', () => { - const test = { + const test: any = { a: null }; diff --git a/src/utils/TestUtils.test.ts b/src/utils/TestUtils.test.ts index 51aafb50..f4bad717 100644 --- a/src/utils/TestUtils.test.ts +++ b/src/utils/TestUtils.test.ts @@ -107,7 +107,6 @@ export class MockTerminal implements ITerminal { children: HTMLElement[]; cursorHidden: boolean; cursorState: number; - defAttr: number; scrollback: number; buffers: IBufferSet; buffer: IBuffer; @@ -182,9 +181,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { insertMode: boolean; wraparoundMode: boolean; bracketedPasteMode: boolean; - defAttr: number; curAttr: number; - prefix: string; savedCols: number; x10Mouse: boolean; vt200Mouse: boolean; @@ -268,7 +265,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal { throw new Error('Method not implemented.'); } setOption(key: string, value: any): void { - this.options[key] = value; + (this.options)[key] = value; } on(type: string, listener: XtermListener): void { throw new Error('Method not implemented.'); @@ -303,6 +300,9 @@ export class MockBuffer implements IBuffer { translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string { return Buffer.prototype.translateBufferLineToString.apply(this, arguments); } + getWrappedRangeForLine(y: number): { first: number; last: number; } { + throw new Error('Method not implemented.'); + } nextStop(x?: number): number { throw new Error('Method not implemented.'); } diff --git a/tsconfig.json b/tsconfig.json index e56930e6..e39ea642 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,10 +12,13 @@ "outDir": "lib", "sourceMap": true, "removeComments": true, - "noUnusedLocals": true + "preserveWatchOutput": true, + "noUnusedLocals": true, + "noImplicitAny": true }, "include": [ - "src/**/*" + "src/**/*", + "typings/xterm.d.ts" ], "exclude": [ "src/addons/**/*" diff --git a/tslint.json b/tslint.json index ac1b9c95..2ae39796 100644 --- a/tslint.json +++ b/tslint.json @@ -31,6 +31,7 @@ "parameter" ], "eofline": true, + "new-parens": true, "no-duplicate-imports": true, "no-eval": true, "no-internal-module": true, @@ -93,13 +94,23 @@ "naming-convention": [ true, - {"type": "property", "modifiers": ["public", "static", "const"], "format": "UPPER_CASE"} + {"type": "default", "format": "camelCase", "leadingUnderscore": "forbid"}, + {"type": "type", "format": "PascalCase"}, + {"type": "class", "format": "PascalCase"}, + {"type": "property", "modifiers": ["const"], "format": "UPPER_CASE"}, + {"type": "member", "modifiers": ["protected"], "format": "camelCase", "leadingUnderscore": "allow"}, + // TODO: Change allow to require when there aren't many PRs out + // {"type": "member", "modifiers": ["protected"], "format": "camelCase", "leadingUnderscore": "require"}, + {"type": "member", "modifiers": ["private"], "format": "camelCase", "leadingUnderscore": "require"}, + {"type": "variable", "modifiers": ["const"], "format": ["camelCase", "UPPER_CASE"]}, + {"type": "interface", "prefix": "I"} ], "no-else-after-return": { "options": "allow-else-if" }, "prefer-const-enum": [ true - ] + ], + "prefer-const": true } } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index a8724922..bda984b7 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -13,6 +13,11 @@ declare module 'xterm' { */ export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'; + /** + * A string representing a renderer type. + */ + export type RendererType = 'dom' | 'canvas'; + /** * An object containing start up options for the terminal. */ @@ -119,6 +124,23 @@ declare module 'xterm' { */ macOptionIsMeta?: boolean; + /** + * (EXPERIMENTAL) The type of renderer to use, this allows using the + * fallback DOM renderer when canvas is too slow for the environment. The + * following features do not work when the DOM renderer is used: + * + * - Links + * - Line height + * - Letter spacing + * - Cursor blink + * - Cursor style + * + * This option is marked as experiemental because it will eventually be + * moved to an addon. You can only set this option in the constructor (not + * setOption). + */ + rendererType?: RendererType; + /** * Whether to select the word under the cursor on right click, this is * standard behavior in a lot of macOS applications. @@ -530,7 +552,7 @@ declare module 'xterm' { * Retrieves an option's value from the terminal. * @param key The option key. */ - getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold'| 'termName'): string; + getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold'| 'rendererType' | 'termName'): string; /** * Retrieves an option's value from the terminal. * @param key The option key.