diff --git a/README.md b/README.md
index 6790fea9..ae003783 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# [](https://xtermjs.org)
-[](https://travis-ci.org/xtermjs/xterm.js) [](https://coveralls.io/github/xtermjs/xterm.js?branch=master) [](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://www.jsdelivr.com/package/npm/xterm)
+[](https://travis-ci.org/xtermjs/xterm.js) [](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1) [](https://coveralls.io/github/xtermjs/xterm.js?branch=master) [](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](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.