diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 4d4e605b..e5736562 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -8,12 +8,19 @@ RUN apt-get update \
# Verify git and process tools are installed
RUN apt-get install -y git procps
-# Install yarn
+# Install yarn, puppeteer deps
RUN apt-get install -y curl apt-transport-https lsb-release \
&& curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \
&& echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
&& apt-get update \
- && apt-get -y install --no-install-recommends yarn
+ && apt-get -y install --no-install-recommends \
+ yarn fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \
+ # https://github.com/Googlechrome/puppeteer/issues/290#issuecomment-322921352
+ gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
+ libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
+ libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
+ libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
+ ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
# Clean up
RUN apt-get autoremove -y \
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 00000000..dccd0c5d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Question
+ url: https://stackoverflow.com/questions/tagged/xtermjs
+ about: Please ask and answer questions here.
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
deleted file mode 100644
index 1fe5c763..00000000
--- a/.github/ISSUE_TEMPLATE/question.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: Question
-about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs
----
-
-🛑 The issue tracker is not for questions 🛑
-
-If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs.
diff --git a/.gitignore b/.gitignore
index 64273dda..8aea04b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ node_modules/
lib/
out/
out-test/
+.nyc_output/
Makefile.gyp
*.Makefile
*.target.gyp.mk
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3c8a2dc2..20e4915c 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,4 @@
{
- "typescript.preferences.quoteStyle": "single"
+ "typescript.preferences.quoteStyle": "single",
+ "typescript.preferences.importModuleSpecifier": "non-relative"
}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e8102a2e..d5efe9d2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -52,3 +52,7 @@ By contributing code to xterm.js you:
holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct
agreement with you.)
+
+### Test coverage
+
+One area that always needs attention is improving out unit test coverage, you can view the code coverage report on [Azure Pipelines](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3) by clicking the Code Coverage tab.
diff --git a/README.md b/README.md
index 2853696f..70ff7891 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ Xterm.js is a front-end component written in TypeScript that lets applications b
- **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
- **Rich unicode support**: Supports CJK, emojis and IMEs.
- **Self-contained**: Requires zero dependencies to work.
-- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option.
+- **Accessible**: Screen reader and minimum contrast ratio support can be turned on
- **And much more**: Links, theming, addons, well documented API, etc.
## What xterm.js is not
@@ -32,8 +32,8 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
-
-
+
+
@@ -110,7 +110,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2.
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE.
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor.
-- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js.
+- [**Next Tech**](https://next.tech "Next Tech"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js.
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
@@ -154,6 +154,8 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**PHP App Server**](https://github.com/cubiclesoft/php-app-server/): Create lightweight, installable almost-native applications for desktop OSes. ExecTerminal (nicely wraps the xterm.js Terminal), TerminalManager, and RunProcessSDK are self-contained, reusable ES5+ compliant Javascript components.
- [**NgTerminal**](https://github.com/qwefgh90/ng-terminal): NgTerminal is a web terminal that leverages xterm.js on Angular 7+. You can easily add it into your application by adding `` into your component.
- [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet.
+- [**Ten Hands**](https://github.com/saisandeepvaddi/ten-hands): One place to run your command-line tasks.
+- [**WebAssembly.sh**](https://webassembly.sh): A WebAssembly WASI browser terminal
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
diff --git a/addons/xterm-addon-attach/README.md b/addons/xterm-addon-attach/README.md
new file mode 100644
index 00000000..67ebf17b
--- /dev/null
+++ b/addons/xterm-addon-attach/README.md
@@ -0,0 +1,22 @@
+## xterm-addon-attach
+
+An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables attaching to a web socket. This addon requires xterm.js v4+.
+
+### Install
+
+```bash
+npm install --save xterm-addon-attach
+```
+
+### Usage
+
+```ts
+import { Terminal } from 'xterm';
+import { AttachAddon } from 'xterm-addon-attach';
+
+const terminal = new Terminal();
+const attachAddon = new AttachAddon(webSocket);
+terminal.loadAddon(attachAddon);
+```
+
+See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts) for more advanced usage.
diff --git a/addons/xterm-addon-attach/package.json b/addons/xterm-addon-attach/package.json
index 84740e1e..5718f356 100644
--- a/addons/xterm-addon-attach/package.json
+++ b/addons/xterm-addon-attach/package.json
@@ -1,12 +1,13 @@
{
"name": "xterm-addon-attach",
- "version": "0.1.0",
+ "version": "0.3.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
},
"main": "lib/xterm-addon-attach.js",
"types": "typings/xterm-addon-attach.d.ts",
+ "repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"scripts": {
"build": "../../node_modules/.bin/tsc -p src",
@@ -15,6 +16,6 @@
"prepublishOnly": "npm run package"
},
"peerDependencies": {
- "xterm": "^3.14.0"
+ "xterm": "^4.0.0"
}
}
diff --git a/addons/xterm-addon-attach/src/AttachAddon.api.ts b/addons/xterm-addon-attach/src/AttachAddon.api.ts
index 945824a2..264b32ee 100644
--- a/addons/xterm-addon-attach/src/AttachAddon.api.ts
+++ b/addons/xterm-addon-attach/src/AttachAddon.api.ts
@@ -20,8 +20,7 @@ describe('AttachAddon', () => {
this.timeout(20000);
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
- slowMo: 80,
- args: [`--window-size=${width},${height}`]
+ args: [`--window-size=${width},${height}`, `--no-sandbox`]
});
page = (await browser.pages())[0];
await page.setViewport({ width, height });
@@ -43,7 +42,7 @@ describe('AttachAddon', () => {
const server = new WebSocket.Server({ port });
server.on('connection', socket => socket.send('foo'));
await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`);
- assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo');
+ await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foo');
server.close();
});
@@ -54,8 +53,8 @@ describe('AttachAddon', () => {
const server = new WebSocket.Server({ port });
const data = new Uint8Array([102, 111, 111]);
server.on('connection', socket => socket.send(data));
- await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}'), { inputUtf8: true }))`);
- assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo');
+ await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`);
+ await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foo');
server.close();
});
});
@@ -69,3 +68,12 @@ async function openTerminal(options: ITerminalOptions = {}): Promise {
await page.waitForSelector('.xterm-text-layer');
}
}
+
+async function pollFor(page: puppeteer.Page, fn: string, val: any): Promise {
+ const result = await page.evaluate(fn);
+ if (result !== val) {
+ return new Promise(r => {
+ setTimeout(() => r(pollFor(page, fn, val)), 10);
+ });
+ }
+}
diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts
index 9dc45ecb..279d1b2e 100644
--- a/addons/xterm-addon-attach/src/AttachAddon.ts
+++ b/addons/xterm-addon-attach/src/AttachAddon.ts
@@ -9,13 +9,11 @@ import { Terminal, IDisposable, ITerminalAddon } from 'xterm';
interface IAttachOptions {
bidirectional?: boolean;
- inputUtf8?: boolean;
}
export class AttachAddon implements ITerminalAddon {
private _socket: WebSocket;
private _bidirectional: boolean;
- private _utf8: boolean;
private _disposables: IDisposable[] = [];
constructor(socket: WebSocket, options?: IAttachOptions) {
@@ -23,20 +21,19 @@ export class AttachAddon implements ITerminalAddon {
// always set binary type to arraybuffer, we do not handle blobs
this._socket.binaryType = 'arraybuffer';
this._bidirectional = (options && options.bidirectional === false) ? false : true;
- this._utf8 = !!(options && options.inputUtf8);
}
public activate(terminal: Terminal): void {
- if (this._utf8) {
- this._disposables.push(addSocketListener(this._socket, 'message',
- (ev: MessageEvent | Event | CloseEvent) => terminal.writeUtf8(new Uint8Array((ev as any).data as ArrayBuffer))));
- } else {
- this._disposables.push(addSocketListener(this._socket, 'message',
- (ev: MessageEvent | Event | CloseEvent) => terminal.write((ev as any).data as string)));
- }
+ this._disposables.push(
+ addSocketListener(this._socket, 'message', ev => {
+ const data: ArrayBuffer | string = ev.data;
+ terminal.write(typeof data === 'string' ? data : new Uint8Array(data));
+ })
+ );
if (this._bidirectional) {
this._disposables.push(terminal.onData(data => this._sendData(data)));
+ this._disposables.push(terminal.onBinary(data => this._sendBinary(data)));
}
this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose()));
@@ -55,9 +52,20 @@ export class AttachAddon implements ITerminalAddon {
}
this._socket.send(data);
}
+
+ private _sendBinary(data: string): void {
+ if (this._socket.readyState !== 1) {
+ return;
+ }
+ const buffer = new Uint8Array(data.length);
+ for (let i = 0; i < data.length; ++i) {
+ buffer[i] = data.charCodeAt(i) & 255;
+ }
+ this._socket.send(buffer);
+ }
}
-function addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: MessageEvent | Event | CloseEvent) => any): IDisposable {
+function addSocketListener(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable {
socket.addEventListener(type, handler);
return {
dispose: () => {
diff --git a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts
index 8c667140..1aa21357 100644
--- a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts
+++ b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts
@@ -11,14 +11,6 @@ declare module 'xterm-addon-attach' {
* Whether input should be written to the backend. Defaults to `true`.
*/
bidirectional?: boolean;
-
- /**
- * Whether to use UTF8 binary transport for incoming messages. Defaults to `false`.
- * Note: This must be in line with the server side of the websocket.
- * Always send string messages from the backend if this options is false,
- * otherwise always binary UTF8 data.
- */
- inputUtf8?: boolean;
}
export class AttachAddon implements ITerminalAddon {
diff --git a/addons/xterm-addon-fit/README.md b/addons/xterm-addon-fit/README.md
new file mode 100644
index 00000000..321b2cf7
--- /dev/null
+++ b/addons/xterm-addon-fit/README.md
@@ -0,0 +1,24 @@
+## xterm-addon-fit
+
+An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables fitting the terminal's dimensions to a containing element. This addon requires xterm.js v4+.
+
+### Install
+
+```bash
+npm install --save xterm-addon-fit
+```
+
+### Usage
+
+```ts
+import { Terminal } from 'xterm';
+import { FitAddon } from 'xterm-addon-fit';
+
+const terminal = new Terminal();
+const fitAddon = new FitAddon();
+terminal.loadAddon(fitAddon);
+terminal.open(containerElement);
+fitAddon.fit();
+```
+
+See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-fit/typings/xterm-addon-fit.d.ts) for more advanced usage.
diff --git a/addons/xterm-addon-fit/package.json b/addons/xterm-addon-fit/package.json
index 6e553700..4bceb258 100644
--- a/addons/xterm-addon-fit/package.json
+++ b/addons/xterm-addon-fit/package.json
@@ -1,12 +1,13 @@
{
"name": "xterm-addon-fit",
- "version": "0.1.0",
+ "version": "0.3.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
},
"main": "lib/xterm-addon-fit.js",
"types": "typings/xterm-addon-fit.d.ts",
+ "repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"scripts": {
"build": "../../node_modules/.bin/tsc -p src",
@@ -15,6 +16,6 @@
"prepublishOnly": "npm run package"
},
"peerDependencies": {
- "xterm": "^3.14.0"
+ "xterm": "^4.0.0"
}
}
diff --git a/addons/xterm-addon-fit/src/FitAddon.api.ts b/addons/xterm-addon-fit/src/FitAddon.api.ts
new file mode 100644
index 00000000..aa73a9b6
--- /dev/null
+++ b/addons/xterm-addon-fit/src/FitAddon.api.ts
@@ -0,0 +1,118 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import * as puppeteer from 'puppeteer';
+import { assert } from 'chai';
+import { ITerminalOptions } from 'xterm';
+
+const APP = 'http://127.0.0.1:3000/test';
+
+let browser: puppeteer.Browser;
+let page: puppeteer.Page;
+const width = 1024;
+const height = 768;
+
+describe('FitAddon', () => {
+ before(async function(): Promise {
+ this.timeout(20000);
+ browser = await puppeteer.launch({
+ headless: process.argv.indexOf('--headless') !== -1,
+ args: [`--window-size=${width},${height}`, `--no-sandbox`]
+ });
+ page = (await browser.pages())[0];
+ await page.setViewport({ width, height });
+ await page.goto(APP);
+ await openTerminal();
+ });
+
+ after(async () => {
+ await browser.close();
+ });
+
+ it('no terminal', async function(): Promise {
+ await page.evaluate(`window.fit = new FitAddon();`);
+ assert.equal(await page.evaluate(`window.fit.proposeDimensions()`), undefined);
+ });
+
+ describe('proposeDimensions', () => {
+ afterEach(async () => {
+ return unloadFit();
+ });
+
+ it('default', async function(): Promise {
+ await loadFit();
+ assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), {
+ cols: 87,
+ rows: 26
+ });
+ });
+
+ it('width', async function(): Promise {
+ await loadFit(1008);
+ assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), {
+ cols: 110,
+ rows: 26
+ });
+ });
+
+ it('small', async function(): Promise {
+ await loadFit(1, 1);
+ assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), {
+ cols: 2,
+ rows: 1
+ });
+ });
+ });
+
+ describe('fit', () => {
+ afterEach(async () => {
+ return unloadFit();
+ });
+
+ it('default', async function(): Promise {
+ await loadFit();
+ await page.evaluate(`window.fit.fit()`);
+ assert.equal(await page.evaluate(`window.term.cols`), 87);
+ assert.equal(await page.evaluate(`window.term.rows`), 26);
+ });
+
+ it('width', async function(): Promise {
+ await loadFit(1008);
+ await page.evaluate(`window.fit.fit()`);
+ assert.equal(await page.evaluate(`window.term.cols`), 110);
+ assert.equal(await page.evaluate(`window.term.rows`), 26);
+ });
+
+ it('small', async function(): Promise {
+ await loadFit(1, 1);
+ await page.evaluate(`window.fit.fit()`);
+ assert.equal(await page.evaluate(`window.term.cols`), 2);
+ assert.equal(await page.evaluate(`window.term.rows`), 1);
+ });
+ });
+});
+
+async function loadFit(width: number = 800, height: number = 450): Promise {
+ await page.evaluate(`
+ window.fit = new FitAddon();
+ window.term.loadAddon(window.fit);
+ document.querySelector('#terminal-container').style.width='${width}px';
+ document.querySelector('#terminal-container').style.height='${height}px';
+ `);
+}
+
+async function unloadFit(): Promise {
+ await page.evaluate(`window.fit.dispose();`);
+}
+
+async function openTerminal(options: ITerminalOptions = {}): Promise {
+ await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
+ await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
+ if (options.rendererType === 'dom') {
+ await page.waitForSelector('.xterm-rows');
+ } else {
+ await page.waitForSelector('.xterm-text-layer');
+ }
+}
diff --git a/addons/xterm-addon-fit/src/FitAddon.ts b/addons/xterm-addon-fit/src/FitAddon.ts
index f23bd161..ca7e24b1 100644
--- a/addons/xterm-addon-fit/src/FitAddon.ts
+++ b/addons/xterm-addon-fit/src/FitAddon.ts
@@ -17,6 +17,9 @@ interface ITerminalDimensions {
cols: number;
}
+const MINIMUM_COLS = 2;
+const MINIMUM_ROWS = 1;
+
export class FitAddon implements ITerminalAddon {
private _terminal: Terminal | undefined;
@@ -49,7 +52,7 @@ export class FitAddon implements ITerminalAddon {
return undefined;
}
- if (!this._terminal.element.parentElement) {
+ if (!this._terminal.element || !this._terminal.element.parentElement) {
return undefined;
}
@@ -71,8 +74,8 @@ export class FitAddon implements ITerminalAddon {
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - core.viewport.scrollBarWidth;
const geometry = {
- cols: Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth),
- rows: Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight)
+ cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)),
+ rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight))
};
return geometry;
}
diff --git a/addons/xterm-addon-search/README.md b/addons/xterm-addon-search/README.md
new file mode 100644
index 00000000..91bcc34b
--- /dev/null
+++ b/addons/xterm-addon-search/README.md
@@ -0,0 +1,23 @@
+## xterm-addon-search
+
+An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables searching the buffer. This addon requires xterm.js v4+.
+
+### Install
+
+```bash
+npm install --save xterm-addon-search
+```
+
+### Usage
+
+```ts
+import { Terminal } from 'xterm';
+import { SearchAddon } from 'xterm-addon-search';
+
+const terminal = new Terminal();
+const searchAddon = new SearchAddon();
+terminal.loadAddon(searchAddon);
+searchAddon.findNext('foo');
+```
+
+See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-search/typings/xterm-addon-search.d.ts) for more advanced usage.
diff --git a/addons/xterm-addon-search/fixtures/issue-2444 b/addons/xterm-addon-search/fixtures/issue-2444
new file mode 100644
index 00000000..cb7f7878
--- /dev/null
+++ b/addons/xterm-addon-search/fixtures/issue-2444
@@ -0,0 +1,70 @@
+/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
+make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release'
+make[3]: Nothing to be done for `libsrc/wrap_curl/CMakeFiles/wrap_curl.dir/build'.
+make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
+[ 0%] /usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
+[ 0%] [ 0%] /usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
+Built target config_utils
+[ 0%] make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release'
+/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
+Built target omega
+make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release'
+cd /home/yan/ssd/searchtrunk/tmp/release && /usr/bin/cmake -E cmake_depends "Unix Makefiles" /home/yan/ssd/searchtrunk /home/yan/ssd/searchtrunk/projects/protogen/cpp /home/yan/ssd/searchtrunk/tmp/release /home/yan/ssd/searchtrunk/tmp/release/projects/protogen/cpp /home/yan/ssd/searchtrunk/tmp/release/projects/protogen/cpp/CMakeFiles/protogen.dir/DependInfo.cmake --color=
+/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
+Built target morpheus
+make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
+make -f libsrc/base/CMakeFiles/base.dir/build.make libsrc/base/CMakeFiles/base.dir/build
+make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release'
+make[3]: Nothing to be done for `libsrc/coroutine/CMakeFiles/coroutine.dir/build'.
+Built target wrap_curl
+make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
+/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
+/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
+[ 0%] [ 0%] [ 0%] [ 0%] Building CXX object libsrc/image_crop/CMakeFiles/image_crop.dir/src/crop_context.cpp.o
+cd /home/yan/ssd/searchtrunk/tmp/release/libsrc/image_crop && /opt/rh/devtoolset-7/root/usr/bin/c++ -DGOGO_USE_LIBEVENT2=1 -DGOGO_USE_OPENSSL_SNI=1 -DHAVE_BOOST -DHAVE_GLIBC -DHAVE_MALLOC_TRIM -DHAVE_PREAD -DHAVE_READAHEAD -D_FILE_OFFSET_BITS=64 -D_STAT_LEMM -pipe -Wall -Wextra -Werror=multichar -Wno-deprecated -Wno-unused-parameter -pthread -fPIC -Woverloaded-virtual -Wnon-virtual-dtor -Werror -std=gnu++11 -Wno-unknown-pragmas -DBOOST_FILESYSTEM_VERSION=3 -DBOOST_NO_CXX11_HDR_CODECVT -DMAGICKCORE_QUANTUM_DEPTH=8 -DMAGICKCORE_HDRI_ENABLE=0 -Wno-unused-local-typedefs -D_GLIBCXX_USE_CXX11_ABI=0 -std=gnu++14 -DCXXHASH128_EXIST -static-libstdc++ -Wno-implicit-fallthrough -Wno-stringop-overflow -O3 -DNDEBUG -DBOOST_UBLAS_NDEBUG -g0 -march=core2 -mssse3 -msse4.1 -I/home/yan/ssd/searchtrunk/tmp/release/__inc -I/home/yan/ssd/searchtrunk/libsrc/image_crop/src -I/home/yan/ssd/searchtrunk/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/boost_1_69_0/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/icu-49.1.2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/protobuf-3.4.0/include -I/home/yan/ssd/searchtrunk/tmp/release -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/opencv-3.0.0m2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/dlib-19.7p1/include -Wno-array-bounds -o CMakeFiles/image_crop.dir/src/crop_context.cpp.o -c /home/yan/ssd/searchtrunk/libsrc/image_crop/src/crop_context.cpp
+Building CXX object libsrc/image_crop/CMakeFiles/image_crop.dir/src/detector.cpp.o
+Building CXX object libsrc/image_crop/CMakeFiles/image_crop.dir/src/image_crop.cpp.o
+cd /home/yan/ssd/searchtrunk/tmp/release/libsrc/image_crop && /opt/rh/devtoolset-7/root/usr/bin/c++ -DGOGO_USE_LIBEVENT2=1 -DGOGO_USE_OPENSSL_SNI=1 -DHAVE_BOOST -DHAVE_GLIBC -DHAVE_MALLOC_TRIM -DHAVE_PREAD -DHAVE_READAHEAD -D_FILE_OFFSET_BITS=64 -D_STAT_LEMM -pipe -Wall -Wextra -Werror=multichar -Wno-deprecated -Wno-unused-parameter -pthread -fPIC -Woverloaded-virtual -Wnon-virtual-dtor -Werror -std=gnu++11 -Wno-unknown-pragmas -DBOOST_FILESYSTEM_VERSION=3 -DBOOST_NO_CXX11_HDR_CODECVT -DMAGICKCORE_QUANTUM_DEPTH=8 -DMAGICKCORE_HDRI_ENABLE=0 -Wno-unused-local-typedefs -D_GLIBCXX_USE_CXX11_ABI=0 -std=gnu++14 -DCXXHASH128_EXIST -static-libstdc++ -Wno-implicit-fallthrough -Wno-stringop-overflow -O3 -DNDEBUG -DBOOST_UBLAS_NDEBUG -g0 -march=core2 -mssse3 -msse4.1 -I/home/yan/ssd/searchtrunk/tmp/release/__inc -I/home/yan/ssd/searchtrunk/libsrc/image_crop/src -I/home/yan/ssd/searchtrunk/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/boost_1_69_0/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/icu-49.1.2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/protobuf-3.4.0/include -I/home/yan/ssd/searchtrunk/tmp/release -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/opencv-3.0.0m2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/dlib-19.7p1/include -Wno-array-bounds -o CMakeFiles/image_crop.dir/src/detector.cpp.o -c /home/yan/ssd/searchtrunk/libsrc/image_crop/src/detector.cpp
+Built target coroutine
+cd /home/yan/ssd/searchtrunk/tmp/release/libsrc/image_crop && /opt/rh/devtoolset-7/root/usr/bin/c++ -DGOGO_USE_LIBEVENT2=1 -DGOGO_USE_OPENSSL_SNI=1 -DHAVE_BOOST -DHAVE_GLIBC -DHAVE_MALLOC_TRIM -DHAVE_PREAD -DHAVE_READAHEAD -D_FILE_OFFSET_BITS=64 -D_STAT_LEMM -pipe -Wall -Wextra -Werror=multichar -Wno-deprecated -Wno-unused-parameter -pthread -fPIC -Woverloaded-virtual -Wnon-virtual-dtor -Werror -std=gnu++11 -Wno-unknown-pragmas -DBOOST_FILESYSTEM_VERSION=3 -DBOOST_NO_CXX11_HDR_CODECVT -DMAGICKCORE_QUANTUM_DEPTH=8 -DMAGICKCORE_HDRI_ENABLE=0 -Wno-unused-local-typedefs -D_GLIBCXX_USE_CXX11_ABI=0 -std=gnu++14 -DCXXHASH128_EXIST -static-libstdc++ -Wno-implicit-fallthrough -Wno-stringop-overflow -O3 -DNDEBUG -DBOOST_UBLAS_NDEBUG -g0 -march=core2 -mssse3 -msse4.1 -I/home/yan/ssd/searchtrunk/tmp/release/__inc -I/home/yan/ssd/searchtrunk/libsrc/image_crop/src -I/home/yan/ssd/searchtrunk/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/boost_1_69_0/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/icu-49.1.2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/protobuf-3.4.0/include -I/home/yan/ssd/searchtrunk/tmp/release -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/opencv-3.0.0m2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/dlib-19.7p1/include -Wno-array-bounds -o CMakeFiles/image_crop.dir/src/image_crop.cpp.o -c /home/yan/ssd/searchtrunk/libsrc/image_crop/src/image_crop.cpp
+make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release'
+make[3]: Nothing to be done for `libsrc/base/CMakeFiles/base.dir/build'.
+make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
+/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
+make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
+make -f projects/protogen/cpp/CMakeFiles/protogen.dir/build.make projects/protogen/cpp/CMakeFiles/protogen.dir/build
+[ 0%] Built target base
+In file included from /home/yan/ssd/searchtrunk/include/image_crop/detector.hpp:3:0,
+ from /home/yan/ssd/searchtrunk/libsrc/image_crop/src/detector.cpp:2:
+/home/yan/ssd/searchtrunk/include/image_crop/crop_context.hpp:6:10: fatal error: opencv2/dnn.hpp: No such file or directory
+ #include
+ ^~~~~~~~~~~~~~~~~
+compilation terminated.
+make[3]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/src/detector.cpp.o] Error 1
+make[3]: *** Waiting for unfinished jobs....
+In file included from /home/yan/ssd/searchtrunk/include/image_crop/image_crop.hpp:3:0,
+ from /home/yan/ssd/searchtrunk/libsrc/image_crop/src/image_crop.cpp:1:
+/home/yan/ssd/searchtrunk/include/image_crop/crop_context.hpp:6:10: fatal error: opencv2/dnn.hpp: No such file or directory
+ #include
+ ^~~~~~~~~~~~~~~~~
+compilation terminated.
+make[3]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/src/image_crop.cpp.o] Error 1
+In file included from /home/yan/ssd/searchtrunk/libsrc/image_crop/src/crop_context.cpp:1:0:
+/home/yan/ssd/searchtrunk/include/image_crop/crop_context.hpp:6:10: fatal error: opencv2/dnn.hpp: No such file or directory
+ #include
+ ^~~~~~~~~~~~~~~~~
+compilation terminated.
+make[3]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/src/crop_context.cpp.o] Error 1
+make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
+make[2]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/all] Error 2
+make[2]: *** Waiting for unfinished jobs....
+make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release'
+make[3]: Nothing to be done for `projects/protogen/cpp/CMakeFiles/protogen.dir/build'.
+make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
+/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles 63 64 65 66
+[ 25%] Built target protogen
+make[2]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
+make[1]: *** [all] Error 2
+make[1]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release/projects/projectX'
+make: *** [install] Error 2
+yan@yPC:~/ssd/searchtrunk/projects/projectX$
diff --git a/addons/xterm-addon-search/package.json b/addons/xterm-addon-search/package.json
index 5a607fe9..c426ee3b 100644
--- a/addons/xterm-addon-search/package.json
+++ b/addons/xterm-addon-search/package.json
@@ -1,12 +1,13 @@
{
"name": "xterm-addon-search",
- "version": "0.1.0",
+ "version": "0.3.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
},
"main": "lib/xterm-addon-search.js",
"types": "typings/xterm-addon-search.d.ts",
+ "repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"scripts": {
"build": "../../node_modules/.bin/tsc -p src",
@@ -15,6 +16,6 @@
"prepublishOnly": "npm run package"
},
"peerDependencies": {
- "xterm": "^3.14.0"
+ "xterm": "^4.0.0"
}
}
diff --git a/addons/xterm-addon-search/src/SearchAddon.api.ts b/addons/xterm-addon-search/src/SearchAddon.api.ts
index f970fcc8..72a69b5f 100644
--- a/addons/xterm-addon-search/src/SearchAddon.api.ts
+++ b/addons/xterm-addon-search/src/SearchAddon.api.ts
@@ -6,6 +6,8 @@
import * as puppeteer from 'puppeteer';
import { assert } from 'chai';
import { ITerminalOptions } from 'xterm';
+import { readFile } from 'fs';
+import { resolve } from 'path';
const APP = 'http://127.0.0.1:3000/test';
@@ -15,13 +17,12 @@ const width = 800;
const height = 600;
describe('Search Tests', function (): void {
- this.timeout(200000);
+ this.timeout(20000);
before(async function (): Promise {
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
- slowMo: 80,
- args: [`--window-size=${width},${height}`]
+ args: [`--window-size=${width},${height}`, `--no-sandbox`]
});
page = (await browser.pages())[0];
await page.setViewport({ width, height });
@@ -91,13 +92,101 @@ describe('Search Tests', function (): void {
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
});
- it ('Simple Regex', async () => {
+ it('Simple Regex', async () => {
await writeSync('abc123defABCD');
await page.evaluate(`window.search.findNext('[a-z]+', {regex: true})`);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
await page.evaluate(`window.search.findNext('[A-Z]+', {regex: true, caseSensitive: true})`);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'ABCD');
});
+
+ it('Search for single result twice should not unselect it', async () => {
+ await writeSync('abc def');
+ assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
+ assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
+ assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
+ assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
+ });
+
+ describe('Regression tests', () => {
+ describe('#2444 wrapped line content not being found', () => {
+ let fixture: string;
+ before(async () => {
+ const rawFixture = await new Promise(r => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => r(data)));
+ fixture = rawFixture.toString()
+ .replace(/\n/g, '\\n\\r')
+ .replace(/'/g, '\\\'');
+ });
+ it('should find all occurrences using findNext', async () => {
+ await writeSync(fixture);
+ assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
+ let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
+ assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 });
+ assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 });
+ assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 });
+ assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 });
+ assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 });
+ assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 });
+ assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 });
+ assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
+ // Wrap around to first result
+ assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
+ });
+ it('should find all occurrences using findPrevious', async () => {
+ await writeSync(fixture);
+ assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
+ let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
+ assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 });
+ assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 });
+ assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 });
+ assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 });
+ assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 });
+ assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 });
+ assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 });
+ assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
+ // Wrap around to first result
+ assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
+ selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
+ assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
+ });
+ });
+ });
});
async function openTerminal(options: ITerminalOptions = {}): Promise {
@@ -111,12 +200,7 @@ async function openTerminal(options: ITerminalOptions = {}): Promise {
}
async function writeSync(data: string): Promise {
- await page.evaluate(`window.term.write('${data}');`);
- while (true) {
- if (await page.evaluate(`window.term._core.writeBuffer.length === 0`)) {
- break;
- }
- }
+ return page.evaluate(`new Promise(resolve => window.term.write('${data}', resolve))`);
}
function makeData(length: number): string {
diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts
index e9f8a9b4..db2968e6 100644
--- a/addons/xterm-addon-search/src/SearchAddon.ts
+++ b/addons/xterm-addon-search/src/SearchAddon.ts
@@ -3,7 +3,7 @@
* @license MIT
*/
-import { Terminal, IDisposable, ITerminalAddon } from 'xterm';
+import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm';
export interface ISearchOptions {
regex?: boolean;
@@ -12,6 +12,11 @@ export interface ISearchOptions {
incremental?: boolean;
}
+export interface ISearchPosition {
+ startCol: number;
+ startRow: number;
+}
+
export interface ISearchResult {
term: string;
col: number;
@@ -59,29 +64,35 @@ export class SearchAddon implements ITerminalAddon {
let startCol = 0;
let startRow = 0;
-
+ let currentSelection: ISelectionPosition | undefined;
if (this._terminal.hasSelection()) {
const incremental = searchOptions ? searchOptions.incremental : false;
// Start from the selection end if there is a selection
// For incremental search, use existing row
- const currentSelection = this._terminal.getSelectionPosition()!;
+ currentSelection = this._terminal.getSelectionPosition()!;
startRow = incremental ? currentSelection.startRow : currentSelection.endRow;
startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn;
}
this._initLinesCache();
+ const searchPosition: ISearchPosition = {
+ startRow,
+ startCol
+ };
+
// Search startRow
- let result = this._findInLine(term, startRow, startCol, searchOptions);
+ let result = this._findInLine(term, searchPosition, searchOptions);
// Search from startRow + 1 to end
if (!result) {
for (let y = startRow + 1; y < this._terminal.buffer.baseY + this._terminal.rows; y++) {
-
+ searchPosition.startRow = y;
+ searchPosition.startCol = 0;
// If the current line is wrapped line, increase index of column to ignore the previous scan
// Otherwise, reset beginning column index to zero with set new unwrapped line index
- result = this._findInLine(term, y, 0, searchOptions);
+ result = this._findInLine(term, searchPosition, searchOptions);
if (result) {
break;
}
@@ -90,13 +101,18 @@ export class SearchAddon implements ITerminalAddon {
// If we hit the bottom and didn't search from the very top wrap back up
if (!result && startRow !== 0) {
for (let y = 0; y < startRow; y++) {
- result = this._findInLine(term, y, 0, searchOptions);
+ searchPosition.startRow = y;
+ searchPosition.startCol = 0;
+ result = this._findInLine(term, searchPosition, searchOptions);
if (result) {
break;
}
}
}
+ // If there is only one result, return true.
+ if (!result && currentSelection) return true;
+
// Set selection and scroll if a result was found
return this._selectResult(result);
}
@@ -121,31 +137,37 @@ export class SearchAddon implements ITerminalAddon {
const isReverseSearch = true;
let startRow = this._terminal.buffer.baseY + this._terminal.rows;
let startCol = this._terminal.cols;
- let result: ISearchResult | undefined = undefined;
+ let result: ISearchResult | undefined;
const incremental = searchOptions ? searchOptions.incremental : false;
+ let currentSelection: ISelectionPosition | undefined;
if (this._terminal.hasSelection()) {
- const currentSelection = this._terminal.getSelectionPosition()!;
+ currentSelection = this._terminal.getSelectionPosition()!;
// Start from selection start if there is a selection
startRow = currentSelection.startRow;
startCol = currentSelection.startColumn;
}
this._initLinesCache();
+ const searchPosition: ISearchPosition = {
+ startRow,
+ startCol
+ };
if (incremental) {
- result = this._findInLine(term, startRow, startCol, searchOptions, false);
+ result = this._findInLine(term, searchPosition, searchOptions, false);
if (!(result && result.row === startRow && result.col === startCol)) {
- result = this._findInLine(term, startRow, startCol, searchOptions, true);
+ result = this._findInLine(term, searchPosition, searchOptions, true);
}
} else {
- result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch);
+ result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
}
// Search from startRow - 1 to top
if (!result) {
- startCol = this._terminal.cols;
+ searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols);
for (let y = startRow - 1; y >= 0; y--) {
- result = this._findInLine(term, y, startCol, searchOptions, isReverseSearch);
+ searchPosition.startRow = y;
+ result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
if (result) {
break;
}
@@ -154,13 +176,17 @@ export class SearchAddon implements ITerminalAddon {
// If we hit the top and didn't search from the very bottom wrap back down
if (!result && startRow !== (this._terminal.buffer.baseY + this._terminal.rows)) {
for (let y = (this._terminal.buffer.baseY + this._terminal.rows); y > startRow; y--) {
- result = this._findInLine(term, y, startCol, searchOptions, isReverseSearch);
+ searchPosition.startRow = y;
+ result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
if (result) {
break;
}
}
}
+ // If there is only one result, return true.
+ if (!result && currentSelection) return true;
+
// Set selection and scroll if a result was found
return this._selectResult(result);
}
@@ -213,18 +239,28 @@ export class SearchAddon implements ITerminalAddon {
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
* text starts on is searched.
* @param term The search term.
- * @param row The line to start the search from.
- * @param col The column to start the search from.
+ * @param position The position to start the search.
* @param searchOptions Search options.
* @return The search result if it was found.
*/
- protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
+ protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
const terminal = this._terminal!;
+ let row = searchPosition.startRow;
+ const col = searchPosition.startCol;
// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
const firstLine = terminal.buffer.getLine(row);
if (firstLine && firstLine.isWrapped) {
- return;
+ if (isReverseSearch) {
+ searchPosition.startCol += terminal.cols;
+ return;
+ }
+
+ // This will iterate until we find the line start.
+ // When we find it, we will search using the calculated start column.
+ searchPosition.startRow--;
+ searchPosition.startCol += terminal.cols;
+ return this._findInLine(term, searchPosition, searchOptions);
}
let stringLine = this._linesCache ? this._linesCache[row] : void 0;
if (stringLine === void 0) {
@@ -343,9 +379,12 @@ export class SearchAddon implements ITerminalAddon {
return false;
}
terminal.select(result.col, result.row, result.term.length);
- let scroll = result.row - terminal.buffer.viewportY;
- scroll = scroll - Math.floor(terminal.rows / 2);
- terminal.scrollLines(scroll);
+ // If it is not in the viewport then we scroll else it just gets selected
+ if (result.row >= (terminal.buffer.viewportY + terminal.rows) || result.row < terminal.buffer.viewportY) {
+ let scroll = result.row - terminal.buffer.viewportY;
+ scroll = scroll - Math.floor(terminal.rows / 2);
+ terminal.scrollLines(scroll);
+ }
return true;
}
}
diff --git a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts
index 94bc5297..f27aba78 100644
--- a/addons/xterm-addon-search/typings/xterm-addon-search.d.ts
+++ b/addons/xterm-addon-search/typings/xterm-addon-search.d.ts
@@ -17,7 +17,7 @@ declare module 'xterm-addon-search' {
/**
* Whether to search for a whole word, the result is only valid if it's
- * suppounded in "non-word" characters such as `_`, `(`, `)` or space.
+ * surrounded in "non-word" characters such as `_`, `(`, `)` or space.
*/
wholeWord?: boolean;
@@ -27,7 +27,7 @@ declare module 'xterm-addon-search' {
caseSensitive?: boolean;
/**
- * Whether to do an indcremental search, this will expand the selection if it
+ * Whether to do an incremental search, this will expand the selection if it
* still matches the term the user typed. Note that this only affects
* `findNext`, not `findPrevious`.
*/
diff --git a/addons/xterm-addon-web-links/README.md b/addons/xterm-addon-web-links/README.md
index dc9e1c32..e53f4edf 100644
--- a/addons/xterm-addon-web-links/README.md
+++ b/addons/xterm-addon-web-links/README.md
@@ -1,8 +1,6 @@
## xterm-addon-web-links
-[](https://dev.azure.com/xtermjs/xterm-addon-web-links/_build/latest?definitionId=5&branchName=master)
-
-An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enabled web links. This addon requires xterm.js 3.14+.
+An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables web links. This addon requires xterm.js v4+.
### Install
@@ -20,4 +18,4 @@ const terminal = new Terminal();
terminal.loadAddon(new WebLinksAddon());
```
-You can also specify a custom handler and options, see the [API](https://github.com/xtermjs/xterm-addon-web-links/blob/master/typings/web-links.d.ts) for more details.
+See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts) for more advanced usage.
diff --git a/addons/xterm-addon-web-links/package.json b/addons/xterm-addon-web-links/package.json
index fd3945d3..6167de74 100644
--- a/addons/xterm-addon-web-links/package.json
+++ b/addons/xterm-addon-web-links/package.json
@@ -1,12 +1,13 @@
{
"name": "xterm-addon-web-links",
- "version": "0.1.0",
+ "version": "0.2.1",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
},
"main": "lib/xterm-addon-web-links.js",
"types": "typings/xterm-addon-web-links.d.ts",
+ "repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"scripts": {
"build": "../../node_modules/.bin/tsc -p src",
@@ -15,6 +16,6 @@
"prepublishOnly": "npm run package"
},
"peerDependencies": {
- "xterm": "^3.14.0"
+ "xterm": "^4.0.0"
}
}
diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts
index 2c0044be..ab61fdbf 100644
--- a/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts
+++ b/addons/xterm-addon-web-links/src/WebLinksAddon.api.ts
@@ -19,8 +19,7 @@ describe('WebLinksAddon', () => {
this.timeout(10000);
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
- slowMo: 80,
- args: [`--window-size=${width},${height}`]
+ args: [`--window-size=${width},${height}`, `--no-sandbox`]
});
page = (await browser.pages())[0];
await page.setViewport({ width, height });
@@ -54,22 +53,21 @@ describe('WebLinksAddon', () => {
async function testHostName(hostname: string): Promise {
await openTerminal({ rendererType: 'dom' });
await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`);
- await page.evaluate(`
- window.term.writeln(' http://${hostname} ');
- window.term.writeln(' http://${hostname}/a~b#c~d?e~f ');
- window.term.writeln(' http://${hostname}/colon:test ');
- window.term.writeln(' http://${hostname}/colon:test: ');
- window.term.writeln('"http://${hostname}/"');
- window.term.writeln('\\'http://${hostname}/\\'');
- window.term.writeln('http://${hostname}/subpath/+/id');
- `);
- assert.equal(await getLinkAtCell(3, 1), `http://${hostname}`);
- assert.equal(await getLinkAtCell(3, 2), `http://${hostname}/a~b#c~d?e~f`);
- assert.equal(await getLinkAtCell(3, 3), `http://${hostname}/colon:test`);
- assert.equal(await getLinkAtCell(3, 4), `http://${hostname}/colon:test`);
- assert.equal(await getLinkAtCell(2, 5), `http://${hostname}/`);
- assert.equal(await getLinkAtCell(2, 6), `http://${hostname}/`);
- assert.equal(await getLinkAtCell(1, 7), `http://${hostname}/subpath/+/id`);
+ const data = ` http://${hostname} \\r\\n` +
+ ` http://${hostname}/a~b#c~d?e~f \\r\\n` +
+ ` http://${hostname}/colon:test \\r\\n` +
+ ` http://${hostname}/colon:test: \\r\\n` +
+ `"http://${hostname}/"\\r\\n` +
+ `\\'http://${hostname}/\\'\\r\\n` +
+ `http://${hostname}/subpath/+/id`;
+ await writeSync(page, data);
+ await pollForLinkAtCell(3, 1, `http://${hostname}`);
+ await pollForLinkAtCell(3, 2, `http://${hostname}/a~b#c~d?e~f`);
+ await pollForLinkAtCell(3, 3, `http://${hostname}/colon:test`);
+ await pollForLinkAtCell(3, 4, `http://${hostname}/colon:test`);
+ await pollForLinkAtCell(2, 5, `http://${hostname}/`);
+ await pollForLinkAtCell(2, 6, `http://${hostname}/`);
+ await pollForLinkAtCell(1, 7, `http://${hostname}/subpath/+/id`);
}
async function openTerminal(options: ITerminalOptions = {}): Promise {
@@ -82,8 +80,30 @@ async function openTerminal(options: ITerminalOptions = {}): Promise {
}
}
-async function getLinkAtCell(col: number, row: number): Promise {
+async function pollForLinkAtCell(col: number, row: number, value: string): Promise {
const rowSelector = `.xterm-rows > :nth-child(${row})`;
- await page.hover(`${rowSelector} > :nth-child(${col})`);
- return await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`);
+ // Ensure the hover element exists before trying to hover it
+ await pollFor(page, `!!document.querySelector('${rowSelector} > :nth-child(${col})')`, true);
+ await pollFor(page, `document.querySelectorAll('${rowSelector} > span[style]').length >= ${value.length}`, true, async () => page.hover(`${rowSelector} > :nth-child(${col})`));
+ assert.equal(await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`), value);
+}
+
+async function pollFor(page: puppeteer.Page, fn: string, val: any, preFn?: () => Promise): Promise {
+ if (preFn) {
+ await preFn();
+ }
+ const result = await page.evaluate(fn);
+ if (result !== val) {
+ return new Promise(r => {
+ setTimeout(() => r(pollFor(page, fn, val, preFn)), 10);
+ });
+ }
+}
+
+async function writeSync(page: puppeteer.Page, data: string): Promise {
+ await page.evaluate(`
+ window.ready = false;
+ window.term.write('${data}', () => window.ready = true);
+ `);
+ await pollFor(page, 'window.ready', true);
}
diff --git a/addons/xterm-addon-webgl/README.md b/addons/xterm-addon-webgl/README.md
new file mode 100644
index 00000000..026a738e
--- /dev/null
+++ b/addons/xterm-addon-webgl/README.md
@@ -0,0 +1,23 @@
+## xterm-addon-webgl
+
+An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL-based renderer. This addon requires xterm.js v4+.
+
+⚠️ This is an experimental addon that is [missing some features and may be unstable](https://github.com/xtermjs/xterm.js/issues?q=is%3Aopen+is%3Aissue+label%3Aarea%2Faddon%2Fwebgl) ⚠️
+
+### Install
+
+```bash
+npm install --save xterm-addon-webgl
+```
+
+### Usage
+
+```ts
+import { Terminal } from 'xterm';
+import { WebglAddon } from 'xterm-addon-webgl';
+
+const terminal = new Terminal();
+terminal.loadAddon(new WebglAddon());
+```
+
+See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts) for more advanced usage.
diff --git a/addons/xterm-addon-webgl/package.json b/addons/xterm-addon-webgl/package.json
index e4f34147..a6befe08 100644
--- a/addons/xterm-addon-webgl/package.json
+++ b/addons/xterm-addon-webgl/package.json
@@ -1,12 +1,13 @@
{
"name": "xterm-addon-webgl",
- "version": "0.1.0",
+ "version": "0.3.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
},
"main": "lib/xterm-addon-webgl.js",
"types": "typings/xterm-addon-webgl.d.ts",
+ "repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"scripts": {
"build": "../../node_modules/.bin/tsc -p src",
@@ -15,6 +16,6 @@
"prepublishOnly": "npm run package"
},
"peerDependencies": {
- "xterm": "^3.14.0"
+ "xterm": "^4.0.0"
}
}
diff --git a/addons/xterm-addon-webgl/src/CharDataCompat.ts b/addons/xterm-addon-webgl/src/CharDataCompat.ts
deleted file mode 100644
index 13613c88..00000000
--- a/addons/xterm-addon-webgl/src/CharDataCompat.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Copyright (c) 2019 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-import { CellData } from 'common/buffer/CellData';
-import { FLAGS } from './Constants';
-import { IBufferLine } from 'common/Types';
-
-export function getCompatAttr(bufferLine: IBufferLine, index: number): number {
- // TODO: Need to move WebGL over to the new system and remove this block
- const cell = new CellData();
- bufferLine.loadCell(index, cell);
- const oldBg = cell.getBgColor() === -1 ? 256 : cell.getBgColor();
- const oldFg = cell.getFgColor() === -1 ? 256 : cell.getFgColor();
- const oldAttr =
- (cell.isBold() ? FLAGS.BOLD : 0) |
- (cell.isUnderline() ? FLAGS.UNDERLINE : 0) |
- (cell.isBlink() ? FLAGS.BLINK : 0) |
- (cell.isInverse() ? FLAGS.INVERSE : 0) |
- (cell.isDim() ? FLAGS.DIM : 0) |
- (cell.isItalic() ? FLAGS.ITALIC : 0);
- const attrCompat =
- oldBg |
- (oldFg << 9) |
- (oldAttr << 18);
- return attrCompat;
-}
diff --git a/addons/xterm-addon-webgl/src/ColorUtils.ts b/addons/xterm-addon-webgl/src/ColorUtils.ts
deleted file mode 100644
index 80372e65..00000000
--- a/addons/xterm-addon-webgl/src/ColorUtils.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * @license MIT
- * Copyright (c) 2018 The xterm.js authors. All rights reserved.
- */
-
-import { IColor } from 'browser/Types';
-
-export function getLuminance(color: IColor): number {
- // Coefficients taken from: https://www.w3.org/TR/AERT/#color-contrast
- const r = color.rgba >> 24 & 0xff;
- const g = color.rgba >> 16 & 0xff;
- const b = color.rgba >> 8 & 0xff;
- return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
-}
diff --git a/addons/xterm-addon-webgl/src/Constants.ts b/addons/xterm-addon-webgl/src/Constants.ts
deleted file mode 100644
index ab18b97a..00000000
--- a/addons/xterm-addon-webgl/src/Constants.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * Copyright (c) 2019 The xterm.js authors. All rights reserved.
- * @license MIT
- */
-
-// TODO: Should be removed after chardata workaround is fixed
-export const enum FLAGS {
- BOLD = 1,
- UNDERLINE = 2,
- BLINK = 4,
- INVERSE = 8,
- INVISIBLE = 16,
- DIM = 32,
- ITALIC = 64
-}
diff --git a/addons/xterm-addon-webgl/src/GlyphRenderer.ts b/addons/xterm-addon-webgl/src/GlyphRenderer.ts
index 186cb2c1..7b35949d 100644
--- a/addons/xterm-addon-webgl/src/GlyphRenderer.ts
+++ b/addons/xterm-addon-webgl/src/GlyphRenderer.ts
@@ -6,12 +6,10 @@
import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
-import { INDICIES_PER_CELL } from './WebglRenderer';
-import { COMBINED_CHAR_BIT_MASK } from './RenderModel';
+import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET } from './RenderModel';
import { fill } from 'common/TypedArrayUtils';
import { slice } from './TypedArray';
-import { NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
-import { getLuminance } from './ColorUtils';
+import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
import { Terminal, IBufferLine } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
@@ -103,11 +101,7 @@ export class GlyphRenderer {
private _dimensions: IRenderDimensions
) {
const gl = this._gl;
-
const program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource));
- if (program === undefined) {
- throw new Error('Could not create WebGL program');
- }
this._program = program;
// Uniform locations
@@ -172,11 +166,11 @@ export class GlyphRenderer {
return this._atlas ? this._atlas.beginFrame() : true;
}
- public updateCell(x: number, y: number, code: number, attr: number, bg: number, fg: number, chars: string): void {
- this._updateCell(this._vertices.attributes, x, y, code, attr, bg, fg, chars);
+ public updateCell(x: number, y: number, code: number, bg: number, fg: number, chars: string): void {
+ this._updateCell(this._vertices.attributes, x, y, code, bg, fg, chars);
}
- private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, attr: number, bg: number, fg: number, chars?: string): void {
+ private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, chars?: string): void {
const terminal = this._terminal;
const i = (y * terminal.cols + x) * INDICES_PER_CELL;
@@ -189,12 +183,12 @@ export class GlyphRenderer {
let rasterizedGlyph: IRasterizedGlyph;
if (!this._atlas) {
- throw new Error('atlas must be set before updating cell');
+ return;
}
if (chars && chars.length > 1) {
- rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, attr, bg, fg);
+ rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg);
} else {
- rasterizedGlyph = this._atlas.getRasterizedGlyph(code, attr, bg, fg);
+ rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg);
}
// Fill empty if no glyph was found
@@ -223,58 +217,51 @@ export class GlyphRenderer {
this._vertices.selectionAttributes = slice(this._vertices.attributes, 0);
- // TODO: Make fg and bg configurable, currently since the buffer doesn't
- // support truecolor the char atlas cannot store it.
- const lumi = getLuminance(this._colors.background);
- const fg = lumi > 0.5 ? 7 : 0;
- const bg = lumi > 0.5 ? 0 : 7;
+ const bg = (this._colors.selectionOpaque.rgba >>> 8) | Attributes.CM_RGB;
if (columnSelectMode) {
const startCol = model.selection.startCol;
const width = model.selection.endCol - startCol;
const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1;
for (let y = model.selection.viewportCappedStartRow; y < model.selection.viewportCappedStartRow + height; y++) {
- this._updateSelectionRange(startCol, startCol + width, y, model, bg, fg);
+ this._updateSelectionRange(startCol, startCol + width, y, model, bg);
}
} else {
// Draw first row
const startCol = model.selection.viewportStartRow === model.selection.viewportCappedStartRow ? model.selection.startCol : 0;
const startRowEndCol = model.selection.viewportCappedStartRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
- this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg, fg);
+ this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg);
// Draw middle rows
const middleRowsCount = Math.max(model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow - 1, 0);
for (let y = model.selection.viewportCappedStartRow + 1; y <= model.selection.viewportCappedStartRow + middleRowsCount; y++) {
- this._updateSelectionRange(0, startRowEndCol, y, model, bg, fg);
+ this._updateSelectionRange(0, startRowEndCol, y, model, bg);
}
// Draw final row
if (model.selection.viewportCappedStartRow !== model.selection.viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewportStartRow
const endCol = model.selection.viewportEndRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
- this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg, fg);
+ this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg);
}
}
}
- private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number, fg: number): void {
+ private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number): void {
const terminal = this._terminal;
const row = y + terminal.buffer.viewportY;
let line: IBufferLine | undefined;
for (let x = startCol; x < endCol; x++) {
- const offset = (y * this._terminal.cols + x) * INDICIES_PER_CELL;
- // Because the cache uses attr as a lookup key it needs to contain the selection colors as well
- let attr = model.cells[offset + 1];
- attr = attr & ~0x3ffff | bg << 9 | fg;
+ const offset = (y * this._terminal.cols + x) * RENDER_MODEL_INDICIES_PER_CELL;
const code = model.cells[offset];
if (code & COMBINED_CHAR_BIT_MASK) {
if (!line) {
line = terminal.buffer.getLine(row);
}
const chars = line!.getCell(x)!.char;
- this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg, chars);
+ this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET], chars);
} else {
- this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg);
+ this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET]);
}
}
}
diff --git a/addons/xterm-addon-webgl/src/RectangleRenderer.ts b/addons/xterm-addon-webgl/src/RectangleRenderer.ts
index 2c464d2a..b52a506e 100644
--- a/addons/xterm-addon-webgl/src/RectangleRenderer.ts
+++ b/addons/xterm-addon-webgl/src/RectangleRenderer.ts
@@ -6,12 +6,11 @@
import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types';
import { fill } from 'common/TypedArrayUtils';
-import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
-import { is256Color } from './atlas/CharAtlasUtils';
-import { DEFAULT_COLOR } from 'common/buffer/Constants';
+import { Attributes, FgFlags } from 'common/buffer/Constants';
import { Terminal } from 'xterm';
import { IColorSet, IColor } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
+import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
const enum VertexAttribLocations {
POSITION = 0,
@@ -156,7 +155,7 @@ export class RectangleRenderer {
private _updateCachedColors(): void {
this._bgFloat = this._colorToFloat32Array(this._colors.background);
- this._selectionFloat = this._colorToFloat32Array(this._colors.selection);
+ this._selectionFloat = this._colorToFloat32Array(this._colors.selectionOpaque);
}
private _updateViewportRectangle(): void {
@@ -247,47 +246,73 @@ export class RectangleRenderer {
for (let y = 0; y < terminal.rows; y++) {
let currentStartX = -1;
- let currentBg = DEFAULT_COLOR;
+ let currentBg = 0;
+ let currentFg = 0;
+ let currentInverse = false;
for (let x = 0; x < terminal.cols; x++) {
- const modelIndex = ((y * terminal.cols) + x) * 4;
- const bg = model.cells[modelIndex + 2];
- if (bg !== currentBg) {
+ const modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
+ const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET];
+ const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET];
+ const inverse = !!(fg & FgFlags.INVERSE);
+ if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) {
// A rectangle needs to be drawn if going from non-default to another color
- if (currentBg !== DEFAULT_COLOR) {
+ if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
- this._updateRectangle(vertices, offset, currentBg, currentStartX, x, y);
+ this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, x, y);
}
currentStartX = x;
currentBg = bg;
+ currentFg = fg;
+ currentInverse = inverse;
}
}
// Finish rectangle if it's still going
- if (currentBg !== DEFAULT_COLOR) {
+ if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
- this._updateRectangle(vertices, offset, currentBg, currentStartX, terminal.cols, y);
+ this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, terminal.cols, y);
}
}
vertices.count = rectangleCount;
}
- private _updateRectangle(vertices: IVertices, offset: number, bg: number, startX: number, endX: number, y: number): void {
- let color: IColor | null = null;
- if (bg === INVERTED_DEFAULT_COLOR) {
- color = this._colors.foreground;
- } else if (is256Color(bg)) {
- color = this._colors.ansi[bg];
+ private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
+ let rgba: number | undefined;
+ if (fg & FgFlags.INVERSE) {
+ switch (fg & Attributes.CM_MASK) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
+ break;
+ case Attributes.CM_RGB:
+ rgba = (fg & Attributes.RGB_MASK) << 8;
+ break;
+ case Attributes.CM_DEFAULT:
+ default:
+ rgba = this._colors.foreground.rgba;
+ }
} else {
- // TODO: Add support for true color
- color = this._colors.foreground;
+ switch (bg & Attributes.CM_MASK) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
+ break;
+ case Attributes.CM_RGB:
+ rgba = (bg & Attributes.RGB_MASK) << 8;
+ break;
+ case Attributes.CM_DEFAULT:
+ default:
+ rgba = this._colors.background.rgba;
+ }
}
+
if (vertices.attributes.length < offset + 4) {
vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE);
}
const x1 = startX * this._dimensions.scaledCellWidth;
const y1 = y * this._dimensions.scaledCellHeight;
- const r = ((color.rgba >> 24) & 0xFF) / 255;
- const g = ((color.rgba >> 16) & 0xFF) / 255;
- const b = ((color.rgba >> 8 ) & 0xFF) / 255;
+ const r = ((rgba >> 24) & 0xFF) / 255;
+ const g = ((rgba >> 16) & 0xFF) / 255;
+ const b = ((rgba >> 8 ) & 0xFF) / 255;
this._addRectangle(vertices.attributes, offset, x1, y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, r, g, b, 1);
}
diff --git a/addons/xterm-addon-webgl/src/RenderModel.ts b/addons/xterm-addon-webgl/src/RenderModel.ts
index a9ee24e9..2b48047f 100644
--- a/addons/xterm-addon-webgl/src/RenderModel.ts
+++ b/addons/xterm-addon-webgl/src/RenderModel.ts
@@ -6,7 +6,9 @@
import { IRenderModel, ISelectionRenderModel } from './Types';
import { fill } from 'common/TypedArrayUtils';
-export const RENDER_MODEL_INDICIES_PER_CELL = 4;
+export const RENDER_MODEL_INDICIES_PER_CELL = 3;
+export const RENDER_MODEL_BG_OFFSET = 1;
+export const RENDER_MODEL_FG_OFFSET = 2;
export const COMBINED_CHAR_BIT_MASK = 0x80000000;
diff --git a/addons/xterm-addon-webgl/src/Types.d.ts b/addons/xterm-addon-webgl/src/Types.d.ts
index 6ebc8d64..f01092ee 100644
--- a/addons/xterm-addon-webgl/src/Types.d.ts
+++ b/addons/xterm-addon-webgl/src/Types.d.ts
@@ -4,7 +4,7 @@
*/
export interface IRasterizedGlyphSet {
- [flags: number]: IRasterizedGlyph;
+ [bg: number]: { [fg: number]: IRasterizedGlyph } | undefined;
}
/**
diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts
index 0074f9b2..be867d75 100644
--- a/addons/xterm-addon-webgl/src/WebglAddon.ts
+++ b/addons/xterm-addon-webgl/src/WebglAddon.ts
@@ -9,20 +9,28 @@ import { IRenderService } from 'browser/services/Services';
import { IColorSet } from 'browser/Types';
export class WebglAddon implements ITerminalAddon {
+ private _terminal?: Terminal;
+
constructor(
private _preserveDrawingBuffer?: boolean
) {}
public activate(terminal: Terminal): void {
if (!terminal.element) {
- throw new Error('Cannot activate WebglRendererAddon before Terminal.open');
+ throw new Error('Cannot activate WebglAddon before Terminal.open');
}
+ this._terminal = terminal;
const renderService: IRenderService = (terminal)._core._renderService;
const colors: IColorSet = (terminal)._core._colorManager.colors;
renderService.setRenderer(new WebglRenderer(terminal, colors, this._preserveDrawingBuffer));
}
public dispose(): void {
- throw new Error('WebglRendererAddon.dispose Not yet implemented');
+ if (!this._terminal) {
+ throw new Error('Cannot dispose WebglAddon because it is activated');
+ }
+ const renderService: IRenderService = (this._terminal)._core._renderService;
+ renderService.setRenderer((this._terminal)._core._createRenderer());
+ renderService.onResize(this._terminal.cols, this._terminal.rows);
}
}
diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts
index b26790c4..e26e054a 100644
--- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts
+++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts
@@ -4,9 +4,10 @@
*/
import * as puppeteer from 'puppeteer';
-import { assert } from 'chai';
import { ITerminalOptions } from '../../../src/Types';
import { ITheme } from 'xterm';
+import { assert } from 'chai';
+import deepEqual = require('deep-equal');
const APP = 'http://127.0.0.1:3000/test';
@@ -16,31 +17,20 @@ const width = 800;
const height = 600;
describe('WebGL Renderer Integration Tests', function(): void {
- this.timeout(20000);
-
- before(async function(): Promise {
- browser = await puppeteer.launch({
- headless: process.argv.indexOf('--headless') !== -1,
- slowMo: 80,
- args: [`--window-size=${width},${height}`]
- });
- page = (await browser.pages())[0];
- await page.setViewport({ width, height });
- await page.goto(APP);
- await openTerminal();
- await page.evaluate(`window.term.loadAddon(new WebglAddon(true));`);
+ it('dispose removes renderer canvases', async () => {
+ await setupBrowser();
+ assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 3);
+ await page.evaluate(`addon.dispose()`);
+ assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 0);
+ await browser.close();
});
- after(() => {
- browser.close();
- });
+ describe('colors', () => {
+ before(async () => setupBrowser());
+ after(async () => browser.close());
+ beforeEach(async () => page.evaluate(`window.term.reset()`));
- beforeEach(async () => {
- await page.evaluate(`window.term.reset()`);
- });
-
- describe('WebGL Renderer', () => {
- it('foreground colors normal', async function(): Promise {
+ it('foreground 0-15', async () => {
const theme: ITheme = {
black: '#010203',
red: '#040506',
@@ -53,17 +43,17 @@ describe('WebGL Renderer Integration Tests', function(): void {
};
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█`);
- assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]);
- assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]);
- assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]);
- assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]);
- assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]);
- assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]);
- assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]);
- assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]);
+ await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
+ await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
+ await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
+ await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
+ await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
+ await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
+ await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
+ await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
});
- it('foreground colors bright', async function(): Promise {
+ it('foreground 0-7 drawBoldTextInBrightColors', async () => {
const theme: ITheme = {
brightBlack: '#010203',
brightRed: '#040506',
@@ -74,19 +64,22 @@ describe('WebGL Renderer Integration Tests', function(): void {
brightCyan: '#131415',
brightWhite: '#161718'
};
- await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
- await writeSync(`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`);
- assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]);
- assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]);
- assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]);
- assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]);
- assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]);
- assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]);
- assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]);
- assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]);
+ await page.evaluate(`
+ window.term.setOption('theme', ${JSON.stringify(theme)});
+ window.term.setOption('drawBoldTextInBrightColors', true);
+ `);
+ await writeSync(`\\x1b[1;30m█\\x1b[1;31m█\\x1b[1;32m█\\x1b[1;33m█\\x1b[1;34m█\\x1b[1;35m█\\x1b[1;36m█\\x1b[1;37m█`);
+ await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
+ await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
+ await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
+ await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
+ await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
+ await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
+ await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
+ await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
});
- it('background colors normal', async function(): Promise {
+ it('background 0-15', async () => {
const theme: ITheme = {
black: '#010203',
red: '#040506',
@@ -99,17 +92,86 @@ describe('WebGL Renderer Integration Tests', function(): void {
};
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(`\\x1b[40m \\x1b[41m \\x1b[42m \\x1b[43m \\x1b[44m \\x1b[45m \\x1b[46m \\x1b[47m `);
- assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]);
- assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]);
- assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]);
- assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]);
- assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]);
- assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]);
- assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]);
- assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]);
+ await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
+ await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
+ await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
+ await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
+ await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
+ await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
+ await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
+ await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
});
- it('background colors bright', async function(): Promise {
+ it('foreground 0-15 inverse', async () => {
+ const theme: ITheme = {
+ black: '#010203',
+ red: '#040506',
+ green: '#070809',
+ yellow: '#0a0b0c',
+ blue: '#0d0e0f',
+ magenta: '#101112',
+ cyan: '#131415',
+ white: '#161718'
+ };
+ await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
+ await writeSync(`\\x1b[7;30m \\x1b[7;31m \\x1b[7;32m \\x1b[7;33m \\x1b[7;34m \\x1b[7;35m \\x1b[7;36m \\x1b[7;37m `);
+ await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
+ await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
+ await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
+ await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
+ await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
+ await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
+ await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
+ await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
+ });
+
+ it('background 0-15 inverse', async () => {
+ const theme: ITheme = {
+ black: '#010203',
+ red: '#040506',
+ green: '#070809',
+ yellow: '#0a0b0c',
+ blue: '#0d0e0f',
+ magenta: '#101112',
+ cyan: '#131415',
+ white: '#161718'
+ };
+ await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
+ await writeSync(`\\x1b[7;40m█\\x1b[7;41m█\\x1b[7;42m█\\x1b[7;43m█\\x1b[7;44m█\\x1b[7;45m█\\x1b[7;46m█\\x1b[7;47m█`);
+ await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
+ await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
+ await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
+ await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
+ await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
+ await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
+ await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
+ await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
+ });
+
+ it('foreground 0-15 bright', async () => {
+ const theme: ITheme = {
+ brightBlack: '#010203',
+ brightRed: '#040506',
+ brightGreen: '#070809',
+ brightYellow: '#0a0b0c',
+ brightBlue: '#0d0e0f',
+ brightMagenta: '#101112',
+ brightCyan: '#131415',
+ brightWhite: '#161718'
+ };
+ await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
+ await writeSync(`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`);
+ await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
+ await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
+ await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
+ await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
+ await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
+ await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
+ await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
+ await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
+ });
+
+ it('background 0-15 bright', async () => {
const theme: ITheme = {
brightBlack: '#010203',
brightRed: '#040506',
@@ -122,14 +184,522 @@ describe('WebGL Renderer Integration Tests', function(): void {
};
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(`\\x1b[100m \\x1b[101m \\x1b[102m \\x1b[103m \\x1b[104m \\x1b[105m \\x1b[106m \\x1b[107m `);
- assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]);
- assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]);
- assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]);
- assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]);
- assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]);
- assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]);
- assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]);
- assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]);
+ await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
+ await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
+ await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
+ await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
+ await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
+ await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
+ await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
+ await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
+ });
+
+ it('foreground 16-255', async () => {
+ let data = '';
+ for (let y = 0; y < 240 / 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ data += `\\x1b[38;5;${16 + y * 16 + x}m█\x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 240 / 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const cssColor = COLORS_16_TO_255[y * 16 + x];
+ const r = parseInt(cssColor.substr(1, 2), 16);
+ const g = parseInt(cssColor.substr(3, 2), 16);
+ const b = parseInt(cssColor.substr(5, 2), 16);
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
+ }
+ }
+ });
+
+ it('background 16-255', async () => {
+ let data = '';
+ for (let y = 0; y < 240 / 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ data += `\\x1b[48;5;${16 + y * 16 + x}m \x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 240 / 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const cssColor = COLORS_16_TO_255[y * 16 + x];
+ const r = parseInt(cssColor.substr(1, 2), 16);
+ const g = parseInt(cssColor.substr(3, 2), 16);
+ const b = parseInt(cssColor.substr(5, 2), 16);
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
+ }
+ }
+ });
+
+ it('foreground 16-255 inverse', async () => {
+ let data = '';
+ for (let y = 0; y < 240 / 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ data += `\\x1b[7;38;5;${16 + y * 16 + x}m \x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 240 / 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const cssColor = COLORS_16_TO_255[y * 16 + x];
+ const r = parseInt(cssColor.substr(1, 2), 16);
+ const g = parseInt(cssColor.substr(3, 2), 16);
+ const b = parseInt(cssColor.substr(5, 2), 16);
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
+ }
+ }
+ });
+
+ it('background 16-255 inverse', async () => {
+ let data = '';
+ for (let y = 0; y < 240 / 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ data += `\\x1b[7;48;5;${16 + y * 16 + x}m█\x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 240 / 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const cssColor = COLORS_16_TO_255[y * 16 + x];
+ const r = parseInt(cssColor.substr(1, 2), 16);
+ const g = parseInt(cssColor.substr(3, 2), 16);
+ const b = parseInt(cssColor.substr(5, 2), 16);
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
+ }
+ }
+ });
+
+ it('foreground true color red', async () => {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[38;2;${i};0;0m█\x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]);
+ }
+ }
+ });
+
+ it('background true color red', async () => {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[48;2;${i};0;0m \x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]);
+ }
+ }
+ });
+
+ it('foreground true color green', async () => {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[38;2;0;${i};0m█\x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]);
+ }
+ }
+ });
+
+ it('background true color green', async () => {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[48;2;0;${i};0m \x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]);
+ }
+ }
+ });
+
+ it('foreground true color blue', async () => {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[38;2;0;0;${i}m█\x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]);
+ }
+ }
+ });
+
+ it('background true color blue', async () => {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[48;2;0;0;${i}m \x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]);
+ }
+ }
+ });
+
+ it('foreground true color grey', async () => {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[38;2;${i};${i};${i}m█\x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]);
+ }
+ }
+ });
+
+ it('background true color grey', async () => {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[48;2;${i};${i};${i}m \x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]);
+ }
+ }
+ });
+
+ it('foreground true color red inverse', async function(): Promise {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\x1b[7;38;2;${i};0;0m \x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]);
+ }
+ }
+ });
+
+ it('background true color red inverse', async function(): Promise {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[7;48;2;${i};0;0m█\\x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]);
+ }
+ }
+ });
+
+ it('foreground true color green inverse', async () => {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[7;38;2;0;${i};0m \x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]);
+ }
+ }
+ });
+
+ it('background true color green inverse', async () => {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[7;48;2;0;${i};0m█\x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]);
+ }
+ }
+ });
+
+ it('foreground true color blue inverse', async () => {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[7;38;2;0;0;${i}m \x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]);
+ }
+ }
+ });
+
+ it('background true color blue inverse', async () => {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[7;48;2;0;0;${i}m█\x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]);
+ }
+ }
+ });
+
+ it('foreground true color grey inverse', async () => {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[7;38;2;${i};${i};${i}m \x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]);
+ }
+ }
+ });
+
+ it('background true color grey inverse', async () => {
+ let data = '';
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ data += `\\x1b[7;48;2;${i};${i};${i}m█\x1b[0m`;
+ }
+ data += '\\r\\n';
+ }
+ await writeSync(data);
+ for (let y = 0; y < 16; y++) {
+ for (let x = 0; x < 16; x++) {
+ const i = y * 16 + x;
+ await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]);
+ }
+ }
+ });
+ });
+
+ describe('minimumContrastRatio', async () => {
+ before(async () => setupBrowser());
+ after(async () => browser.close());
+ beforeEach(async () => page.evaluate(`window.term.reset()`));
+
+ it('should adjust 0-15 colors on black background', async () => {
+ const theme: ITheme = {
+ black: '#2e3436',
+ red: '#cc0000',
+ green: '#4e9a06',
+ yellow: '#c4a000',
+ blue: '#3465a4',
+ magenta: '#75507b',
+ cyan: '#06989a',
+ white: '#d3d7cf',
+ brightBlack: '#555753',
+ brightRed: '#ef2929',
+ brightGreen: '#8ae234',
+ brightYellow: '#fce94f',
+ brightBlue: '#729fcf',
+ brightMagenta: '#ad7fa8',
+ brightCyan: '#34e2e2',
+ brightWhite: '#eeeeec'
+ };
+ await page.evaluate(`
+ window.term.setOption('theme', ${JSON.stringify(theme)});
+ window.term.setOption('minimumContrastRatio', 1);
+ `);
+ await writeSync(
+ `\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
+ `\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
+ );
+ // Validate before minimumContrastRatio is applied
+ await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]);
+ await pollFor(page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]);
+ await pollFor(page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]);
+ await pollFor(page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]);
+ await pollFor(page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]);
+ await pollFor(page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]);
+ await pollFor(page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]);
+ await pollFor(page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]);
+ await pollFor(page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]);
+ await pollFor(page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]);
+ await pollFor(page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]);
+ await pollFor(page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]);
+ await pollFor(page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]);
+ await pollFor(page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]);
+ await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]);
+ await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]);
+ // Setting and check for minimum contrast values, note that these are note
+ // exact to the contrast ratio, if the increase luminance algorithm
+ // changes then these will probably fail
+ await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`);
+ await pollFor(page, () => getCellColor(1, 1), [176, 180, 180, 255]);
+ await pollFor(page, () => getCellColor(2, 1), [238, 158, 158, 255]);
+ await pollFor(page, () => getCellColor(3, 1), [197, 223, 171, 255]);
+ await pollFor(page, () => getCellColor(4, 1), [235, 221, 158, 255]);
+ await pollFor(page, () => getCellColor(5, 1), [124, 156, 198, 255]);
+ await pollFor(page, () => getCellColor(6, 1), [183, 165, 187, 255]);
+ await pollFor(page, () => getCellColor(7, 1), [110, 197, 198, 255]);
+ await pollFor(page, () => getCellColor(8, 1), [211, 215, 207, 255]);
+ await pollFor(page, () => getCellColor(1, 2), [183, 185, 183, 255]);
+ await pollFor(page, () => getCellColor(2, 2), [249, 156, 156, 255]);
+ await pollFor(page, () => getCellColor(3, 2), [138, 226, 52, 255]);
+ await pollFor(page, () => getCellColor(4, 2), [252, 233, 79, 255]);
+ await pollFor(page, () => getCellColor(5, 2), [114, 159, 207, 255]);
+ await pollFor(page, () => getCellColor(6, 2), [190, 152, 185, 255]);
+ // Unchanged
+ await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]);
+ await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]);
+ });
+
+ it('should adjust 0-15 colors on white background', async () => {
+ const theme: ITheme = {
+ background: '#ffffff',
+ black: '#2e3436',
+ red: '#cc0000',
+ green: '#4e9a06',
+ yellow: '#c4a000',
+ blue: '#3465a4',
+ magenta: '#75507b',
+ cyan: '#06989a',
+ white: '#d3d7cf',
+ brightBlack: '#555753',
+ brightRed: '#ef2929',
+ brightGreen: '#8ae234',
+ brightYellow: '#fce94f',
+ brightBlue: '#729fcf',
+ brightMagenta: '#ad7fa8',
+ brightCyan: '#34e2e2',
+ brightWhite: '#eeeeec'
+ };
+ await page.evaluate(`
+ window.term.setOption('theme', ${JSON.stringify(theme)});
+ window.term.setOption('minimumContrastRatio', 1);
+ `);
+ await writeSync(
+ `\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
+ `\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
+ );
+ // Validate before minimumContrastRatio is applied
+ await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]);
+ await pollFor(page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]);
+ await pollFor(page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]);
+ await pollFor(page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]);
+ await pollFor(page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]);
+ await pollFor(page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]);
+ await pollFor(page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]);
+ await pollFor(page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]);
+ await pollFor(page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]);
+ await pollFor(page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]);
+ await pollFor(page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]);
+ await pollFor(page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]);
+ await pollFor(page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]);
+ await pollFor(page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]);
+ await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]);
+ await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]);
+ // Setting and check for minimum contrast values, note that these are note
+ // exact to the contrast ratio, if the increase luminance algorithm
+ // changes then these will probably fail
+ await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`);
+ await pollFor(page, () => getCellColor(1, 1), [46, 52, 54, 255]);
+ await pollFor(page, () => getCellColor(2, 1), [132, 0, 0, 255]);
+ await pollFor(page, () => getCellColor(3, 1), [78, 154, 6, 255]);
+ await pollFor(page, () => getCellColor(4, 1), [114, 93, 0, 255]);
+ await pollFor(page, () => getCellColor(5, 1), [19, 40, 68, 255]);
+ await pollFor(page, () => getCellColor(6, 1), [60, 40, 64, 255]);
+ await pollFor(page, () => getCellColor(7, 1), [0, 71, 72, 255]);
+ await pollFor(page, () => getCellColor(8, 1), [64, 64, 63, 255]);
+ await pollFor(page, () => getCellColor(1, 2), [61, 63, 59, 255]);
+ await pollFor(page, () => getCellColor(2, 2), [125, 19, 19, 255]);
+ await pollFor(page, () => getCellColor(3, 2), [89, 146, 32, 255]);
+ await pollFor(page, () => getCellColor(4, 2), [105, 98, 32, 255]);
+ await pollFor(page, () => getCellColor(5, 2), [36, 52, 70, 255]);
+ await pollFor(page, () => getCellColor(6, 2), [64, 45, 63, 255]);
+ await pollFor(page, () => getCellColor(7, 2), [13, 67, 67, 255]);
+ await pollFor(page, () => getCellColor(8, 2), [64, 64, 64, 255]);
});
});
});
@@ -145,12 +715,7 @@ async function openTerminal(options: ITerminalOptions = {}): Promise {
}
async function writeSync(data: string): Promise {
- await page.evaluate(`window.term.write('${data}');`);
- while (true) {
- if (await page.evaluate(`window.term._core.writeBuffer.length === 0`)) {
- break;
- }
- }
+ return page.evaluate(`new Promise(resolve => window.term.write('${data}', resolve))`);
}
async function getCellColor(col: number, row: number): Promise {
@@ -166,3 +731,50 @@ async function getCellColor(col: number, row: number): Promise {
`);
return await page.evaluate(`Array.from(window.result)`);
}
+
+async function setupBrowser(): Promise {
+ browser = await puppeteer.launch({
+ headless: process.argv.indexOf('--headless') !== -1,
+ args: [`--window-size=${width},${height}`, `--no-sandbox`]
+ });
+ page = (await browser.pages())[0];
+ await page.setViewport({ width, height });
+ await page.goto(APP);
+ await openTerminal({
+ rendererType: 'dom'
+ });
+ await page.evaluate(`
+ window.addon = new WebglAddon(true);
+ window.term.loadAddon(window.addon);
+ `);
+}
+
+export async function pollFor(page: puppeteer.Page, evalOrFn: string | (() => Promise), val: T, preFn?: () => Promise): Promise {
+ if (preFn) {
+ await preFn();
+ }
+ const result = typeof evalOrFn === 'string' ? await page.evaluate(evalOrFn) : await evalOrFn();
+ if (!deepEqual(result, val)) {
+ return new Promise(r => {
+ setTimeout(() => r(pollFor(page, evalOrFn, val, preFn)), 1);
+ });
+ }
+}
+
+const COLORS_16_TO_255 = [
+ '#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af',
+ '#0087d7', '#0087ff', '#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff', '#00d700', '#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f',
+ '#00ff87', '#00ffaf', '#00ffd7', '#00ffff', '#5f0000', '#5f005f', '#5f0087', '#5f00af', '#5f00d7', '#5f00ff', '#5f5f00', '#5f5f5f', '#5f5f87', '#5f5faf', '#5f5fd7', '#5f5fff',
+ '#5f8700', '#5f875f', '#5f8787', '#5f87af', '#5f87d7', '#5f87ff', '#5faf00', '#5faf5f', '#5faf87', '#5fafaf', '#5fafd7', '#5fafff', '#5fd700', '#5fd75f', '#5fd787', '#5fd7af',
+ '#5fd7d7', '#5fd7ff', '#5fff00', '#5fff5f', '#5fff87', '#5fffaf', '#5fffd7', '#5fffff', '#870000', '#87005f', '#870087', '#8700af', '#8700d7', '#8700ff', '#875f00', '#875f5f',
+ '#875f87', '#875faf', '#875fd7', '#875fff', '#878700', '#87875f', '#878787', '#8787af', '#8787d7', '#8787ff', '#87af00', '#87af5f', '#87af87', '#87afaf', '#87afd7', '#87afff',
+ '#87d700', '#87d75f', '#87d787', '#87d7af', '#87d7d7', '#87d7ff', '#87ff00', '#87ff5f', '#87ff87', '#87ffaf', '#87ffd7', '#87ffff', '#af0000', '#af005f', '#af0087', '#af00af',
+ '#af00d7', '#af00ff', '#af5f00', '#af5f5f', '#af5f87', '#af5faf', '#af5fd7', '#af5fff', '#af8700', '#af875f', '#af8787', '#af87af', '#af87d7', '#af87ff', '#afaf00', '#afaf5f',
+ '#afaf87', '#afafaf', '#afafd7', '#afafff', '#afd700', '#afd75f', '#afd787', '#afd7af', '#afd7d7', '#afd7ff', '#afff00', '#afff5f', '#afff87', '#afffaf', '#afffd7', '#afffff',
+ '#d70000', '#d7005f', '#d70087', '#d700af', '#d700d7', '#d700ff', '#d75f00', '#d75f5f', '#d75f87', '#d75faf', '#d75fd7', '#d75fff', '#d78700', '#d7875f', '#d78787', '#d787af',
+ '#d787d7', '#d787ff', '#d7af00', '#d7af5f', '#d7af87', '#d7afaf', '#d7afd7', '#d7afff', '#d7d700', '#d7d75f', '#d7d787', '#d7d7af', '#d7d7d7', '#d7d7ff', '#d7ff00', '#d7ff5f',
+ '#d7ff87', '#d7ffaf', '#d7ffd7', '#d7ffff', '#ff0000', '#ff005f', '#ff0087', '#ff00af', '#ff00d7', '#ff00ff', '#ff5f00', '#ff5f5f', '#ff5f87', '#ff5faf', '#ff5fd7', '#ff5fff',
+ '#ff8700', '#ff875f', '#ff8787', '#ff87af', '#ff87d7', '#ff87ff', '#ffaf00', '#ffaf5f', '#ffaf87', '#ffafaf', '#ffafd7', '#ffafff', '#ffd700', '#ffd75f', '#ffd787', '#ffd7af',
+ '#ffd7d7', '#ffd7ff', '#ffff00', '#ffff5f', '#ffff87', '#ffffaf', '#ffffd7', '#ffffff', '#080808', '#121212', '#1c1c1c', '#262626', '#303030', '#3a3a3a', '#444444', '#4e4e4e',
+ '#585858', '#626262', '#6c6c6c', '#767676', '#808080', '#8a8a8a', '#949494', '#9e9e9e', '#a8a8a8', '#b2b2b2', '#bcbcbc', '#c6c6c6', '#d0d0d0', '#dadada', '#e4e4e4', '#eeeeee'
+];
diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts
index 3d41e1b5..6f98d2c1 100644
--- a/addons/xterm-addon-webgl/src/WebglRenderer.ts
+++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts
@@ -11,19 +11,15 @@ import { acquireCharAtlas } from './atlas/CharAtlasCache';
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { RectangleRenderer } from './RectangleRenderer';
import { IWebGL2RenderingContext } from './Types';
-import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
-import { RenderModel, COMBINED_CHAR_BIT_MASK } from './RenderModel';
+import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { Disposable } from 'common/Lifecycle';
-import { DEFAULT_COLOR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CODE } from 'common/buffer/Constants';
-import { Terminal } from 'xterm';
-import { getLuminance } from './ColorUtils';
+import { NULL_CELL_CODE } from 'common/buffer/Constants';
+import { Terminal, IEvent } from 'xterm';
import { IRenderLayer } from './renderLayer/Types';
-import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
+import { IRenderDimensions, IRenderer, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
import { IColorSet } from 'browser/Types';
-import { FLAGS } from './Constants';
-import { getCompatAttr } from './CharDataCompat';
-
-export const INDICIES_PER_CELL = 4;
+import { EventEmitter } from 'common/EventEmitter';
+import { CellData } from 'common/buffer/CellData';
export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
@@ -31,6 +27,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _devicePixelRatio: number;
private _model: RenderModel = new RenderModel();
+ private _workCell: CellData = new CellData();
private _canvas: HTMLCanvasElement;
private _gl: IWebGL2RenderingContext;
@@ -40,6 +37,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
public dimensions: IRenderDimensions;
private _core: ITerminal;
+ private _isAttached: boolean;
+
+ private _onRequestRefreshRows = new EventEmitter();
+ public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; }
constructor(
private _terminal: Terminal,
@@ -50,11 +51,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._core = (this._terminal)._core;
- this._applyBgLuminanceBasedSelection();
-
this._renderLayers = [
new LinkRenderLayer(this._core.screenElement, 2, this._colors, this._core),
- new CursorRenderLayer(this._core.screenElement, 3, this._colors)
+ new CursorRenderLayer(this._core.screenElement, 3, this._colors, this._onRequestRefreshRows)
];
this.dimensions = {
scaledCharWidth: 0,
@@ -91,6 +90,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Update dimensions and acquire char atlas
this.onCharSizeChanged();
+
+ this._isAttached = document.body.contains(this._core.screenElement);
}
public dispose(): void {
@@ -99,20 +100,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
super.dispose();
}
- private _applyBgLuminanceBasedSelection(): void {
- // HACK: This is needed until webgl renderer adds support for selection colors
- if (getLuminance(this._colors.background) > 0.5) {
- this._colors.selection = { css: '#000', rgba: 255 };
- } else {
- this._colors.selection = { css: '#fff', rgba: 4294967295 };
- }
- }
-
public setColors(colors: IColorSet): void {
this._colors = colors;
-
- this._applyBgLuminanceBasedSelection();
-
// Clear layers and force a full render
this._renderLayers.forEach(l => {
l.setColors(this._terminal, this._colors);
@@ -185,8 +174,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._rectangleRenderer.updateSelection(this._model.selection, columnSelectMode);
this._glyphRenderer.updateSelection(this._model, columnSelectMode);
- // TODO: #2102 Should this move to RenderCoordinator?
- this._core.refresh(0, this._terminal.rows - 1);
+ this._onRequestRefreshRows.fire({ start: 0, end: this._terminal.rows - 1 });
}
public onCursorMove(): void {
@@ -206,6 +194,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
*/
private _refreshCharAtlas(): void {
if (this.dimensions.scaledCharWidth <= 0 && this.dimensions.scaledCharHeight <= 0) {
+ // Mark as not attached so char atlas gets refreshed on next render
+ this._isAttached = false;
return;
}
@@ -231,6 +221,16 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
public renderRows(start: number, end: number): void {
+ if (!this._isAttached) {
+ if (document.body.contains(this._core.screenElement) && (this._core)._charSizeService.width && (this._core)._charSizeService.height) {
+ this._updateDimensions();
+ this._refreshCharAtlas();
+ this._isAttached = true;
+ } else {
+ return;
+ }
+ }
+
// Update render layers
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
@@ -255,52 +255,34 @@ export class WebglRenderer extends Disposable implements IRenderer {
const line = terminal.buffer.lines.get(row)!;
this._model.lineLengths[y] = 0;
for (let x = 0; x < terminal.cols; x++) {
- const charData = line.get(x);
- const chars = charData[CHAR_DATA_CHAR_INDEX];
- let code = charData[CHAR_DATA_CODE_INDEX];
- const attr = getCompatAttr(line, x); // charData[CHAR_DATA_ATTR_INDEX];
- const i = ((y * terminal.cols) + x) * INDICIES_PER_CELL;
+ line.loadCell(x, this._workCell);
+
+ const chars = this._workCell.getChars();
+ let code = this._workCell.getCode();
+ const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
if (code !== NULL_CELL_CODE) {
this._model.lineLengths[y] = x + 1;
}
// Nothing has changed, no updates needed
- if (this._model.cells[i] === code && this._model.cells[i + 1] === attr) {
+ if (this._model.cells[i] === code &&
+ this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workCell.bg &&
+ this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workCell.fg) {
continue;
}
- // Resolve bg and fg and cache in the model
- const flags = attr >> 18;
- let bg = attr & 0x1ff;
- let fg = (attr >> 9) & 0x1ff;
-
- // If inverse flag is on, the foreground should become the background.
- if (flags & FLAGS.INVERSE) {
- const temp = bg;
- bg = fg;
- fg = temp;
- if (fg === DEFAULT_COLOR) {
- fg = INVERTED_DEFAULT_COLOR;
- }
- if (bg === DEFAULT_COLOR) {
- bg = INVERTED_DEFAULT_COLOR;
- }
- }
- const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && !!(flags & FLAGS.BOLD) && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
- fg += drawInBrightColor ? 8 : 0;
-
// Flag combined chars with a bit mask so they're easily identifiable
if (chars.length > 1) {
code = code | COMBINED_CHAR_BIT_MASK;
}
- this._model.cells[i ] = code;
- this._model.cells[i + 1] = attr;
- this._model.cells[i + 2] = bg;
- this._model.cells[i + 3] = fg;
+ // Cache the results in the model
+ this._model.cells[i] = code;
+ this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workCell.bg;
+ this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workCell.fg;
- this._glyphRenderer.updateCell(x, y, code, attr, bg, fg, chars);
+ this._glyphRenderer.updateCell(x, y, code, this._workCell.bg, this._workCell.fg, chars);
}
}
this._rectangleRenderer.updateBackgrounds(this._model);
diff --git a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts
index 857fa628..8e26b1d6 100644
--- a/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts
+++ b/addons/xterm-addon-webgl/src/atlas/CharAtlasUtils.ts
@@ -4,7 +4,7 @@
*/
import { ICharAtlasConfig } from './Types';
-import { DEFAULT_COLOR } from 'common/buffer/Constants';
+import { Attributes } from 'common/buffer/Constants';
import { Terminal, FontWeight } from 'xterm';
import { IColorSet, IColor } from 'browser/Types';
@@ -21,9 +21,11 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
cursor: NULL_COLOR,
cursorAccent: NULL_COLOR,
selection: NULL_COLOR,
+ selectionOpaque: NULL_COLOR,
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
// dynamic character atlas.
- ansi: colors.ansi.slice()
+ ansi: colors.ansi.slice(),
+ contrastCache: colors.contrastCache
};
return {
devicePixelRatio: window.devicePixelRatio,
@@ -34,6 +36,8 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
fontWeight: terminal.getOption('fontWeight') as FontWeight,
fontWeightBold: terminal.getOption('fontWeightBold') as FontWeight,
allowTransparency: terminal.getOption('allowTransparency'),
+ drawBoldTextInBrightColors: terminal.getOption('drawBoldTextInBrightColors'),
+ minimumContrastRatio: terminal.getOption('minimumContrastRatio'),
colors: clonedColors
};
}
@@ -52,10 +56,12 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
+ a.drawBoldTextInBrightColors === b.drawBoldTextInBrightColors &&
+ a.minimumContrastRatio === b.minimumContrastRatio &&
a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background;
}
export function is256Color(colorCode: number): boolean {
- return colorCode < DEFAULT_COLOR;
+ return (colorCode & Attributes.CM_MASK) === Attributes.CM_P16 || (colorCode & Attributes.CM_MASK) === Attributes.CM_P256;
}
diff --git a/addons/xterm-addon-webgl/src/atlas/Types.d.ts b/addons/xterm-addon-webgl/src/atlas/Types.d.ts
index 1de843e0..cd73393c 100644
--- a/addons/xterm-addon-webgl/src/atlas/Types.d.ts
+++ b/addons/xterm-addon-webgl/src/atlas/Types.d.ts
@@ -25,5 +25,7 @@ export interface ICharAtlasConfig {
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
+ drawBoldTextInBrightColors: boolean;
+ minimumContrastRatio: number;
colors: IColorSet;
}
diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts
index 8d8e995b..cda20225 100644
--- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts
+++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts
@@ -4,14 +4,14 @@
*/
import { ICharAtlasConfig } from './Types';
-import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
+import { DIM_OPACITY } from 'browser/renderer/atlas/Constants';
import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types';
-import { DEFAULT_COLOR, DEFAULT_ATTR } from 'common/buffer/Constants';
-import { is256Color } from './CharAtlasUtils';
+import { DEFAULT_COLOR, FgFlags, Attributes, BgFlags } from 'common/buffer/Constants';
import { throwIfFalsy } from '../WebglUtils';
import { IColor } from 'browser/Types';
-import { FLAGS } from '../Constants';
import { IDisposable } from 'xterm';
+import { AttributeData } from 'common/buffer/AttributeData';
+import { toCss, ensureContrastRatioRgba } from 'browser/Color';
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
@@ -68,8 +68,12 @@ export class WebglCharAtlas implements IDisposable {
public hasCanvasChanged = false;
private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 };
+ private _workAttributeData: AttributeData = new AttributeData();
- constructor(document: Document, private _config: ICharAtlasConfig) {
+ constructor(
+ document: Document,
+ private _config: ICharAtlasConfig
+ ) {
this.cacheCanvas = document.createElement('canvas');
this.cacheCanvas.width = TEXTURE_WIDTH;
this.cacheCanvas.height = TEXTURE_HEIGHT;
@@ -103,9 +107,11 @@ export class WebglCharAtlas implements IDisposable {
protected _doWarmUp(): void {
// Pre-fill with ASCII 33-126
for (let i = 33; i < 126; i++) {
- const rasterizedGlyph = this._drawToCache(i, DEFAULT_ATTR, DEFAULT_COLOR, DEFAULT_COLOR);
+ const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR);
this._cacheMap[i] = {
- [DEFAULT_ATTR]: rasterizedGlyph
+ [DEFAULT_COLOR]: {
+ [DEFAULT_COLOR]: rasterizedGlyph
+ }
};
}
}
@@ -123,16 +129,23 @@ export class WebglCharAtlas implements IDisposable {
return false;
}
- public getRasterizedGlyphCombinedChar(chars: string, attr: number, bg: number, fg: number): IRasterizedGlyph {
+ public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number): IRasterizedGlyph {
let rasterizedGlyphSet = this._cacheMapCombined[chars];
if (!rasterizedGlyphSet) {
rasterizedGlyphSet = {};
this._cacheMapCombined[chars] = rasterizedGlyphSet;
}
- let rasterizedGlyph = rasterizedGlyphSet[attr];
+ let rasterizedGlyph: IRasterizedGlyph | undefined;
+ const rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
+ if (rasterizedGlyphSetBg) {
+ rasterizedGlyph = rasterizedGlyphSetBg[fg];
+ }
if (!rasterizedGlyph) {
- rasterizedGlyph = this._drawToCache(chars, attr, bg, fg);
- rasterizedGlyphSet[attr] = rasterizedGlyph;
+ rasterizedGlyph = this._drawToCache(chars, bg, fg);
+ if (!rasterizedGlyphSet[bg]) {
+ rasterizedGlyphSet[bg] = {};
+ }
+ rasterizedGlyphSet[bg]![fg] = rasterizedGlyph;
}
return rasterizedGlyph;
}
@@ -140,16 +153,23 @@ export class WebglCharAtlas implements IDisposable {
/**
* Gets the glyphs texture coords, drawing the texture if it's not already
*/
- public getRasterizedGlyph(code: number, attr: number, bg: number, fg: number): IRasterizedGlyph {
+ public getRasterizedGlyph(code: number, bg: number, fg: number): IRasterizedGlyph {
let rasterizedGlyphSet = this._cacheMap[code];
if (!rasterizedGlyphSet) {
rasterizedGlyphSet = {};
this._cacheMap[code] = rasterizedGlyphSet;
}
- let rasterizedGlyph = rasterizedGlyphSet[attr];
+ let rasterizedGlyph: IRasterizedGlyph | undefined;
+ const rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
+ if (rasterizedGlyphSetBg) {
+ rasterizedGlyph = rasterizedGlyphSetBg[fg];
+ }
if (!rasterizedGlyph) {
- rasterizedGlyph = this._drawToCache(code, attr, bg, fg);
- rasterizedGlyphSet[attr] = rasterizedGlyph;
+ rasterizedGlyph = this._drawToCache(code, bg, fg);
+ if (!rasterizedGlyphSet[bg]) {
+ rasterizedGlyphSet[bg] = {};
+ }
+ rasterizedGlyphSet[bg]![fg] = rasterizedGlyph;
}
return rasterizedGlyph;
}
@@ -161,48 +181,155 @@ export class WebglCharAtlas implements IDisposable {
return this._config.colors.ansi[idx];
}
- private _getBackgroundColor(bg: number): IColor {
+ private _getBackgroundColor(bgColorMode: number, bgColor: number, inverse: boolean): IColor {
if (this._config.allowTransparency) {
// The background color might have some transparency, so we need to render it as fully
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
// around the anti-aliased edges of the glyph, and it would look too dark.
return TRANSPARENT_COLOR;
- } else if (bg === INVERTED_DEFAULT_COLOR) {
- return this._config.colors.foreground;
- } else if (is256Color(bg)) {
- return this._getColorFromAnsiIndex(bg);
}
- // TODO: Support true color
- return this._config.colors.background;
+
+ switch (bgColorMode) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ return this._getColorFromAnsiIndex(bgColor);
+ case Attributes.CM_RGB:
+ const arr = AttributeData.toColorRGB(bgColor);
+ // TODO: This object creation is slow
+ return {
+ rgba: bgColor << 8,
+ css: `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}`
+ };
+ case Attributes.CM_DEFAULT:
+ default:
+ if (inverse) {
+ return this._config.colors.foreground;
+ }
+ return this._config.colors.background;
+ }
}
- private _getForegroundColor(fg: number): IColor {
- if (fg === INVERTED_DEFAULT_COLOR) {
- return this._config.colors.background;
- } else if (is256Color(fg)) {
- return this._getColorFromAnsiIndex(fg);
+ private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string {
+ const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
+ if (minimumContrastCss) {
+ return minimumContrastCss;
+ }
+
+ switch (fgColorMode) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) {
+ fgColor += 8;
+ }
+ return this._getColorFromAnsiIndex(fgColor).css;
+ case Attributes.CM_RGB:
+ const arr = AttributeData.toColorRGB(fgColor);
+ return toCss(arr[0], arr[1], arr[2]);
+ case Attributes.CM_DEFAULT:
+ default:
+ if (inverse) {
+ return this._config.colors.background.css;
+ }
+ return this._config.colors.foreground.css;
}
- // TODO: Support true color
- return this._config.colors.foreground;
}
- private _drawToCache(code: number, attr: number, bg: number, fg: number): IRasterizedGlyph;
- private _drawToCache(chars: string, attr: number, bg: number, fg: number): IRasterizedGlyph;
- private _drawToCache(codeOrChars: number | string, attr: number, bg: number, fg: number): IRasterizedGlyph {
+ private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number {
+ switch (bgColorMode) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ return this._getColorFromAnsiIndex(bgColor).rgba;
+ case Attributes.CM_RGB:
+ return bgColor << 8;
+ case Attributes.CM_DEFAULT:
+ default:
+ if (inverse) {
+ return this._config.colors.foreground.rgba;
+ }
+ return this._config.colors.background.rgba;
+ }
+ }
+
+ private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number {
+ switch (fgColorMode) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) {
+ fgColor += 8;
+ }
+ return this._getColorFromAnsiIndex(fgColor).rgba;
+ case Attributes.CM_RGB:
+ return fgColor << 8;
+ case Attributes.CM_DEFAULT:
+ default:
+ if (inverse) {
+ return this._config.colors.background.rgba;
+ }
+ return this._config.colors.foreground.rgba;
+ }
+ }
+
+ private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string | undefined {
+ if (this._config.minimumContrastRatio === 1) {
+ return undefined;
+ }
+
+ // Try get from cache first
+ const adjustedColor = this._config.colors.contrastCache.getCss(bg, fg);
+ if (adjustedColor !== undefined) {
+ return adjustedColor || undefined;
+ }
+
+ const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
+ const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold);
+ const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._config.minimumContrastRatio);
+
+ if (!result) {
+ this._config.colors.contrastCache.setCss(bg, fg, null);
+ return undefined;
+ }
+
+ const css = toCss(
+ (result >> 24) & 0xFF,
+ (result >> 16) & 0xFF,
+ (result >> 8) & 0xFF
+ );
+ this._config.colors.contrastCache.setCss(bg, fg, css);
+
+ return css;
+ }
+
+ private _drawToCache(code: number, bg: number, fg: number): IRasterizedGlyph;
+ private _drawToCache(chars: string, bg: number, fg: number): IRasterizedGlyph;
+ private _drawToCache(codeOrChars: number | string, bg: number, fg: number): IRasterizedGlyph {
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
this.hasCanvasChanged = true;
- const flags = attr >> 18;
-
- const bold = !!(flags & FLAGS.BOLD);
- const dim = !!(flags & FLAGS.DIM);
- const italic = !!(flags & FLAGS.ITALIC);
-
this._tmpCtx.save();
+ this._workAttributeData.fg = fg;
+ this._workAttributeData.bg = bg;
+
+ const bold = !!this._workAttributeData.isBold();
+ const inverse = !!this._workAttributeData.isInverse();
+ const dim = !!this._workAttributeData.isDim();
+ const italic = !!this._workAttributeData.isItalic();
+ let fgColor = this._workAttributeData.getFgColor();
+ let fgColorMode = this._workAttributeData.getFgColorMode();
+ let bgColor = this._workAttributeData.getBgColor();
+ let bgColorMode = this._workAttributeData.getBgColorMode();
+ if (inverse) {
+ const temp = fgColor;
+ fgColor = bgColor;
+ bgColor = temp;
+ const temp2 = fgColorMode;
+ fgColorMode = bgColorMode;
+ bgColorMode = temp2;
+ }
+
// draw the background
- const backgroundColor = this._getBackgroundColor(bg);
+ const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse);
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
// transparency in backgroundColor
this._tmpCtx.globalCompositeOperation = 'copy';
@@ -217,7 +344,7 @@ export class WebglCharAtlas implements IDisposable {
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = 'top';
- this._tmpCtx.fillStyle = this._getForegroundColor(fg).css;
+ this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
// Apply alpha to dim the character
if (dim) {
@@ -398,3 +525,25 @@ function clearColor(imageData: ImageData, color: IColor): boolean {
}
return isEmpty;
}
+
+function toPaddedHex(c: number): string {
+ const s = c.toString(16);
+ return s.length < 2 ? '0' + s : s;
+}
+
+function getFgColor(fg: number): number {
+ switch (fg & Attributes.CM_MASK) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256: return fg & Attributes.PCOLOR_MASK;
+ case Attributes.CM_RGB: return fg & Attributes.RGB_MASK;
+ default: return -1; // CM_DEFAULT defaults to -1
+ }
+}
+function getBgColor(bg: number): number {
+ switch (bg & Attributes.CM_MASK) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256: return bg & Attributes.PCOLOR_MASK;
+ case Attributes.CM_RGB: return bg & Attributes.RGB_MASK;
+ default: return -1; // CM_DEFAULT defaults to -1
+ }
+}
diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts
index 0e2d645e..29487ad0 100644
--- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts
+++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts
@@ -4,7 +4,6 @@
*/
import { IRenderLayer } from './Types';
-import { IGlyphIdentifier } from '../atlas/Types';
import { acquireCharAtlas } from '../atlas/CharAtlasCache';
import { Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
@@ -25,19 +24,6 @@ export abstract class BaseRenderLayer implements IRenderLayer {
protected _charAtlas: WebglCharAtlas | undefined;
- /**
- * An object that's reused when drawing glyphs in order to reduce GC.
- */
- private _currentGlyphIdentifier: IGlyphIdentifier = {
- chars: '',
- code: 0,
- bg: 0,
- fg: 0,
- bold: false,
- dim: false,
- italic: false
- };
-
constructor(
private _container: HTMLElement,
id: string,
diff --git a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts
index 6aed5f53..413f4fdd 100644
--- a/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts
+++ b/addons/xterm-addon-webgl/src/renderLayer/CursorRenderLayer.ts
@@ -8,7 +8,8 @@ import { BaseRenderLayer } from './BaseRenderLayer';
import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { IColorSet } from 'browser/Types';
-import { IRenderDimensions } from 'browser/renderer/Types';
+import { IRenderDimensions, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
+import { IEventEmitter } from 'common/EventEmitter';
interface ICursorState {
x: number;
@@ -29,7 +30,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _cursorBlinkStateManager: CursorBlinkStateManager | undefined;
private _cell: ICellData = new CellData();
- constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
+ constructor(
+ container: HTMLElement,
+ zIndex: number,
+ colors: IColorSet,
+ private _onRequestRefreshRowsEvent: IEventEmitter
+ ) {
super(container, 'cursor', zIndex, true, colors);
this._state = {
x: 0,
@@ -70,14 +76,14 @@ export class CursorRenderLayer extends BaseRenderLayer {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.pause();
}
- terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY);
+ this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY });
}
public onFocus(terminal: Terminal): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.resume(terminal);
} else {
- terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY);
+ this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY });
}
}
@@ -89,13 +95,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
});
}
} else {
- if (this._cursorBlinkStateManager) {
- this._cursorBlinkStateManager.dispose();
- }
- // Request a refresh from the terminal as management of rendering is being
- // moved back to the terminal
- terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY);
+ this._cursorBlinkStateManager?.dispose();
+ this._cursorBlinkStateManager = undefined;
}
+ // Request a refresh from the terminal as management of rendering is being
+ // moved back to the terminal
+ this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY });
}
public onCursorMove(terminal: Terminal): void {
@@ -115,7 +120,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _render(terminal: Terminal, triggeredByAnimationFrame: boolean): void {
// Don't draw the cursor if it's hidden
// TODO: Need to expose API for this
- if (!(terminal as any)._core.cursorState || (terminal as any)._core.cursorHidden) {
+ if (!(terminal as any)._core._coreService.isCursorInitialized || (terminal as any)._core._coreService.isCursorHidden) {
this._clearCursor();
return;
}
@@ -139,12 +144,17 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._clearCursor();
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
- this._renderBlurCursor(terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell);
+ const cursorStyle = terminal.getOption('cursorStyle');
+ if (cursorStyle && cursorStyle !== 'block') {
+ this._cursorRenderers[cursorStyle](terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell);
+ } else {
+ this._renderBlurCursor(terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell);
+ }
this._ctx.restore();
this._state.x = terminal.buffer.cursorX;
this._state.y = viewportRelativeCursorY;
this._state.isFocused = false;
- this._state.style = terminal.getOption('cursorStyle');
+ this._state.style = cursorStyle;
this._state.width = this._cell.getWidth();
return;
}
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index c784a611..f6f892c7 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -7,59 +7,78 @@ jobs:
- job: Linux
pool:
vmImage: 'ubuntu-16.04'
+ variables:
+ PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
+ - task: YarnInstaller@3
inputs:
- versionSpec: "1.9.4"
+ versionSpec: '1.x'
displayName: 'Install Yarn'
- - script: |
- yarn
+ - task: CacheBeta@1
+ inputs:
+ key: yarn2 | $(Agent.OS) | yarn.lock
+ path: node_modules
+ displayName: Cache node modules
+ - script: yarn --frozen-lockfile
displayName: 'Install dependencies and build'
- script: |
- yarn test-unit
+ yarn test-unit-coverage --forbid-only
+ ./node_modules/.bin/nyc report --reporter=cobertura
displayName: 'Unit tests'
- - script: |
- yarn lint
+ - script: yarn lint
displayName: 'Lint'
+ - task: PublishCodeCoverageResults@1
+ inputs:
+ codeCoverageTool: Cobertura
+ summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/*coverage.xml'
+ displayName: 'Publish coverage'
- job: macOS
pool:
vmImage: 'xcode9-macos10.13'
+ variables:
+ PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- - script: |
- yarn
+ - task: CacheBeta@1
+ inputs:
+ key: yarn2 | $(Agent.OS) | yarn.lock
+ path: node_modules
+ displayName: Cache node modules
+ - script: yarn --frozen-lockfile
displayName: 'Install dependencies and build'
- - script: |
- yarn test-unit
+ - script: yarn test-unit --forbid-only
displayName: 'Unit tests'
- - script: |
- yarn lint
+ - script: yarn lint
displayName: 'Lint'
- job: Windows
pool:
vmImage: 'vs2017-win2016'
+ variables:
+ PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- - script: |
- yarn
+ - task: CacheBeta@1
+ inputs:
+ key: yarn2 | $(Agent.OS) | yarn.lock
+ path: node_modules
+ displayName: Cache node modules
+ - script: yarn --frozen-lockfile
displayName: 'Install dependencies and build'
- - script: |
- yarn test-unit
+ - script: yarn test-unit --forbid-only
displayName: 'Unit tests'
- - script: |
- yarn lint
+ - script: yarn lint
displayName: 'Lint'
- job: Linux_IntegrationTests
@@ -70,17 +89,16 @@ jobs:
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
+ - task: YarnInstaller@3
inputs:
- versionSpec: "1.9.4"
+ versionSpec: '1.x'
displayName: 'Install Yarn'
- - script: |
- yarn
+ - script: yarn --frozen-lockfile
displayName: 'Install dependencies and build'
- script: |
yarn start &
sleep 10
- yarn test-api --headless
+ yarn test-api --headless --forbid-only
displayName: 'Linux Integration tests'
- job: macOS_IntegrationTests
@@ -91,13 +109,12 @@ jobs:
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- - script: |
- yarn
+ - script: yarn --frozen-lockfile
displayName: 'Install dependencies and build'
- script: |
yarn start &
sleep 10
- yarn test-api --headless
+ yarn test-api --headless --forbid-only
displayName: 'MacOS Integration tests'
- job: Release
@@ -107,22 +124,26 @@ jobs:
- Windows
- Linux_IntegrationTests
- macOS_IntegrationTests
- condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/*')))
+ condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true')))
pool:
vmImage: 'ubuntu-16.04'
+ variables:
+ PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
steps:
- task: NodeTool@0
inputs:
versionSpec: '8.x'
displayName: 'Install Node.js'
- - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
+ - task: YarnInstaller@3
inputs:
- versionSpec: "1.9.4"
+ versionSpec: '1.x'
displayName: 'Install Yarn'
- - script: |
- yarn
- BUILD_DIR=dist npm run build
+ - task: CacheBeta@1
+ inputs:
+ key: yarn2 | $(Agent.OS) | yarn.lock
+ path: node_modules
+ displayName: Cache node modules
+ - script: yarn --frozen-lockfile
displayName: 'Install dependencies and build'
- - script: |
- NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js
- displayName: 'Publish to npm'
+ - script: NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js
+ displayName: 'Package and publish to npm'
diff --git a/bin/install-addons.js b/bin/install-addons.js
new file mode 100644
index 00000000..83adea1f
--- /dev/null
+++ b/bin/install-addons.js
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ *
+ * Script to initialize addon packages under "addons/" with outer deps.
+ */
+
+const path = require('path');
+const cp = require('child_process');
+const fs = require('fs');
+
+const PACKAGE_ROOT = path.join(__dirname, '..');
+
+// install addon deps
+const addonsPath = path.join(PACKAGE_ROOT, 'addons');
+if (fs.existsSync(addonsPath)) {
+ console.log('pulling addon dependencies...');
+
+ // whether to use yarn or npm
+ let hasYarn = false;
+ try {
+ cp.execSync('yarn --version').toString();
+ hasYarn = true;
+ } catch(e) {}
+
+ // walk all addon folders
+ fs.readdir(addonsPath, (err, files) => {
+ files.forEach(folder => {
+ const addonPath = path.join(addonsPath, folder);
+
+ // install only if there are dependencies listed
+ let packageJson;
+ try {
+ packageJson = require(path.join(addonPath, 'package.json'));
+ } catch (e) {
+ // swallow as changing branches can leave folders around
+ }
+ if (packageJson
+ && (
+ (packageJson.devDependencies && Object.keys(packageJson.devDependencies).length)
+ || (packageJson.dependencies && Object.keys(packageJson.dependencies).length)
+ )
+ )
+ {
+ console.log('Preparing', folder);
+ if (hasYarn) {
+ cp.execSync('yarn', {cwd: addonPath});
+ } else {
+ cp.execSync('npm install', {cwd: addonPath});
+ }
+ } else {
+ console.log('Skipped', folder);
+ }
+ });
+ });
+}
diff --git a/bin/publish.js b/bin/publish.js
index 0610d116..9050c344 100644
--- a/bin/publish.js
+++ b/bin/publish.js
@@ -5,6 +5,7 @@
const cp = require('child_process');
const fs = require('fs');
+const os = require('os');
const path = require('path');
// Setup auth
@@ -18,8 +19,9 @@ if (isDryRun) {
const changedFiles = getChangedFilesInCommit('HEAD');
// Publish xterm if any files were changed outside of the addons directory
+let isStableRelease = false;
if (changedFiles.some(e => e.search(/^addons\//) === -1)) {
- checkAndPublishPackage(path.resolve(__dirname, '..'));
+ isStableRelease = checkAndPublishPackage(path.resolve(__dirname, '..'));
}
// Publish addons if any files were changed inside of the addon
@@ -30,13 +32,20 @@ const addonPackageDirs = [
path.resolve(__dirname, '../addons/xterm-addon-web-links'),
path.resolve(__dirname, '../addons/xterm-addon-webgl')
];
+console.log(`Checking if addons need to be published`);
addonPackageDirs.forEach(p => {
const addon = path.basename(p);
if (changedFiles.some(e => e.indexOf(addon) !== -1)) {
+ console.log(`Try publish ${addon}`);
checkAndPublishPackage(p);
}
});
+// Publish website if it's a stable release
+if (isStableRelease) {
+ updateWebsite();
+}
+
function checkAndPublishPackage(packageDir) {
const packageJson = require(path.join(packageDir, 'package.json'));
@@ -74,6 +83,8 @@ function checkAndPublishPackage(packageDir) {
}
console.groupEnd();
+
+ return isStableRelease;
}
function getNextBetaVersion(packageJson) {
@@ -86,7 +97,7 @@ function getNextBetaVersion(packageJson) {
const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`;
const publishedVersions = getPublishedVersions(packageJson, nextStableVersion, tag);
if (publishedVersions.length === 0) {
- return `${nextStableVersion}-${tag}1`;
+ return `${nextStableVersion}-${tag}.1`;
}
const latestPublishedVersion = publishedVersions.sort((a, b) => {
const aVersion = parseInt(a.substr(a.search(/[0-9]+$/)));
@@ -94,14 +105,14 @@ function getNextBetaVersion(packageJson) {
return aVersion > bVersion ? -1 : 1;
})[0];
const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/[0-9]+$/)), 10);
- return `${nextStableVersion}-${tag}${latestTagVersion + 1}`;
+ return `${nextStableVersion}-${tag}.${latestTagVersion + 1}`;
}
function getPublishedVersions(packageJson, version, tag) {
const versionsProcess = cp.spawnSync('npm', ['view', packageJson.name, 'versions', '--json']);
const versionsJson = JSON.parse(versionsProcess.stdout);
if (tag) {
- return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}[0-9]+`)));
+ return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}.[0-9]+`)));
}
return versionsJson;
}
@@ -113,3 +124,12 @@ function getChangedFilesInCommit(commit) {
const changedFiles = output.split('\n').filter(e => e.length > 0);
return changedFiles;
}
+
+function updateWebsite() {
+ console.log('Updating website');
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'website-'));
+ const packageJson = require(path.join(path.resolve(__dirname, '..'), 'package.json'));
+ if (!isDryRun) {
+ cp.spawnSync('sh', [path.join(__dirname, 'update-website.sh'), packageJson.version], { cwd, stdio: [process.stdin, process.stdout, process.stderr] });
+ }
+}
diff --git a/bin/test.js b/bin/test.js
index 7beb4f9a..1380a512 100644
--- a/bin/test.js
+++ b/bin/test.js
@@ -6,6 +6,8 @@
const cp = require('child_process');
const path = require('path');
+const COVERAGE_LINES_THRESHOLD = 60;
+
// Add `out` to the NODE_PATH so absolute paths can be resolved.
const env = { ...process.env };
env.NODE_PATH = path.resolve(__dirname, '../out');
@@ -16,15 +18,42 @@ let testFiles = [
'./addons/**/out/*test.js',
];
-// ability to inject particular test files via
-// yarn test [testFileA testFileB ...]
+let flagArgs = [];
+
if (process.argv.length > 2) {
- testFiles = process.argv.slice(2);
+ const args = process.argv.slice(2);
+ flagArgs = args.filter(e => e.startsWith('--'));
+ // ability to inject particular test files via
+ // yarn test [testFileA testFileB ...]
+ files = args.filter(e => !e.startsWith('--'));
+ if (files.length) {
+ testFiles = files;
+ }
+}
+
+const checkCoverage = flagArgs.indexOf('--coverage') >= 0;
+
+if (checkCoverage) {
+ flagArgs.splice(flagArgs.indexOf('--coverage'), 1);
+ const executable = npmBinScript('nyc');
+ const args = ['--check-coverage', `--lines=${COVERAGE_LINES_THRESHOLD}`, npmBinScript('mocha'), ...testFiles, ...flagArgs];
+ console.info('executable', executable);
+ console.info('args', args);
+ const run = cp.spawnSync(
+ executable,
+ args,
+ {
+ cwd: path.resolve(__dirname, '..'),
+ env,
+ stdio: 'inherit'
+ }
+ );
+ process.exit(run.status);
}
const run = cp.spawnSync(
- path.resolve(__dirname, '../node_modules/.bin/mocha'),
- testFiles,
+ npmBinScript('mocha'),
+ [...testFiles, ...flagArgs],
{
cwd: path.resolve(__dirname, '..'),
env,
@@ -32,4 +61,8 @@ const run = cp.spawnSync(
}
);
-process.exit(run.status);
\ No newline at end of file
+function npmBinScript(script) {
+ return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ? `${script}.cmd` : script));
+}
+
+process.exit(run.status);
diff --git a/bin/update-website.sh b/bin/update-website.sh
new file mode 100644
index 00000000..4379d915
--- /dev/null
+++ b/bin/update-website.sh
@@ -0,0 +1,41 @@
+#!/bin/sh
+
+# Name the arguments
+VERSION=$1
+
+# Clone docs repo and update the documentation
+git clone https://github.com/xtermjs/xtermjs.org
+cd xtermjs.org
+yarn
+./bin/update-docs
+
+# Add changes to index and only proceed if there are changes to commit
+touch test-file
+git add .
+if ! git diff-index --quiet HEAD --; then
+
+ # Delete the upstream branch if it exists for some reason
+ export BRANCH_NAME=update-$VERSION
+ git branch -D $BRANCH_NAME || true
+ git push origin :$BRANCH_NAME || true
+
+ # Create commit and push it to update-x.y.z
+ git checkout -b $BRANCH_NAME
+ git config --global user.name Daniel Imms
+ git config --global user.email tyriar@tyriar.com
+ git commit -m 'Update docs for v$VERSION'
+ git push --set-upstream origin update-4.2.0
+ git push -f
+
+ # Create a PR in the GitHub repo
+ curl \
+ -H "Authorization: token $GITHUB_TOKEN" \
+ -X POST \
+ -d "{\"title\":\"Update docs for v$VERSION\",\"base\":\"master\",\"head\":\"xtermjs:$BRANCH_NAME\"}" \
+ https://api.github.com/repos/xtermjs/xtermjs.org/pulls
+
+else
+
+ echo "No changes to commit"
+
+fi
diff --git a/demo/client.ts b/demo/client.ts
index 91c5d33d..e045dc43 100644
--- a/demo/client.ts
+++ b/demo/client.ts
@@ -12,9 +12,9 @@ import { Terminal } from '../out/public/Terminal';
import { AttachAddon } from '../addons/xterm-addon-attach/out/AttachAddon';
import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon';
import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon';
+import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAddon';
import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon';
import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon';
-import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAddon';
// Use webpacked version (yarn package)
// import { Terminal } from '../lib/xterm';
@@ -34,21 +34,49 @@ export interface IWindowWithTerminal extends Window {
AttachAddon?: typeof AttachAddon;
FitAddon?: typeof FitAddon;
SearchAddon?: typeof SearchAddon;
+ SerializeAddon?: typeof SerializeAddon;
WebLinksAddon?: typeof WebLinksAddon;
WebglAddon?: typeof WebglAddon;
- SerializeAddon?: typeof SerializeAddon;
}
declare let window: IWindowWithTerminal;
let term;
-let fitAddon: FitAddon;
-let searchAddon: SearchAddon;
-let serializeAddon: SerializeAddon;
let protocol;
let socketURL;
let socket;
let pid;
+type AddonType = 'attach' | 'fit' | 'search' | 'serialize' | 'web-links' | 'webgl';
+
+interface IDemoAddon {
+ name: T;
+ canChange: boolean;
+ ctor:
+ T extends 'attach' ? typeof AttachAddon :
+ T extends 'fit' ? typeof FitAddon :
+ T extends 'search' ? typeof SearchAddon :
+ T extends 'serialize' ? typeof SerializeAddon :
+ T extends 'web-links' ? typeof WebLinksAddon :
+ typeof WebglAddon;
+ instance?:
+ T extends 'attach' ? AttachAddon :
+ T extends 'fit' ? FitAddon :
+ T extends 'search' ? SearchAddon :
+ T extends 'serialize' ? SerializeAddon :
+ T extends 'web-links' ? WebLinksAddon :
+ T extends 'webgl' ? WebglAddon :
+ never;
+}
+
+const addons: { [T in AddonType]: IDemoAddon} = {
+ attach: { name: 'attach', ctor: AttachAddon, canChange: false },
+ fit: { name: 'fit', ctor: FitAddon, canChange: false },
+ search: { name: 'search', ctor: SearchAddon, canChange: true },
+ serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true },
+ 'web-links': { name: 'web-links', ctor: WebLinksAddon, canChange: true },
+ webgl: { name: 'webgl', ctor: WebglAddon, canChange: true }
+};
+
const terminalContainer = document.getElementById('terminal-container');
const actionElements = {
findNext: document.querySelector('#find-next'),
@@ -96,7 +124,6 @@ if (document.location.pathname === '/test') {
} else {
createTerminal();
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
- document.getElementById('webgl').addEventListener('click', () => term.loadAddon(new WebglAddon()));
document.getElementById('serialize').addEventListener('click', serializeButtonHandler);
}
@@ -113,13 +140,14 @@ function createTerminal(): void {
// Load addons
const typedTerm = term as TerminalType;
- typedTerm.loadAddon(new WebLinksAddon());
- searchAddon = new SearchAddon();
- typedTerm.loadAddon(searchAddon);
- fitAddon = new FitAddon();
- typedTerm.loadAddon(fitAddon);
- serializeAddon = new SerializeAddon();
- typedTerm.loadAddon(serializeAddon);
+ addons.search.instance = new SearchAddon();
+ addons.serialize.instance = new SerializeAddon();
+ addons.fit.instance = new FitAddon();
+ addons['web-links'].instance = new WebLinksAddon();
+ typedTerm.loadAddon(addons.fit.instance);
+ typedTerm.loadAddon(addons.search.instance);
+ typedTerm.loadAddon(addons.serialize.instance);
+ typedTerm.loadAddon(addons['web-links'].instance);
window.term = term; // Expose `term` to window for debugging purposes
term.onResize((size: { cols: number, rows: number }) => {
@@ -136,17 +164,17 @@ function createTerminal(): void {
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
term.open(terminalContainer);
- fitAddon.fit();
+ addons.fit.instance!.fit();
term.focus();
addDomListener(paddingElement, 'change', setPadding);
addDomListener(actionElements.findNext, 'keyup', (e) => {
- searchAddon.findNext(actionElements.findNext.value, getSearchOptions(e));
+ addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions(e));
});
addDomListener(actionElements.findPrevious, 'keyup', (e) => {
- searchAddon.findPrevious(actionElements.findPrevious.value, getSearchOptions(e));
+ addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions(e));
});
// fit is called within a setTimeout, cols and rows need this.
@@ -174,15 +202,10 @@ function createTerminal(): void {
}
function runRealTerminal(): void {
- /**
- * The demo defaults to string transport by default.
- * To run it with UTF8 binary transport, swap comment on
- * the lines below. (Must also be switched in server.js)
- */
- term.loadAddon(new AttachAddon(socket));
- // term.loadAddon(new AttachAddon(socket, {inputUtf8: true}));
-
+ addons.attach.instance = new AttachAddon(socket);
+ term.loadAddon(addons.attach.instance);
term._initialized = true;
+ initAddons(term);
}
function runFakeTerminal(): void {
@@ -191,6 +214,7 @@ function runFakeTerminal(): void {
}
term._initialized = true;
+ initAddons(term);
term.prompt = () => {
term.write('\r\n$ ');
@@ -235,6 +259,7 @@ function initOptions(term: TerminalType): void {
bellSound: null,
bellStyle: ['none', 'sound'],
cursorStyle: ['block', 'underline', 'bar'],
+ fastScrollModifier: ['alt', 'ctrl', 'shift', undefined],
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'],
@@ -267,7 +292,7 @@ function initOptions(term: TerminalType): void {
});
html += '
';
numberOptions.forEach(o => {
- html += ``;
+ html += ``;
});
html += '
These options can be set in the Terminal constructor or using the Terminal.setOption function.
+
+
Addons
+
Addons can be loaded and unloaded on a particular terminal to extend its functionality.
+
+
Style
@@ -42,7 +47,6 @@
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/server.js b/demo/server.js
index 8955431b..85e1aca7 100644
--- a/demo/server.js
+++ b/demo/server.js
@@ -3,12 +3,8 @@ var expressWs = require('express-ws');
var os = require('os');
var pty = require('node-pty');
-/**
- * Whether to use UTF8 binary transport.
- * (Must also be switched in client.ts)
- */
-const USE_BINARY_UTF8 = false;
-
+// Whether to use binary transport.
+const USE_BINARY = os.platform() !== "win32";
function startServer() {
var app = express();
@@ -18,25 +14,26 @@ function startServer() {
logs = {};
app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css'));
- app.get('/logo.png', (req, res) => res.sendFile(__dirname + '/logo.png'));
+ app.get('/logo.png', (req, res) => { // lgtm [js/missing-rate-limiting]
+ res.sendFile(__dirname + '/logo.png');
+ });
- app.get('/', function(req, res){
+ app.get('/', (req, res) => { // lgtm [js/missing-rate-limiting]
res.sendFile(__dirname + '/index.html');
});
- app.get('/test', function(req, res){
+ app.get('/test', (req, res) => { // lgtm [js/missing-rate-limiting]
res.sendFile(__dirname + '/test.html');
});
- app.get('/style.css', function(req, res){
+ app.get('/style.css', (req, res) => { // lgtm [js/missing-rate-limiting]
res.sendFile(__dirname + '/style.css');
});
- app.get('/dist/client-bundle.js', function(req, res){
- res.sendFile(__dirname + '/dist/client-bundle.js');
- });
+ app.use('/dist', express.static(__dirname + '/dist'));
+ app.use('/src', express.static(__dirname + '/src'));
- app.post('/terminals', function (req, res) {
+ app.post('/terminals', (req, res) => {
const env = Object.assign({}, process.env);
env['COLORTERM'] = 'truecolor';
var cols = parseInt(req.query.cols),
@@ -47,7 +44,7 @@ function startServer() {
rows: rows || 24,
cwd: env.PWD,
env: env,
- encoding: USE_BINARY_UTF8 ? null : 'utf8'
+ encoding: USE_BINARY ? null : 'utf8'
});
console.log('Created terminal with PID: ' + term.pid);
@@ -60,7 +57,7 @@ function startServer() {
res.end();
});
- app.post('/terminals/:pid/size', function (req, res) {
+ app.post('/terminals/:pid/size', (req, res) => {
var pid = parseInt(req.params.pid),
cols = parseInt(req.query.cols),
rows = parseInt(req.query.rows),
@@ -109,7 +106,7 @@ function startServer() {
}
};
}
- const send = USE_BINARY_UTF8 ? bufferUtf8(ws, 5) : buffer(ws, 5);
+ const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5);
term.on('data', function(data) {
try {
diff --git a/demo/start.js b/demo/start.js
index 64f41805..8ffedb42 100644
--- a/demo/start.js
+++ b/demo/start.js
@@ -20,9 +20,6 @@ startServer();
* For production builds see `webpack.config.js` in the root directory. If that is built the demo
* can use that by switching out which `Terminal` is imported in `client.ts`, this is useful for
* validating that the packaged version works correctly.
- *
- * The addons are not webpacked right now and are built directly to `lib/` via `tsc` as they are
- * the legacy format (`applyAddon`) and will be removed soon anyway.
*/
const clientConfig = {
entry: path.resolve(__dirname, 'client.ts'),
diff --git a/demo/tsconfig.json b/demo/tsconfig.json
index 28ea5262..4e1b5035 100644
--- a/demo/tsconfig.json
+++ b/demo/tsconfig.json
@@ -9,8 +9,9 @@
"xterm-addon-attach": ["../addons/xterm-addon-attach"],
"xterm-addon-fit": ["../addons/xterm-addon-fit"],
"xterm-addon-search": ["../addons/xterm-addon-search"],
+ "xterm-addon-serialize": ["../addons/xterm-addon-serialize"],
"xterm-addon-web-links": ["../addons/xterm-addon-web-links"],
- "xterm-addon-serialize": ["../addons/xterm-addon-serialize"]
+ "xterm-addon-webgl": ["../addons/xterm-addon-webgl"]
}
},
"include": [
diff --git a/fixtures/escape_sequence_files/t0070-DECSTBM_LF.in b/fixtures/escape_sequence_files/t0070-DECSTBM_LF.in
index 571f2233..c56b99a0 100644
--- a/fixtures/escape_sequence_files/t0070-DECSTBM_LF.in
+++ b/fixtures/escape_sequence_files/t0070-DECSTBM_LF.in
@@ -27,5 +27,5 @@ r
s
t
u[78Gvwxyz
-[r[24d
+[r[25d
The end.
diff --git a/fixtures/escape_sequence_files/t0070-DECSTBM_LF.text b/fixtures/escape_sequence_files/t0070-DECSTBM_LF.text
index 0940b073..940bc86d 100644
--- a/fixtures/escape_sequence_files/t0070-DECSTBM_LF.text
+++ b/fixtures/escape_sequence_files/t0070-DECSTBM_LF.text
@@ -1,25 +1,25 @@
-1
- 2
- 6
- 7
- 8
- 9 ABC
+6
+7
+8
+9 ABC
DEF
a
- b
- c
- d
- e
- f
- g
- h
- i
- j
- k
- l
- m
- n
- o
- p
-yz qrstu vwx
+b
+c
+d
+e
+f
+g
+h
+i
+j
+k
+l
+m
+n
+o
+p
+q
+yz vwx
The end.
+
diff --git a/fixtures/escape_sequence_files/t0071-DECSTBM_IND.in b/fixtures/escape_sequence_files/t0071-DECSTBM_IND.in
index f9f6a424..a6aa1810 100644
--- a/fixtures/escape_sequence_files/t0071-DECSTBM_IND.in
+++ b/fixtures/escape_sequence_files/t0071-DECSTBM_IND.in
@@ -1,3 +1,3 @@
[3;7r1D2D3D4D5D6D7D8D9[78GABCDEF[8daDbDcDdDeDfDgDhDiDjDkDlDmDnDoDpDqDrDsDtDu[78Gvwxyz
-[r[24d
+[r[25d
The end.
diff --git a/fixtures/escape_sequence_files/t0071-DECSTBM_IND.text b/fixtures/escape_sequence_files/t0071-DECSTBM_IND.text
index f6644fb5..bc7344f9 100644
--- a/fixtures/escape_sequence_files/t0071-DECSTBM_IND.text
+++ b/fixtures/escape_sequence_files/t0071-DECSTBM_IND.text
@@ -1,4 +1,3 @@
- 2
6
7
8
@@ -21,5 +20,6 @@ DEF
o
p
q
-The end. rstu vwx
+yz rstu vwx
+The end.
diff --git a/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text b/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text
index f89893ba..92c10331 100644
--- a/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text
+++ b/fixtures/escape_sequence_files/t0076-DECSTBM_IL_DL.text
@@ -1,4 +1,3 @@
- 6 C
8 ^^^^
9 vvvv DL on line 11, expected: ACD_
10 A
@@ -13,14 +12,14 @@
19 vvvv IL on line 21, expected: A_
20 A
-
22 ^^^^
-24 A
-25 B
-27 vvvv DL on line 28, expected: B_
+23 vvvv IL on line 24, expected: _A
+25 B
+26 ^^^^
28 A
+
29 B
30 ^^^^
31
-32
\ No newline at end of file
+32
diff --git a/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.in b/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.in
new file mode 100644
index 00000000..b279f92a
--- /dev/null
+++ b/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.in
@@ -0,0 +1,24 @@
+a
+b
+c
+d
+e
+f
+g
+h
+i
+j
+k
+l
+m
+n
+o
+p
+q
+r
+s
+t
+u
+v
+w
+x[10;19r[25E1[24d[25F2[r[24d
diff --git a/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.text b/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.text
new file mode 100644
index 00000000..3acccf1a
--- /dev/null
+++ b/fixtures/escape_sequence_files/t0078-DECSTBM_CPL_CNL.text
@@ -0,0 +1,25 @@
+a
+b
+c
+d
+e
+f
+g
+h
+i
+2
+k
+l
+m
+n
+o
+p
+q
+r
+1
+t
+u
+v
+w
+x
+
diff --git a/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.in b/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.in
new file mode 100644
index 00000000..f818ad7f
--- /dev/null
+++ b/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.in
@@ -0,0 +1,24 @@
+a
+b
+c
+d
+e
+f
+g
+h
+i
+j
+k
+l
+m
+n
+o
+p
+q
+r
+s
+t
+u
+v
+w
+x[10;19r[H[25e1[r[24d
diff --git a/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.text b/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.text
new file mode 100644
index 00000000..a2d66f8e
--- /dev/null
+++ b/fixtures/escape_sequence_files/t0079-DECSTBM_VPR.text
@@ -0,0 +1,25 @@
+a
+b
+c
+d
+e
+f
+g
+h
+i
+j
+k
+l
+m
+n
+o
+p
+q
+r
+s
+t
+u
+v
+w
+x
+1
diff --git a/fixtures/escape_sequence_files/t600-DECSTBM_SR.in b/fixtures/escape_sequence_files/t600-DECSTBM_SR.in
new file mode 100644
index 00000000..9fa002fc
--- /dev/null
+++ b/fixtures/escape_sequence_files/t600-DECSTBM_SR.in
@@ -0,0 +1,25 @@
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB[10;15r[5;5H[5 A[12;12H[5 A[20;20H[5 A
\ No newline at end of file
diff --git a/fixtures/escape_sequence_files/t600-DECSTBM_SR.text b/fixtures/escape_sequence_files/t600-DECSTBM_SR.text
new file mode 100644
index 00000000..8592dd1b
--- /dev/null
+++ b/fixtures/escape_sequence_files/t600-DECSTBM_SR.text
@@ -0,0 +1,25 @@
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
+ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
+ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
+ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
+ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
+ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
diff --git a/fixtures/escape_sequence_files/t601-DECSTBM_SL.in b/fixtures/escape_sequence_files/t601-DECSTBM_SL.in
new file mode 100644
index 00000000..e99f5190
--- /dev/null
+++ b/fixtures/escape_sequence_files/t601-DECSTBM_SL.in
@@ -0,0 +1,25 @@
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB[10;15r[5;5H[5 @[12;12H[5 @[20;20H[5 @
\ No newline at end of file
diff --git a/fixtures/escape_sequence_files/t601-DECSTBM_SL.text b/fixtures/escape_sequence_files/t601-DECSTBM_SL.text
new file mode 100644
index 00000000..1912cb56
--- /dev/null
+++ b/fixtures/escape_sequence_files/t601-DECSTBM_SL.text
@@ -0,0 +1,25 @@
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
diff --git a/fixtures/escape_sequence_files/t602-DECSTBM_DECIC.in b/fixtures/escape_sequence_files/t602-DECSTBM_DECIC.in
new file mode 100644
index 00000000..e31f4e12
--- /dev/null
+++ b/fixtures/escape_sequence_files/t602-DECSTBM_DECIC.in
@@ -0,0 +1,25 @@
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB[10;15r[5;5H[5'}[12;12H[5'}[20;20H[5'}
\ No newline at end of file
diff --git a/fixtures/escape_sequence_files/t602-DECSTBM_DECIC.text b/fixtures/escape_sequence_files/t602-DECSTBM_DECIC.text
new file mode 100644
index 00000000..f3fa17bc
--- /dev/null
+++ b/fixtures/escape_sequence_files/t602-DECSTBM_DECIC.text
@@ -0,0 +1,25 @@
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
+abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
+abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
+abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
+abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
+abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
diff --git a/fixtures/escape_sequence_files/t603-DECSTBM_DECDC.in b/fixtures/escape_sequence_files/t603-DECSTBM_DECDC.in
new file mode 100644
index 00000000..ecb966d9
--- /dev/null
+++ b/fixtures/escape_sequence_files/t603-DECSTBM_DECDC.in
@@ -0,0 +1,25 @@
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB[10;15r[5;5H[5'~[12;12H[5'~[20;20H[5'~
\ No newline at end of file
diff --git a/fixtures/escape_sequence_files/t603-DECSTBM_DECDC.text b/fixtures/escape_sequence_files/t603-DECSTBM_DECDC.text
new file mode 100644
index 00000000..b98db23c
--- /dev/null
+++ b/fixtures/escape_sequence_files/t603-DECSTBM_DECDC.text
@@ -0,0 +1,25 @@
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
diff --git a/package.json b/package.json
index 19ccf0d9..fe98b468 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
- "version": "3.14.0",
+ "version": "4.2.0",
"main": "lib/xterm.js",
"style": "css/xterm.css",
"types": "typings/xterm.d.ts",
@@ -11,13 +11,16 @@
"prepackage": "npm run build",
"package": "webpack",
"start": "node demo/start",
- "lint": "tslint 'src/**/*.ts' 'addons/**/*.ts'",
+ "lint": "tslint 'src/**/*.ts' 'addons/*/src/**/*.ts'",
"test": "npm run test-unit",
"posttest": "npm run lint",
"test-api": "mocha \"**/*.api.js\"",
"test-unit": "node ./bin/test.js",
+ "test-unit-coverage": "node ./bin/test.js --coverage",
"build": "tsc -b ./tsconfig.all.json",
- "prepare": "npm run build",
+ "prepare": "npm run setup",
+ "setup": "npm run build",
+ "presetup": "node ./bin/install-addons.js",
"prepublishOnly": "npm run package",
"watch": "tsc -b -w ./tsconfig.all.json --preserveWatchOutput",
"benchmark": "NODE_PATH=./out:./out-test/benchmark xterm-benchmark -r 5 -c test/benchmark/benchmark.json",
@@ -27,6 +30,7 @@
},
"devDependencies": {
"@types/chai": "^3.4.34",
+ "@types/deep-equal": "^1.0.1",
"@types/glob": "^5.0.35",
"@types/jsdom": "11.0.1",
"@types/mocha": "^2.2.33",
@@ -36,18 +40,20 @@
"@types/webpack": "^4.4.11",
"@types/ws": "^6.0.1",
"chai": "3.5.0",
+ "deep-equal": "^1.1.0",
"express": "^4.17.1",
"express-ws": "^4.0.0",
"glob": "^7.0.5",
"jsdom": "^11.11.0",
"mocha": "^6.1.4",
"node-pty": "0.7.6",
+ "nyc": "13",
"puppeteer": "^1.15.0",
"source-map-loader": "^0.2.4",
"ts-loader": "^6.0.4",
"tslint": "^5.18.0",
"tslint-consistent-codestyle": "^1.13.0",
- "typescript": "3.5",
+ "typescript": "3.7",
"utf8": "^3.0.0",
"v8-profiler-node8": "^6.1.1",
"webpack": "^4.35.3",
diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts
index 38645a0b..d8defa1c 100644
--- a/src/InputHandler.test.ts
+++ b/src/InputHandler.test.ts
@@ -13,7 +13,7 @@ import { CellData } from 'common/buffer/CellData';
import { Attributes } from 'common/buffer/Constants';
import { AttributeData } from 'common/buffer/AttributeData';
import { Params } from 'common/parser/Params';
-import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService } from 'common/TestUtils.test';
+import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService } from 'common/TestUtils.test';
import { IBufferService } from 'common/services/Services';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { clone } from 'common/Clone';
@@ -25,6 +25,14 @@ function getCursor(term: TestTerminal): number[] {
];
}
+function getLines(term: TestTerminal, limit: number = term.rows): string[] {
+ const res: string[] = [];
+ for (let i = 0; i < limit; ++i) {
+ res.push(term.buffer.lines.get(i).translateToString(true));
+ }
+ return res;
+}
+
describe('InputHandler', () => {
describe('save and restore cursor', () => {
const terminal = new MockInputHandlingTerminal();
@@ -33,7 +41,7 @@ describe('InputHandler', () => {
bufferService.buffer.x = 1;
bufferService.buffer.y = 2;
bufferService.buffer.ybase = 0;
- const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
+ const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
// Save cursor position
inputHandler.saveCursor();
assert.equal(bufferService.buffer.x, 1);
@@ -52,40 +60,39 @@ describe('InputHandler', () => {
describe('setCursorStyle', () => {
it('should call Terminal.setOption with correct params', () => {
const optionsService = new MockOptionsService();
- const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService);
- const collect = ' ';
+ const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService());
- inputHandler.setCursorStyle(Params.fromArray([0]), collect);
+ inputHandler.setCursorStyle(Params.fromArray([0]));
assert.equal(optionsService.options['cursorStyle'], 'block');
assert.equal(optionsService.options['cursorBlink'], true);
optionsService.options = clone(DEFAULT_OPTIONS);
- inputHandler.setCursorStyle(Params.fromArray([1]), collect);
+ inputHandler.setCursorStyle(Params.fromArray([1]));
assert.equal(optionsService.options['cursorStyle'], 'block');
assert.equal(optionsService.options['cursorBlink'], true);
optionsService.options = clone(DEFAULT_OPTIONS);
- inputHandler.setCursorStyle(Params.fromArray([2]), collect);
+ inputHandler.setCursorStyle(Params.fromArray([2]));
assert.equal(optionsService.options['cursorStyle'], 'block');
assert.equal(optionsService.options['cursorBlink'], false);
optionsService.options = clone(DEFAULT_OPTIONS);
- inputHandler.setCursorStyle(Params.fromArray([3]), collect);
+ inputHandler.setCursorStyle(Params.fromArray([3]));
assert.equal(optionsService.options['cursorStyle'], 'underline');
assert.equal(optionsService.options['cursorBlink'], true);
optionsService.options = clone(DEFAULT_OPTIONS);
- inputHandler.setCursorStyle(Params.fromArray([4]), collect);
+ inputHandler.setCursorStyle(Params.fromArray([4]));
assert.equal(optionsService.options['cursorStyle'], 'underline');
assert.equal(optionsService.options['cursorBlink'], false);
optionsService.options = clone(DEFAULT_OPTIONS);
- inputHandler.setCursorStyle(Params.fromArray([5]), collect);
+ inputHandler.setCursorStyle(Params.fromArray([5]));
assert.equal(optionsService.options['cursorStyle'], 'bar');
assert.equal(optionsService.options['cursorBlink'], true);
optionsService.options = clone(DEFAULT_OPTIONS);
- inputHandler.setCursorStyle(Params.fromArray([6]), collect);
+ inputHandler.setCursorStyle(Params.fromArray([6]));
assert.equal(optionsService.options['cursorStyle'], 'bar');
assert.equal(optionsService.options['cursorBlink'], false);
});
@@ -93,14 +100,13 @@ describe('InputHandler', () => {
describe('setMode', () => {
it('should toggle Terminal.bracketedPasteMode', () => {
const terminal = new MockInputHandlingTerminal();
- const collect = '?';
terminal.bracketedPasteMode = false;
- const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
+ const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
// Set bracketed paste mode
- inputHandler.setMode(Params.fromArray([2004]), collect);
+ inputHandler.setModePrivate(Params.fromArray([2004]));
assert.equal(terminal.bracketedPasteMode, true);
// Reset bracketed paste mode
- inputHandler.resetMode(Params.fromArray([2004]), collect);
+ inputHandler.resetModePrivate(Params.fromArray([2004]));
assert.equal(terminal.bracketedPasteMode, false);
});
});
@@ -114,7 +120,7 @@ describe('InputHandler', () => {
it('insertChars', function(): void {
const term = new Terminal();
const bufferService = new MockBufferService(80, 30);
- const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
+ const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
// insert some data in first and second line
inputHandler.parse(Array(bufferService.cols - 9).join('a'));
@@ -152,7 +158,7 @@ describe('InputHandler', () => {
it('deleteChars', function(): void {
const term = new Terminal();
const bufferService = new MockBufferService(80, 30);
- const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
+ const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
// insert some data in first and second line
inputHandler.parse(Array(bufferService.cols - 9).join('a'));
@@ -193,7 +199,7 @@ describe('InputHandler', () => {
it('eraseInLine', function(): void {
const term = new Terminal();
const bufferService = new MockBufferService(80, 30);
- const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
+ const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
// fill 6 lines to test 3 different states
inputHandler.parse(Array(bufferService.cols + 1).join('a'));
@@ -222,7 +228,7 @@ describe('InputHandler', () => {
it('eraseInDisplay', function(): void {
const term = new Terminal({cols: 80, rows: 7});
const bufferService = new MockBufferService(80, 7);
- const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
+ const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
// fill display with a's
for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a'));
@@ -357,7 +363,7 @@ describe('InputHandler', () => {
describe('print', () => {
it('should not cause an infinite loop (regression test)', () => {
const term = new Terminal();
- const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
+ const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
const container = new Uint32Array(10);
container[0] = 0x200B;
inputHandler.print(container, 0, 1);
@@ -372,7 +378,7 @@ describe('InputHandler', () => {
beforeEach(() => {
term = new Terminal();
bufferService = new MockBufferService(80, 30);
- handler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService());
+ handler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
});
it('should handle DECSET/DECRST 47 (alt screen buffer)', () => {
handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST');
@@ -1127,13 +1133,6 @@ describe('InputHandler', () => {
beforeEach(() => {
term = new TestTerminal({cols: 10, rows: 10});
});
- function getLines(term: TestTerminal, limit: number = term.rows): string[] {
- const res: string[] = [];
- for (let i = 0; i < limit; ++i) {
- res.push(term.buffer.lines.get(i).translateToString(true));
- }
- return res;
- }
it('scrollUp', () => {
term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Sm');
assert.deepEqual(getLines(term), ['m', '3', '', '', '4', '5', '6', '7', '8', '9']);
@@ -1193,4 +1192,78 @@ describe('InputHandler', () => {
assert.deepEqual(getLines(term), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']);
});
});
+ describe('SL/SR/DECIC/DECDC', () => {
+ let term: TestTerminal;
+ beforeEach(() => {
+ term = new TestTerminal({cols: 5, rows: 5, scrollback: 1});
+ });
+ it('SL (scrollLeft)', () => {
+ term.writeSync('12345'.repeat(6));
+ term.writeSync('\x1b[ @');
+ assert.deepEqual(getLines(term, term.rows + 1), ['12345', '2345', '2345', '2345', '2345', '2345']);
+ term.writeSync('\x1b[0 @');
+ assert.deepEqual(getLines(term, term.rows + 1), ['12345', '345', '345', '345', '345', '345']);
+ term.writeSync('\x1b[2 @');
+ assert.deepEqual(getLines(term, term.rows + 1), ['12345', '5', '5', '5', '5', '5']);
+ });
+ it('SR (scrollRight)', () => {
+ term.writeSync('12345'.repeat(6));
+ term.writeSync('\x1b[ A');
+ assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']);
+ term.writeSync('\x1b[0 A');
+ assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']);
+ term.writeSync('\x1b[2 A');
+ assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']);
+ });
+ it('insertColumns (DECIC)', () => {
+ term.writeSync('12345'.repeat(6));
+ term.writeSync('\x1b[3;3H');
+ term.writeSync('\x1b[\'}');
+ assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
+ term.reset();
+ term.writeSync('12345'.repeat(6));
+ term.writeSync('\x1b[3;3H');
+ term.writeSync('\x1b[1\'}');
+ assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
+ term.reset();
+ term.writeSync('12345'.repeat(6));
+ term.writeSync('\x1b[3;3H');
+ term.writeSync('\x1b[2\'}');
+ assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']);
+ });
+ it('deleteColumns (DECDC)', () => {
+ term.writeSync('12345'.repeat(6));
+ term.writeSync('\x1b[3;3H');
+ term.writeSync('\x1b[\'~');
+ assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']);
+ term.reset();
+ term.writeSync('12345'.repeat(6));
+ term.writeSync('\x1b[3;3H');
+ term.writeSync('\x1b[1\'~');
+ assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']);
+ term.reset();
+ term.writeSync('12345'.repeat(6));
+ term.writeSync('\x1b[3;3H');
+ term.writeSync('\x1b[2\'~');
+ assert.deepEqual(getLines(term, term.rows + 1), ['12345', '125', '125', '125', '125', '125']);
+ });
+ });
+ it('should parse big chunks in smaller subchunks', () => {
+ // max single chunk size is hardcoded as 131072
+ const calls: any[] = [];
+ const term = new TestTerminal({cols: 10, rows: 10});
+ (term as any)._inputHandler._parser.parse = (data: Uint32Array, length: number) => {
+ calls.push([data.length, length]);
+ };
+ term.writeSync('12345');
+ term.writeSync('a'.repeat(10000));
+ term.writeSync('a'.repeat(200000));
+ term.writeSync('a'.repeat(300000));
+ assert.deepEqual(calls, [
+ [4096, 5],
+ [10000, 10000],
+ [131072, 131072], [131072, 200000 - 131072],
+ [131072, 131072], [131072, 131072], [131072, 300000 - 131072 - 131072]
+ ]);
+ });
});
diff --git a/src/InputHandler.ts b/src/InputHandler.ts
index f47085d4..3994d532 100644
--- a/src/InputHandler.ts
+++ b/src/InputHandler.ts
@@ -14,19 +14,25 @@ import { concat } from 'common/TypedArrayUtils';
import { StringToUtf32, stringFromCodePoint, utf32ToString, Utf8ToUtf32 } from 'common/input/TextDecoder';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { EventEmitter, IEvent } from 'common/EventEmitter';
-import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams } from 'common/parser/Types';
+import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { AttributeData } from 'common/buffer/AttributeData';
import { IAttributeData, IDisposable } from 'common/Types';
-import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService } from 'common/services/Services';
-import { ISelectionService } from 'browser/services/Services';
+import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService } from 'common/services/Services';
+import { OscHandler } from 'common/parser/OscParser';
+import { DcsHandler } from 'common/parser/DcsParser';
/**
* Map collect to glevel. Used in `selectCharset`.
*/
const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2};
+/**
+ * Max length of the UTF32 input buffer. Real memory consumption is 4 times higher.
+ */
+const MAX_PARSEBUFFER_LENGTH = 131072;
+
/**
* DCS subparser implementations
@@ -48,7 +54,7 @@ class DECRQSS implements IDcsHandler {
private _optionsService: IOptionsService
) { }
- hook(collect: string, params: IParams, flag: number): void {
+ hook(params: IParams): void {
this._data = new Uint32Array(0);
}
@@ -56,7 +62,11 @@ class DECRQSS implements IDcsHandler {
this._data = concat(this._data, data.subarray(start, end));
}
- unhook(): void {
+ unhook(success: boolean): void {
+ if (!success) {
+ this._data = new Uint32Array(0);
+ return;
+ }
const data = utf32ToString(this._data);
this._data = new Uint32Array(0);
switch (data) {
@@ -118,8 +128,6 @@ export class InputHandler extends Disposable implements IInputHandler {
private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32();
private _workCell: CellData = new CellData();
- private _selectionService: ISelectionService | undefined;
-
private _onCursorMove = new EventEmitter();
public get onCursorMove(): IEvent { return this._onCursorMove.event; }
private _onLineFeed = new EventEmitter();
@@ -134,6 +142,7 @@ export class InputHandler extends Disposable implements IInputHandler {
private readonly _dirtyRowService: IDirtyRowService,
private readonly _logService: ILogService,
private readonly _optionsService: IOptionsService,
+ private readonly _coreMouseService: ICoreMouseService,
private readonly _parser: IEscapeSequenceParser = new EscapeSequenceParser())
{
super();
@@ -143,63 +152,79 @@ export class InputHandler extends Disposable implements IInputHandler {
/**
* custom fallback handlers
*/
- this._parser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => {
- this._logService.debug('Unknown CSI code: ', { collect, params: params.toArray(), flag: String.fromCharCode(flag) });
+ this._parser.setCsiHandlerFallback((ident, params) => {
+ this._logService.debug('Unknown CSI code: ', { identifier: this._parser.identToString(ident), params: params.toArray() });
});
- this._parser.setEscHandlerFallback((collect: string, flag: number) => {
- this._logService.debug('Unknown ESC code: ', { collect, flag: String.fromCharCode(flag) });
+ this._parser.setEscHandlerFallback(ident => {
+ this._logService.debug('Unknown ESC code: ', { identifier: this._parser.identToString(ident) });
});
- this._parser.setExecuteHandlerFallback((code: number) => {
+ this._parser.setExecuteHandlerFallback(code => {
this._logService.debug('Unknown EXECUTE code: ', { code });
});
- this._parser.setOscHandlerFallback((identifier: number, data: string) => {
- this._logService.debug('Unknown OSC code: ', { identifier, data });
+ this._parser.setOscHandlerFallback((identifier, action, data) => {
+ this._logService.debug('Unknown OSC code: ', { identifier, action, data });
+ });
+ this._parser.setDcsHandlerFallback((ident, action, payload) => {
+ if (action === 'HOOK') {
+ payload = payload.toArray();
+ }
+ this._logService.debug('Unknown DCS code: ', { identifier: this._parser.identToString(ident), action, payload });
});
/**
* print handler
*/
- this._parser.setPrintHandler((data, start, end): void => this.print(data, start, end));
+ this._parser.setPrintHandler((data, start, end) => 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));
+ this._parser.setCsiHandler({final: '@'}, params => this.insertChars(params));
+ this._parser.setCsiHandler({intermediates: ' ', final: '@'}, params => this.scrollLeft(params));
+ this._parser.setCsiHandler({final: 'A'}, params => this.cursorUp(params));
+ this._parser.setCsiHandler({intermediates: ' ', final: 'A'}, params => this.scrollRight(params));
+ this._parser.setCsiHandler({final: 'B'}, params => this.cursorDown(params));
+ this._parser.setCsiHandler({final: 'C'}, params => this.cursorForward(params));
+ this._parser.setCsiHandler({final: 'D'}, params => this.cursorBackward(params));
+ this._parser.setCsiHandler({final: 'E'}, params => this.cursorNextLine(params));
+ this._parser.setCsiHandler({final: 'F'}, params => this.cursorPrecedingLine(params));
+ this._parser.setCsiHandler({final: 'G'}, params => this.cursorCharAbsolute(params));
+ this._parser.setCsiHandler({final: 'H'}, params => this.cursorPosition(params));
+ this._parser.setCsiHandler({final: 'I'}, params => this.cursorForwardTab(params));
+ this._parser.setCsiHandler({final: 'J'}, params => this.eraseInDisplay(params));
+ this._parser.setCsiHandler({prefix: '?', final: 'J'}, params => this.eraseInDisplay(params));
+ this._parser.setCsiHandler({final: 'K'}, params => this.eraseInLine(params));
+ this._parser.setCsiHandler({prefix: '?', final: 'K'}, params => this.eraseInLine(params));
+ this._parser.setCsiHandler({final: 'L'}, params => this.insertLines(params));
+ this._parser.setCsiHandler({final: 'M'}, params => this.deleteLines(params));
+ this._parser.setCsiHandler({final: 'P'}, params => this.deleteChars(params));
+ this._parser.setCsiHandler({final: 'S'}, params => this.scrollUp(params));
+ this._parser.setCsiHandler({final: 'T'}, params => this.scrollDown(params));
+ this._parser.setCsiHandler({final: 'X'}, params => this.eraseChars(params));
+ this._parser.setCsiHandler({final: 'Z'}, params => this.cursorBackwardTab(params));
+ this._parser.setCsiHandler({final: '`'}, params => this.charPosAbsolute(params));
+ this._parser.setCsiHandler({final: 'a'}, params => this.hPositionRelative(params));
+ this._parser.setCsiHandler({final: 'b'}, params => this.repeatPrecedingCharacter(params));
+ this._parser.setCsiHandler({final: 'c'}, params => this.sendDeviceAttributesPrimary(params));
+ this._parser.setCsiHandler({prefix: '>', final: 'c'}, params => this.sendDeviceAttributesSecondary(params));
+ this._parser.setCsiHandler({final: 'd'}, params => this.linePosAbsolute(params));
+ this._parser.setCsiHandler({final: 'e'}, params => this.vPositionRelative(params));
+ this._parser.setCsiHandler({final: 'f'}, params => this.hVPosition(params));
+ this._parser.setCsiHandler({final: 'g'}, params => this.tabClear(params));
+ this._parser.setCsiHandler({final: 'h'}, params => this.setMode(params));
+ this._parser.setCsiHandler({prefix: '?', final: 'h'}, params => this.setModePrivate(params));
+ this._parser.setCsiHandler({final: 'l'}, params => this.resetMode(params));
+ this._parser.setCsiHandler({prefix: '?', final: 'l'}, params => this.resetModePrivate(params));
+ this._parser.setCsiHandler({final: 'm'}, params => this.charAttributes(params));
+ this._parser.setCsiHandler({final: 'n'}, params => this.deviceStatus(params));
+ this._parser.setCsiHandler({prefix: '?', final: 'n'}, params => this.deviceStatusPrivate(params));
+ this._parser.setCsiHandler({intermediates: '!', final: 'p'}, params => this.softReset(params));
+ this._parser.setCsiHandler({intermediates: ' ', final: 'q'}, params => this.setCursorStyle(params));
+ this._parser.setCsiHandler({final: 'r'}, params => this.setScrollRegion(params));
+ this._parser.setCsiHandler({final: 's'}, params => this.saveCursor(params));
+ this._parser.setCsiHandler({final: 'u'}, params => this.restoreCursor(params));
+ this._parser.setCsiHandler({intermediates: '\'', final: '}'}, params => this.insertColumns(params));
+ this._parser.setCsiHandler({intermediates: '\'', final: '~'}, params => this.deleteColumns(params));
/**
* execute handler
@@ -215,7 +240,6 @@ export class InputHandler extends Disposable implements IInputHandler {
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());
@@ -224,10 +248,10 @@ export class InputHandler extends Disposable implements IInputHandler {
* OSC handler
*/
// 0 - icon name + title
- this._parser.setOscHandler(0, (data) => this.setTitle(data));
+ this._parser.setOscHandler(0, new OscHandler((data: string) => this.setTitle(data)));
// 1 - icon name
// 2 - title
- this._parser.setOscHandler(2, (data) => this.setTitle(data));
+ this._parser.setOscHandler(2, new OscHandler((data: string) => this.setTitle(data)));
// 3 - set property X in the form "prop=value"
// 4 - Change Color Number
// 5 - Change Special Color Number
@@ -264,32 +288,32 @@ export class InputHandler extends Disposable implements IInputHandler {
/**
* 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());
+ this._parser.setEscHandler({final: '7'}, () => this.saveCursor());
+ this._parser.setEscHandler({final: '8'}, () => this.restoreCursor());
+ this._parser.setEscHandler({final: 'D'}, () => this.index());
+ this._parser.setEscHandler({final: 'E'}, () => this.nextLine());
+ this._parser.setEscHandler({final: 'H'}, () => this.tabSet());
+ this._parser.setEscHandler({final: 'M'}, () => this.reverseIndex());
+ this._parser.setEscHandler({final: '='}, () => this.keypadApplicationMode());
+ this._parser.setEscHandler({final: '>'}, () => this.keypadNumericMode());
+ this._parser.setEscHandler({final: 'c'}, () => this.reset());
+ this._parser.setEscHandler({final: 'n'}, () => this.setgLevel(2));
+ this._parser.setEscHandler({final: 'o'}, () => this.setgLevel(3));
+ this._parser.setEscHandler({final: '|'}, () => this.setgLevel(3));
+ this._parser.setEscHandler({final: '}'}, () => this.setgLevel(2));
+ this._parser.setEscHandler({final: '~'}, () => this.setgLevel(1));
+ this._parser.setEscHandler({intermediates: '%', final: '@'}, () => this.selectDefaultCharset());
+ this._parser.setEscHandler({intermediates: '%', final: '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?
+ this._parser.setEscHandler({intermediates: '(', final: flag}, () => this.selectCharset('(' + flag));
+ this._parser.setEscHandler({intermediates: ')', final: flag}, () => this.selectCharset(')' + flag));
+ this._parser.setEscHandler({intermediates: '*', final: flag}, () => this.selectCharset('*' + flag));
+ this._parser.setEscHandler({intermediates: '+', final: flag}, () => this.selectCharset('+' + flag));
+ this._parser.setEscHandler({intermediates: '-', final: flag}, () => this.selectCharset('-' + flag));
+ this._parser.setEscHandler({intermediates: '.', final: flag}, () => this.selectCharset('.' + flag));
+ this._parser.setEscHandler({intermediates: '/', final: flag}, () => this.selectCharset('/' + flag)); // TODO: supported?
}
- this._parser.setEscHandler('#8', () => this.screenAlignmentPattern());
+ this._parser.setEscHandler({intermediates: '#', final: '8'}, () => this.screenAlignmentPattern());
/**
* error handler
@@ -302,52 +326,53 @@ export class InputHandler extends Disposable implements IInputHandler {
/**
* DCS handler
*/
- this._parser.setDcsHandler('$q', new DECRQSS(this._bufferService, this._coreService, this._logService, this._optionsService));
+ this._parser.setDcsHandler({intermediates: '$', final: 'q'}, new DECRQSS(this._bufferService, this._coreService, this._logService, this._optionsService));
}
public dispose(): void {
super.dispose();
}
- // TODO: When InputHandler moves into common, browser dependencies need to move out
- public setBrowserServices(selectionService: ISelectionService): void {
- this._selectionService = selectionService;
- }
-
- public parse(data: string): void {
+ public parse(data: string | Uint8Array): void {
let buffer = this._bufferService.buffer;
const cursorStartX = buffer.x;
const cursorStartY = buffer.y;
this._logService.debug('parsing data', data);
+ // resize input buffer if needed
if (this._parseBuffer.length < data.length) {
- this._parseBuffer = new Uint32Array(data.length);
+ if (this._parseBuffer.length < MAX_PARSEBUFFER_LENGTH) {
+ this._parseBuffer = new Uint32Array(Math.min(data.length, MAX_PARSEBUFFER_LENGTH));
+ }
+ }
+
+ // Clear the dirty row service so we know which lines changed as a result of parsing
+ this._dirtyRowService.clearRange();
+
+ // process big data in smaller chunks
+ if (data.length > MAX_PARSEBUFFER_LENGTH) {
+ for (let i = 0; i < data.length; i += MAX_PARSEBUFFER_LENGTH) {
+ const end = i + MAX_PARSEBUFFER_LENGTH < data.length ? i + MAX_PARSEBUFFER_LENGTH : data.length;
+ const len = (typeof data === 'string')
+ ? this._stringDecoder.decode(data.substring(i, end), this._parseBuffer)
+ : this._utf8Decoder.decode(data.subarray(i, end), this._parseBuffer);
+ this._parser.parse(this._parseBuffer, len);
+ }
+ } else {
+ const len = (typeof data === 'string')
+ ? this._stringDecoder.decode(data, this._parseBuffer)
+ : this._utf8Decoder.decode(data, this._parseBuffer);
+ this._parser.parse(this._parseBuffer, len);
}
- this._parser.parse(this._parseBuffer, this._stringDecoder.decode(data, this._parseBuffer));
buffer = this._bufferService.buffer;
if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) {
this._onCursorMove.fire();
}
- }
- public parseUtf8(data: Uint8Array): void {
- let buffer = this._bufferService.buffer;
- const cursorStartX = buffer.x;
- const cursorStartY = buffer.y;
-
- this._logService.debug('parsing data', data);
-
- if (this._parseBuffer.length < data.length) {
- this._parseBuffer = new Uint32Array(data.length);
- }
- this._parser.parse(this._parseBuffer, this._utf8Decoder.decode(data, this._parseBuffer));
-
- buffer = this._bufferService.buffer;
- if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) {
- this._onCursorMove.fire();
- }
+ // Refresh any dirty rows accumulated as part of parsing
+ this._terminal.refresh(this._dirtyRowService.start, this._dirtyRowService.end);
}
public print(data: Uint32Array, start: number, end: number): void {
@@ -412,10 +437,13 @@ export class InputHandler extends Disposable implements IInputHandler {
if (wraparoundMode) {
buffer.x = 0;
buffer.y++;
- if (buffer.y > buffer.scrollBottom) {
+ if (buffer.y === buffer.scrollBottom + 1) {
buffer.y--;
this._terminal.scroll(true);
} else {
+ if (buffer.y >= this._bufferService.rows) {
+ buffer.y = this._bufferService.rows - 1;
+ }
// The line already exists (eg. the initial viewport), mark it as a
// wrapped line
buffer.lines.get(buffer.y).isWrapped = true;
@@ -477,15 +505,29 @@ export class InputHandler extends Disposable implements IInputHandler {
/**
* Forward addCsiHandler from parser.
*/
- public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable {
- return this._parser.addCsiHandler(flag, callback);
+ public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
+ return this._parser.addCsiHandler(id, callback);
+ }
+
+ /**
+ * Forward addDcsHandler from parser.
+ */
+ public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
+ return this._parser.addDcsHandler(id, new DcsHandler(callback));
+ }
+
+ /**
+ * Forward addEscHandler from parser.
+ */
+ public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable {
+ return this._parser.addEscHandler(id, callback);
}
/**
* Forward addOscHandler from parser.
*/
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
- return this._parser.addOscHandler(ident, callback);
+ return this._parser.addOscHandler(ident, new OscHandler(callback));
}
/**
@@ -508,9 +550,11 @@ export class InputHandler extends Disposable implements IInputHandler {
buffer.x = 0;
}
buffer.y++;
- if (buffer.y > buffer.scrollBottom) {
+ if (buffer.y === buffer.scrollBottom + 1) {
buffer.y--;
this._terminal.scroll();
+ } else if (buffer.y >= this._bufferService.rows) {
+ buffer.y = this._bufferService.rows - 1;
}
// If the end of the line is hit, prevent this action from wrapping around to the next line.
if (buffer.x >= this._bufferService.cols) {
@@ -611,7 +655,13 @@ export class InputHandler extends Disposable implements IInputHandler {
* Cursor Up Ps Times (default = 1) (CUU).
*/
public cursorUp(params: IParams): void {
- this._moveCursor(0, -(params.params[0] || 1));
+ // stop at scrollTop
+ const diffToTop = this._bufferService.buffer.y - this._bufferService.buffer.scrollTop;
+ if (diffToTop >= 0) {
+ this._moveCursor(0, -Math.min(diffToTop, params.params[0] || 1));
+ } else {
+ this._moveCursor(0, -(params.params[0] || 1));
+ }
}
/**
@@ -619,7 +669,13 @@ export class InputHandler extends Disposable implements IInputHandler {
* Cursor Down Ps Times (default = 1) (CUD).
*/
public cursorDown(params: IParams): void {
- this._moveCursor(0, params.params[0] || 1);
+ // stop at scrollBottom
+ const diffToBottom = this._bufferService.buffer.scrollBottom - this._bufferService.buffer.y;
+ if (diffToBottom >= 0) {
+ this._moveCursor(0, Math.min(diffToBottom, params.params[0] || 1));
+ } else {
+ this._moveCursor(0, params.params[0] || 1);
+ }
}
/**
@@ -644,7 +700,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* Other than cursorDown (CUD) also set the cursor to first column.
*/
public cursorNextLine(params: IParams): void {
- this._moveCursor(0, params.params[0] || 1);
+ this.cursorDown(params);
this._bufferService.buffer.x = 0;
}
@@ -654,7 +710,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* Other than cursorUp (CUU) also set the cursor to first column.
*/
public cursorPrecedingLine(params: IParams): void {
- this._moveCursor(0, -(params.params[0] || 1));
+ this.cursorUp(params);
this._bufferService.buffer.x = 0;
}
@@ -996,7 +1052,7 @@ export class InputHandler extends Disposable implements IInputHandler {
while (param--) {
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1);
- buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
+ buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(this._terminal.eraseAttrData()));
}
this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
}
@@ -1004,19 +1060,109 @@ export class InputHandler extends Disposable implements IInputHandler {
/**
* CSI Ps T Scroll down Ps lines (default = 1) (SD).
*/
- public scrollDown(params: IParams, collect?: string): void {
- if (params.length < 2 && !collect) {
- let param = params.params[0] || 1;
+ public scrollDown(params: IParams): void {
+ let param = params.params[0] || 1;
- // make buffer local for faster access
- const buffer = this._bufferService.buffer;
+ // make buffer local for faster access
+ const buffer = this._bufferService.buffer;
- while (param--) {
- buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
- buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
- }
- this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
+ while (param--) {
+ buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
+ buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
+ this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
+ }
+
+ /**
+ * CSI Ps SP @ Scroll left Ps columns (default = 1) (SL) ECMA-48
+ *
+ * Notation: (Pn)
+ * Representation: CSI Pn 02/00 04/00
+ * Parameter default value: Pn = 1
+ * SL causes the data in the presentation component to be moved by n character positions
+ * if the line orientation is horizontal, or by n line positions if the line orientation
+ * is vertical, such that the data appear to move to the left; where n equals the value of Pn.
+ * The active presentation position is not affected by this control function.
+ *
+ * Supported:
+ * - always left shift (no line orientation setting respected)
+ */
+ public scrollLeft(params: IParams): void {
+ const buffer = this._bufferService.buffer;
+ if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) {
+ return;
+ }
+ const param = params.params[0] || 1;
+ for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) {
+ const line = buffer.lines.get(buffer.ybase + y);
+ line.deleteCells(0, param, buffer.getNullCell(this._terminal.eraseAttrData()));
+ line.isWrapped = false;
+ }
+ this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
+ }
+
+ /**
+ * CSI Ps SP A Scroll right Ps columns (default = 1) (SR) ECMA-48
+ *
+ * Notation: (Pn)
+ * Representation: CSI Pn 02/00 04/01
+ * Parameter default value: Pn = 1
+ * SR causes the data in the presentation component to be moved by n character positions
+ * if the line orientation is horizontal, or by n line positions if the line orientation
+ * is vertical, such that the data appear to move to the right; where n equals the value of Pn.
+ * The active presentation position is not affected by this control function.
+ *
+ * Supported:
+ * - always right shift (no line orientation setting respected)
+ */
+ public scrollRight(params: IParams): void {
+ const buffer = this._bufferService.buffer;
+ if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) {
+ return;
+ }
+ const param = params.params[0] || 1;
+ for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) {
+ const line = buffer.lines.get(buffer.ybase + y);
+ line.insertCells(0, param, buffer.getNullCell(this._terminal.eraseAttrData()));
+ line.isWrapped = false;
+ }
+ this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
+ }
+
+ /**
+ * CSI Pm ' }
+ * Insert Ps Column(s) (default = 1) (DECIC), VT420 and up.
+ */
+ public insertColumns(params: IParams): void {
+ const buffer = this._bufferService.buffer;
+ if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) {
+ return;
+ }
+ const param = params.params[0] || 1;
+ for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) {
+ const line = this._bufferService.buffer.lines.get(buffer.ybase + y);
+ line.insertCells(buffer.x, param, buffer.getNullCell(this._terminal.eraseAttrData()));
+ line.isWrapped = false;
+ }
+ this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
+ }
+
+ /**
+ * CSI Pm ' ~
+ * Delete Ps Column(s) (default = 1) (DECDC), VT420 and up.
+ */
+ public deleteColumns(params: IParams): void {
+ const buffer = this._bufferService.buffer;
+ if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) {
+ return;
+ }
+ const param = params.params[0] || 1;
+ for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) {
+ const line = buffer.lines.get(buffer.ybase + y);
+ line.deleteCells(buffer.x, param, buffer.getNullCell(this._terminal.eraseAttrData()));
+ line.isWrapped = false;
+ }
+ this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
}
/**
@@ -1108,32 +1254,33 @@ export class InputHandler extends Disposable implements IInputHandler {
* xterm/charproc.c - line 2012, for more information.
* vim responds with ^[[?0c or ^[[?1c after the terminal's response (?)
*/
- public sendDeviceAttributes(params: IParams, collect?: string): void {
+ public sendDeviceAttributesPrimary(params: IParams): void {
if (params.params[0] > 0) {
return;
}
-
- if (!collect) {
- if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) {
- this._coreService.triggerDataEvent(C0.ESC + '[?1;2c');
- } else if (this._terminal.is('linux')) {
- this._coreService.triggerDataEvent(C0.ESC + '[?6c');
- }
- } else if (collect === '>') {
- // xterm and urxvt
- // seem to spit this
- // out around ~370 times (?).
- if (this._terminal.is('xterm')) {
- this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c');
- } else if (this._terminal.is('rxvt-unicode')) {
- this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c');
- } else if (this._terminal.is('linux')) {
- // not supported by linux console.
- // linux console echoes parameters.
- this._coreService.triggerDataEvent(params.params[0] + 'c');
- } else if (this._terminal.is('screen')) {
- this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c');
- }
+ if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) {
+ this._coreService.triggerDataEvent(C0.ESC + '[?1;2c');
+ } else if (this._terminal.is('linux')) {
+ this._coreService.triggerDataEvent(C0.ESC + '[?6c');
+ }
+ }
+ public sendDeviceAttributesSecondary(params: IParams): void {
+ if (params.params[0] > 0) {
+ return;
+ }
+ // xterm and urxvt
+ // seem to spit this
+ // out around ~370 times (?).
+ if (this._terminal.is('xterm')) {
+ this._coreService.triggerDataEvent(C0.ESC + '[>0;276;0c');
+ } else if (this._terminal.is('rxvt-unicode')) {
+ this._coreService.triggerDataEvent(C0.ESC + '[>85;95;0c');
+ } else if (this._terminal.is('linux')) {
+ // not supported by linux console.
+ // linux console echoes parameters.
+ this._coreService.triggerDataEvent(params.params[0] + 'c');
+ } else if (this._terminal.is('screen')) {
+ this._coreService.triggerDataEvent(C0.ESC + '[>83;40003;0c');
}
}
@@ -1223,15 +1370,9 @@ export class InputHandler extends Disposable implements IInputHandler {
* Modes:
* http: *vt100.net/docs/vt220-rm/chapter4.html
*/
- public setMode(params: IParams, collect?: string): void {
+ public setMode(params: IParams): void {
for (let i = 0; i < params.length; i++) {
- this._setMode(params.params[i], collect);
- }
- }
-
- private _setMode(param: number, collect?: string): void {
- if (!collect) {
- switch (param) {
+ switch (params.params[i]) {
case 4:
this._terminal.insertMode = true;
break;
@@ -1239,8 +1380,11 @@ export class InputHandler extends Disposable implements IInputHandler {
// this._t.convertEol = true;
break;
}
- } else if (collect === '?') {
- switch (param) {
+ }
+ }
+ public setModePrivate(params: IParams): void {
+ for (let i = 0; i < params.length; i++) {
+ switch (params.params[i]) {
case 1:
this._coreService.decPrivateModes.applicationCursorKeys = true;
break;
@@ -1276,54 +1420,36 @@ export class InputHandler extends Disposable implements IInputHandler {
break;
case 9: // X10 Mouse
// no release, no motion, no wheel, no modifiers.
+ this._coreMouseService.activeProtocol = 'X10';
+ break;
case 1000: // vt200 mouse
// no motion.
- // no modifiers, except control on the wheel.
+ this._coreMouseService.activeProtocol = 'VT200';
+ break;
case 1002: // button event mouse
+ this._coreMouseService.activeProtocol = 'DRAG';
+ break;
case 1003: // any event mouse
// any event - sends motion events,
// even if there is no button held down.
-
- // TODO: Why are params[0] compares nested within a switch for params[0]?
-
- this._terminal.x10Mouse = param === 9;
- this._terminal.vt200Mouse = param === 1000;
- this._terminal.normalMouse = param > 1000;
- this._terminal.mouseEvents = true;
- if (this._terminal.element) {
- this._terminal.element.classList.add('enable-mouse-events');
- }
- if (this._selectionService) {
- this._selectionService.disable();
- }
- this._logService.debug('Binding to mouse events.');
+ this._coreMouseService.activeProtocol = 'ANY';
break;
case 1004: // send focusin/focusout events
// focusin: ^[[I
// focusout: ^[[O
this._terminal.sendFocus = true;
break;
- case 1005: // utf8 ext mode mouse
- this._terminal.utfMouse = true;
- // for wide terminals
- // simply encodes large values as utf8 characters
+ case 1005: // utf8 ext mode mouse - removed in #2507
+ this._logService.debug('DECSET 1005 not supported (see #2507)');
break;
case 1006: // sgr ext mode mouse
- this._terminal.sgrMouse = true;
- // for wide terminals
- // does not add 32 to fields
- // press: ^[[ Keyboard Action Mode (AM).
@@ -1429,15 +1556,9 @@ export class InputHandler extends Disposable 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: IParams, collect?: string): void {
+ public resetMode(params: IParams): void {
for (let i = 0; i < params.length; i++) {
- this._resetMode(params.params[i], collect);
- }
- }
-
- private _resetMode(param: number, collect?: string): void {
- if (!collect) {
- switch (param) {
+ switch (params.params[i]) {
case 4:
this._terminal.insertMode = false;
break;
@@ -1445,8 +1566,11 @@ export class InputHandler extends Disposable implements IInputHandler {
// this._t.convertEol = false;
break;
}
- } else if (collect === '?') {
- switch (param) {
+ }
+ }
+ public resetModePrivate(params: IParams): void {
+ for (let i = 0; i < params.length; i++) {
+ switch (params.params[i]) {
case 1:
this._coreService.decPrivateModes.applicationCursorKeys = false;
break;
@@ -1481,31 +1605,22 @@ export class InputHandler extends Disposable implements IInputHandler {
case 1000: // vt200 mouse
case 1002: // button event mouse
case 1003: // any event mouse
- this._terminal.x10Mouse = false;
- this._terminal.vt200Mouse = false;
- this._terminal.normalMouse = false;
- this._terminal.mouseEvents = false;
- if (this._terminal.element) {
- this._terminal.element.classList.remove('enable-mouse-events');
- }
- if (this._selectionService) {
- this._selectionService.enable();
- }
+ this._coreMouseService.activeProtocol = 'NONE';
break;
case 1004: // send focusin/focusout events
this._terminal.sendFocus = false;
break;
- case 1005: // utf8 ext mode mouse
- this._terminal.utfMouse = false;
+ case 1005: // utf8 ext mode mouse - removed in #2507
+ this._logService.debug('DECRST 1005 not supported (see #2507)');
break;
case 1006: // sgr ext mode mouse
- this._terminal.sgrMouse = false;
+ this._coreMouseService.activeEncoding = 'DEFAULT';
break;
- case 1015: // urxvt ext mode mouse
- this._terminal.urxvtMouse = false;
+ case 1015: // urxvt ext mode mouse - removed in #2507
+ this._logService.debug('DECRST 1015 not supported (see #2507)');
break;
case 25: // hide cursor
- this._terminal.cursorHidden = true;
+ this._coreService.isCursorHidden = true;
break;
case 1048: // alt screen cursor
this.restoreCursor();
@@ -1516,7 +1631,7 @@ export class InputHandler extends Disposable implements IInputHandler {
case 1047: // normal screen buffer - clearing it first
// Ensure the selection manager has the correct buffer
this._bufferService.buffers.activateNormalBuffer();
- if (param === 1049) {
+ if (params.params[i] === 1049) {
this.restoreCursor();
}
this._terminal.refresh(0, this._bufferService.rows - 1);
@@ -1790,47 +1905,47 @@ export class InputHandler extends Disposable implements IInputHandler {
* CSI ? 5 3 n Locator available, if compiled-in, or
* CSI ? 5 0 n No Locator, if not.
*/
- public deviceStatus(params: IParams, collect?: string): void {
- if (!collect) {
- switch (params.params[0]) {
- case 5:
- // status report
- this._coreService.triggerDataEvent(`${C0.ESC}[0n`);
- break;
- case 6:
- // cursor position
- const y = this._bufferService.buffer.y + 1;
- const x = this._bufferService.buffer.x + 1;
- this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`);
- break;
- }
- } else if (collect === '?') {
- // modern xterm doesnt seem to
- // respond to any of these except ?6, 6, and 5
- switch (params.params[0]) {
- case 6:
- // cursor position
- const y = this._bufferService.buffer.y + 1;
- const x = this._bufferService.buffer.x + 1;
- this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`);
- break;
- case 15:
- // no printer
- // this.handler(C0.ESC + '[?11n');
- break;
- case 25:
- // dont support user defined keys
- // this.handler(C0.ESC + '[?21n');
- break;
- case 26:
- // north american keyboard
- // this.handler(C0.ESC + '[?27;1;0;0n');
- break;
- case 53:
- // no dec locator/mouse
- // this.handler(C0.ESC + '[?50n');
- break;
- }
+ public deviceStatus(params: IParams): void {
+ switch (params.params[0]) {
+ case 5:
+ // status report
+ this._coreService.triggerDataEvent(`${C0.ESC}[0n`);
+ break;
+ case 6:
+ // cursor position
+ const y = this._bufferService.buffer.y + 1;
+ const x = this._bufferService.buffer.x + 1;
+ this._coreService.triggerDataEvent(`${C0.ESC}[${y};${x}R`);
+ break;
+ }
+ }
+
+ public deviceStatusPrivate(params: IParams): void {
+ // modern xterm doesnt seem to
+ // respond to any of these except ?6, 6, and 5
+ switch (params.params[0]) {
+ case 6:
+ // cursor position
+ const y = this._bufferService.buffer.y + 1;
+ const x = this._bufferService.buffer.x + 1;
+ this._coreService.triggerDataEvent(`${C0.ESC}[?${y};${x}R`);
+ break;
+ case 15:
+ // no printer
+ // this.handler(C0.ESC + '[?11n');
+ break;
+ case 25:
+ // dont support user defined keys
+ // this.handler(C0.ESC + '[?21n');
+ break;
+ case 26:
+ // north american keyboard
+ // this.handler(C0.ESC + '[?27;1;0;0n');
+ break;
+ case 53:
+ // no dec locator/mouse
+ // this.handler(C0.ESC + '[?50n');
+ break;
}
}
@@ -1838,25 +1953,23 @@ export class InputHandler extends Disposable implements IInputHandler {
* CSI ! p Soft terminal reset (DECSTR).
* http://vt100.net/docs/vt220-rm/table4-10.html
*/
- public softReset(params: IParams, 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; // ?
- if (this._terminal.viewport) {
- this._terminal.viewport.syncScrollArea();
- }
- this._coreService.decPrivateModes.applicationCursorKeys = false;
- this._bufferService.buffer.scrollTop = 0;
- this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1;
- this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone();
- this._bufferService.buffer.x = this._bufferService.buffer.y = 0; // ?
- this._terminal.charset = null;
- this._terminal.glevel = 0; // ??
- this._terminal.charsets = [null]; // ??
+ public softReset(params: IParams): void {
+ this._coreService.isCursorHidden = false;
+ this._terminal.insertMode = false;
+ this._terminal.originMode = false;
+ this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false
+ this._terminal.applicationKeypad = false; // ?
+ if (this._terminal.viewport) {
+ this._terminal.viewport.syncScrollArea();
}
+ this._coreService.decPrivateModes.applicationCursorKeys = false;
+ this._bufferService.buffer.scrollTop = 0;
+ this._bufferService.buffer.scrollBottom = this._bufferService.rows - 1;
+ this._terminal.curAttrData = DEFAULT_ATTR_DATA.clone();
+ this._bufferService.buffer.x = this._bufferService.buffer.y = 0; // ?
+ this._terminal.charset = null;
+ this._terminal.glevel = 0; // ??
+ this._terminal.charsets = [null]; // ??
}
/**
@@ -1869,40 +1982,32 @@ export class InputHandler extends Disposable implements IInputHandler {
* Ps = 5 -> blinking bar (xterm).
* Ps = 6 -> steady bar (xterm).
*/
- public setCursorStyle(params?: IParams, collect?: string): void {
- if (collect === ' ') {
- const param = params.params[0] || 1;
- switch (param) {
- case 1:
- case 2:
- this._optionsService.options.cursorStyle = 'block';
- break;
- case 3:
- case 4:
- this._optionsService.options.cursorStyle = 'underline';
- break;
- case 5:
- case 6:
- this._optionsService.options.cursorStyle = 'bar';
- break;
- }
- const isBlinking = param % 2 === 1;
- this._optionsService.options.cursorBlink = isBlinking;
+ public setCursorStyle(params: IParams): void {
+ const param = params.params[0] || 1;
+ switch (param) {
+ case 1:
+ case 2:
+ this._optionsService.options.cursorStyle = 'block';
+ break;
+ case 3:
+ case 4:
+ this._optionsService.options.cursorStyle = 'underline';
+ break;
+ case 5:
+ case 6:
+ this._optionsService.options.cursorStyle = 'bar';
+ break;
}
+ const isBlinking = param % 2 === 1;
+ this._optionsService.options.cursorBlink = isBlinking;
}
/**
* CSI Ps ; Ps r
* Set Scrolling Region [top;bottom] (default = full size of win-
* dow) (DECSTBM).
- * CSI ? Pm r
- * currently skipped
*/
- public setScrollRegion(params: IParams, collect?: string): void {
- if (collect) {
- return;
- }
-
+ public setScrollRegion(params: IParams): void {
const top = params.params[0] || 1;
let bottom: number;
@@ -2043,10 +2148,13 @@ export class InputHandler extends Disposable implements IInputHandler {
*/
public index(): void {
this._restrictCursor();
+ const buffer = this._bufferService.buffer;
this._bufferService.buffer.y++;
- if (this._bufferService.buffer.y > this._bufferService.buffer.scrollBottom) {
- this._bufferService.buffer.y--;
+ if (buffer.y === buffer.scrollBottom + 1) {
+ buffer.y--;
this._terminal.scroll();
+ } else if (buffer.y >= this._bufferService.rows) {
+ buffer.y = this._bufferService.rows - 1;
}
this._restrictCursor();
}
diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts
index 714257f9..b55a516b 100644
--- a/src/Terminal.test.ts
+++ b/src/Terminal.test.ts
@@ -29,11 +29,6 @@ describe('Terminal', () => {
(term).renderer = new MockRenderer();
term.viewport = new MockViewport();
(term)._compositionHelper = new MockCompositionHelper();
- // Force synchronous writes
- term.write = (data) => {
- term.writeBuffer.push(data);
- (term)._innerWrite();
- };
(term).element = {
classList: {
toggle: () => { },
@@ -59,18 +54,18 @@ describe('Terminal', () => {
// });
it('should fire the onCursorMove event', (done) => {
term.onCursorMove(() => done());
- term.write('foo');
+ term.writeSync('foo');
});
it('should fire the onLineFeed event', (done) => {
term.onLineFeed(() => done());
- term.write('\n');
+ term.writeSync('\n');
});
it('should fire a scroll event when scrollback is created', (done) => {
term.onScroll(() => done());
- term.write('\n'.repeat(INIT_ROWS));
+ term.writeSync('\n'.repeat(INIT_ROWS));
});
it('should fire a scroll event when scrollback is cleared', (done) => {
- term.write('\n'.repeat(INIT_ROWS));
+ term.writeSync('\n'.repeat(INIT_ROWS));
term.onScroll(() => done());
term.clear();
});
@@ -168,17 +163,6 @@ describe('Terminal', () => {
});
});
- describe('reset', () => {
- it('should not affect cursorState', () => {
- term.cursorState = 1;
- term.reset();
- assert.equal(term.cursorState, 1);
- term.cursorState = 0;
- term.reset();
- assert.equal(term.cursorState, 0);
- });
- });
-
describe('clear', () => {
it('should clear a buffer equal to rows', () => {
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
@@ -195,7 +179,7 @@ describe('Terminal', () => {
it('should clear a buffer larger than rows', () => {
// Fill the buffer with dummy rows
for (let i = 0; i < term.rows * 2; i++) {
- term.write('test\n');
+ term.writeSync('test\n');
}
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
@@ -224,12 +208,37 @@ describe('Terminal', () => {
});
});
+ describe('paste', () => {
+ it('should fire data event', done => {
+ term.onData(e => {
+ assert.equal(e, 'foo');
+ done();
+ });
+ term.paste('foo');
+ });
+ it('should sanitize \n chars', done => {
+ term.onData(e => {
+ assert.equal(e, '\rfoo\rbar\r');
+ done();
+ });
+ term.paste('\r\nfoo\nbar\r');
+ });
+ it('should respect bracketed paste mode', done => {
+ term.onData(e => {
+ assert.equal(e, '\x1b[200~foo\x1b[201~');
+ done();
+ });
+ term.writeSync('\x1b[?2004h');
+ term.paste('foo');
+ });
+ });
+
describe('scroll', () => {
describe('scrollLines', () => {
let startYDisp: number;
beforeEach(() => {
for (let i = 0; i < INIT_ROWS * 2; i++) {
- term.writeln('test');
+ term.writeSync('test\r\n');
}
startYDisp = INIT_ROWS + 1;
});
@@ -264,7 +273,7 @@ describe('Terminal', () => {
let startYDisp: number;
beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) {
- term.writeln('test');
+ term.writeSync('test\r\n');
}
startYDisp = (term.rows * 2) + 1;
});
@@ -287,7 +296,7 @@ describe('Terminal', () => {
describe('scrollToTop', () => {
beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) {
- term.writeln('test');
+ term.writeSync('test\r\n');
}
});
it('should scroll to the top', () => {
@@ -301,7 +310,7 @@ describe('Terminal', () => {
let startYDisp: number;
beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) {
- term.writeln('test');
+ term.writeSync('test\r\n');
}
startYDisp = (term.rows * 2) + 1;
});
@@ -322,7 +331,7 @@ describe('Terminal', () => {
let startYDisp: number;
beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) {
- term.writeln('test');
+ term.writeSync('test\r\n');
}
startYDisp = (term.rows * 2) + 1;
});
@@ -367,7 +376,7 @@ describe('Terminal', () => {
it('should not scroll down, when a custom keydown handler prevents the event', () => {
// Add some output to the terminal
for (let i = 0; i < term.rows * 3; i++) {
- term.writeln('test');
+ term.writeSync('test\r\n');
}
const startYDisp = (term.rows * 2) + 1;
term.attachCustomKeyEventHandler(() => {
@@ -712,7 +721,7 @@ describe('Terminal', () => {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
- term.write(high + String.fromCharCode(i));
+ term.writeSync(high + String.fromCharCode(i));
const tchar = term.buffer.lines.get(0).loadCell(0, cell);
expect(tchar.getChars()).eql(high + String.fromCharCode(i));
expect(tchar.getChars().length).eql(2);
@@ -726,7 +735,7 @@ describe('Terminal', () => {
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
- term.write(high + String.fromCharCode(i));
+ term.writeSync(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars()).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars().length).eql(2);
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql('');
@@ -739,7 +748,7 @@ describe('Terminal', () => {
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.wraparoundMode = true;
- term.write('a' + high + String.fromCharCode(i));
+ term.writeSync('a' + high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a');
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars().length).eql(2);
@@ -757,7 +766,7 @@ describe('Terminal', () => {
if (width !== 1) {
continue;
}
- term.write('a' + high + String.fromCharCode(i));
+ term.writeSync('a' + high + String.fromCharCode(i));
// auto wraparound mode should cut off the rest of the line
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(2);
@@ -769,8 +778,8 @@ describe('Terminal', () => {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
- term.write(high);
- term.write(String.fromCharCode(i));
+ term.writeSync(high);
+ term.writeSync(String.fromCharCode(i));
const tchar = term.buffer.lines.get(0).loadCell(0, cell);
expect(tchar.getChars()).eql(high + String.fromCharCode(i));
expect(tchar.getChars().length).eql(2);
@@ -784,7 +793,7 @@ describe('Terminal', () => {
describe('unicode - combining characters', () => {
const cell = new CellData();
it('café', () => {
- term.write('cafe\u0301');
+ term.writeSync('cafe\u0301');
term.buffer.lines.get(0).loadCell(3, cell);
expect(cell.getChars()).eql('e\u0301');
expect(cell.getChars().length).eql(2);
@@ -792,7 +801,7 @@ describe('Terminal', () => {
});
it('café - end of line', () => {
term.buffer.x = term.cols - 1 - 3;
- term.write('cafe\u0301');
+ term.writeSync('cafe\u0301');
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
expect(cell.getChars()).eql('e\u0301');
expect(cell.getChars().length).eql(2);
@@ -804,7 +813,7 @@ describe('Terminal', () => {
});
it('multiple combined é', () => {
term.wraparoundMode = true;
- term.write(Array(100).join('e\u0301'));
+ term.writeSync(Array(100).join('e\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell);
expect(cell.getChars()).eql('e\u0301');
@@ -818,7 +827,7 @@ describe('Terminal', () => {
});
it('multiple surrogate with combined', () => {
term.wraparoundMode = true;
- term.write(Array(100).join('\uD800\uDC00\u0301'));
+ term.writeSync(Array(100).join('\uD800\uDC00\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell);
expect(cell.getChars()).eql('\uD800\uDC00\u0301');
@@ -836,18 +845,18 @@ describe('Terminal', () => {
const cell = new CellData();
it('cursor movement even', () => {
expect(term.buffer.x).eql(0);
- term.write('¥');
+ term.writeSync('¥');
expect(term.buffer.x).eql(2);
});
it('cursor movement odd', () => {
term.buffer.x = 1;
expect(term.buffer.x).eql(1);
- term.write('¥');
+ term.writeSync('¥');
expect(term.buffer.x).eql(3);
});
it('line of ¥ even', () => {
term.wraparoundMode = true;
- term.write(Array(50).join('¥'));
+ term.writeSync(Array(50).join('¥'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell);
if (i % 2) {
@@ -868,7 +877,7 @@ describe('Terminal', () => {
it('line of ¥ odd', () => {
term.wraparoundMode = true;
term.buffer.x = 1;
- term.write(Array(50).join('¥'));
+ term.writeSync(Array(50).join('¥'));
for (let i = 1; i < term.cols - 1; ++i) {
term.buffer.lines.get(0).loadCell(i, cell);
if (!(i % 2)) {
@@ -893,7 +902,7 @@ describe('Terminal', () => {
it('line of ¥ with combining odd', () => {
term.wraparoundMode = true;
term.buffer.x = 1;
- term.write(Array(50).join('¥\u0301'));
+ term.writeSync(Array(50).join('¥\u0301'));
for (let i = 1; i < term.cols - 1; ++i) {
term.buffer.lines.get(0).loadCell(i, cell);
if (!(i % 2)) {
@@ -917,7 +926,7 @@ describe('Terminal', () => {
});
it('line of ¥ with combining even', () => {
term.wraparoundMode = true;
- term.write(Array(50).join('¥\u0301'));
+ term.writeSync(Array(50).join('¥\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell);
if (i % 2) {
@@ -938,7 +947,7 @@ describe('Terminal', () => {
it('line of surrogate fullwidth with combining odd', () => {
term.wraparoundMode = true;
term.buffer.x = 1;
- term.write(Array(50).join('\ud843\ude6d\u0301'));
+ term.writeSync(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 1; i < term.cols - 1; ++i) {
term.buffer.lines.get(0).loadCell(i, cell);
if (!(i % 2)) {
@@ -962,7 +971,7 @@ describe('Terminal', () => {
});
it('line of surrogate fullwidth with combining even', () => {
term.wraparoundMode = true;
- term.write(Array(50).join('\ud843\ude6d\u0301'));
+ term.writeSync(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell);
if (i % 2) {
@@ -985,11 +994,11 @@ describe('Terminal', () => {
describe('insert mode', () => {
const cell = new CellData();
it('halfwidth - all', () => {
- term.write(Array(9).join('0123456789').slice(-80));
+ term.writeSync(Array(9).join('0123456789').slice(-80));
term.buffer.x = 10;
term.buffer.y = 0;
term.insertMode = true;
- term.write('abcde');
+ term.writeSync('abcde');
expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a');
expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('e');
@@ -997,11 +1006,11 @@ describe('Terminal', () => {
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('4');
});
it('fullwidth - insert', () => {
- term.write(Array(9).join('0123456789').slice(-80));
+ term.writeSync(Array(9).join('0123456789').slice(-80));
term.buffer.x = 10;
term.buffer.y = 0;
term.insertMode = true;
- term.write('¥¥¥');
+ term.writeSync('¥¥¥');
expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('¥');
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('');
@@ -1010,16 +1019,16 @@ describe('Terminal', () => {
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('3');
});
it('fullwidth - right border', () => {
- term.write(Array(41).join('¥'));
+ term.writeSync(Array(41).join('¥'));
term.buffer.x = 10;
term.buffer.y = 0;
term.insertMode = true;
- term.write('a');
+ term.writeSync('a');
expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a');
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('¥');
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // fullwidth char got replaced
- term.write('b');
+ term.writeSync('b');
expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('b');
expect(term.buffer.lines.get(0).loadCell(12, cell).getChars()).eql('¥');
@@ -1123,6 +1132,241 @@ describe('Terminal', () => {
});
});
});
+
+ describe('Buffer.stringIndexToBufferIndex', () => {
+ let terminal: TestTerminal;
+
+ beforeEach(() => {
+ terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
+ });
+
+ it('multiline ascii', () => {
+ const input = 'This is ASCII text spanning multiple lines.';
+ terminal.writeSync(input);
+ const s = terminal.buffer.iterator(true).next().content;
+ assert.equal(input, s);
+ for (let i = 0; i < input.length; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
+ assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
+ }
+ });
+
+ it('combining e\u0301 in a sentence', () => {
+ const input = 'Sitting in the cafe\u0301 drinking coffee.';
+ terminal.writeSync(input);
+ const s = terminal.buffer.iterator(true).next().content;
+ assert.equal(input, s);
+ for (let i = 0; i < 19; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
+ assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
+ }
+ // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index
+ assert.deepEqual(
+ terminal.buffer.stringIndexToBufferIndex(0, 18),
+ terminal.buffer.stringIndexToBufferIndex(0, 19));
+ // after the combining char every string index has an offset of -1
+ for (let i = 19; i < input.length; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
+ assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
+ }
+ });
+
+ it('multiline combining e\u0301', () => {
+ const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301';
+ terminal.writeSync(input);
+ const s = terminal.buffer.iterator(true).next().content;
+ assert.equal(input, s);
+ // every buffer cell index contains 2 string indices
+ for (let i = 0; i < input.length; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
+ assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
+ }
+ });
+
+ it('surrogate char in a sentence', () => {
+ const input = 'The 𝄞 is a clef widely used in modern notation.';
+ terminal.writeSync(input);
+ const s = terminal.buffer.iterator(true).next().content;
+ assert.equal(input, s);
+ for (let i = 0; i < 5; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
+ assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
+ }
+ // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index
+ assert.deepEqual(
+ terminal.buffer.stringIndexToBufferIndex(0, 4),
+ terminal.buffer.stringIndexToBufferIndex(0, 5));
+ // after the combining char every string index has an offset of -1
+ for (let i = 5; i < input.length; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
+ assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
+ }
+ });
+
+ it('multiline surrogate char', () => {
+ const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞';
+ terminal.writeSync(input);
+ const s = terminal.buffer.iterator(true).next().content;
+ assert.equal(input, s);
+ // every buffer cell index contains 2 string indices
+ for (let i = 0; i < input.length; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
+ assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
+ }
+ });
+
+ it('surrogate char with combining', () => {
+ // eye of Ra with acute accent - string length of 3
+ const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.';
+ terminal.writeSync(input);
+ const s = terminal.buffer.iterator(true).next().content;
+ assert.equal(input, s);
+ // index 0..2 should map to 0
+ assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1));
+ assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2));
+ for (let i = 2; i < input.length; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
+ assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex);
+ }
+ });
+
+ it('multiline surrogate with combining', () => {
+ const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301';
+ terminal.writeSync(input);
+ const s = terminal.buffer.iterator(true).next().content;
+ assert.equal(input, s);
+ // every buffer cell index contains 3 string indices
+ for (let i = 0; i < input.length; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
+ assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex);
+ }
+ });
+
+ it('fullwidth chars', () => {
+ const input = 'These 123 are some fat numbers.';
+ terminal.writeSync(input);
+ const s = terminal.buffer.iterator(true).next().content;
+ assert.equal(input, s);
+ for (let i = 0; i < 6; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
+ assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
+ }
+ // string index 6, 7, 8 take 2 cells
+ assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7));
+ assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8));
+ // rest of the string has offset of +3
+ for (let i = 9; i < input.length; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
+ assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex);
+ }
+ });
+
+ it('multiline fullwidth chars', () => {
+ const input = '12345678901234567890';
+ terminal.writeSync(input);
+ const s = terminal.buffer.iterator(true).next().content;
+ assert.equal(input, s);
+ for (let i = 9; i < input.length; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
+ assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex);
+ }
+ });
+
+ it('fullwidth combining with emoji - match emoji cell', () => {
+ const input = 'Lots of ¥\u0301 make me 😃.';
+ terminal.writeSync(input);
+ const s = terminal.buffer.iterator(true).next().content;
+ assert.equal(input, s);
+ const stringIndex = s.match(/😃/).index;
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex);
+ assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars(), '😃');
+ });
+
+ it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => {
+ const input = 'a12345678901234567890';
+ // the 'a' at the beginning moves all fullwidth chars one to the right
+ // now the end of the line contains a dangling empty cell since
+ // the next fullwidth char has to wrap early
+ // the dangling last cell is wrongly added in the string
+ // --> fixable after resolving #1685
+ terminal.writeSync(input);
+ const s = terminal.buffer.iterator(true).next().content;
+ assert.equal(input, s);
+ for (let i = 10; i < input.length; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
+ const j = (i - 0) << 1;
+ assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex);
+ }
+ });
+
+ it('test fully wrapped buffer up to last char', () => {
+ const input = Array(6).join('1234567890');
+ terminal.writeSync(input);
+ const s = terminal.buffer.iterator(true).next().content;
+ assert.equal(input, s);
+ for (let i = 0; i < input.length; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
+ assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars());
+ }
+ });
+
+ it('test fully wrapped buffer up to last char with full width odd', () => {
+ const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'
+ + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301';
+ terminal.writeSync(input);
+ const s = terminal.buffer.iterator(true).next().content;
+ assert.equal(input, s);
+ for (let i = 0; i < input.length; ++i) {
+ const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
+ assert.equal(
+ (!(i % 3))
+ ? input[i]
+ : (i % 3 === 1)
+ ? input.substr(i, 2)
+ : input.substr(i - 1, 2),
+ terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars());
+ }
+ });
+
+ it('should handle \t in lines correctly', () => {
+ const input = '\thttps://google.de';
+ terminal.writeSync(input);
+ const s = terminal.buffer.iterator(true).next().content;
+ assert.equal(s, Array(terminal.optionsService.options.tabStopWidth + 1).join(' ') + 'https://google.de');
+ });
+ });
+
+ describe('BufferStringIterator', function(): void {
+ it('iterator does not overflow buffer limits', function(): void {
+ const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
+ const data = [
+ 'aaaaaaaaaa',
+ 'aaaaaaaaa\n',
+ 'aaaaaaaaaa',
+ 'aaaaaaaaa\n',
+ 'aaaaaaaaaa',
+ 'aaaaaaaaaa',
+ 'aaaaaaaaaa',
+ 'aaaaaaaaa\n',
+ 'aaaaaaaaaa',
+ 'aaaaaaaaaa'
+ ];
+ terminal.writeSync(data.join(''));
+ // brute force test with insane values
+ expect(() => {
+ for (let overscan = 0; overscan < 20; ++overscan) {
+ for (let start = -10; start < 20; ++start) {
+ for (let end = -10; end < 20; ++end) {
+ const it = terminal.buffer.iterator(false, start, end, overscan, overscan);
+ while (it.hasNext()) {
+ it.next();
+ }
+ }
+ }
+ }
+ }).to.not.throw();
+ });
+ });
});
class TestLinkifier extends Linkifier {
diff --git a/src/Terminal.ts b/src/Terminal.ts
index 78f16ef9..5c08a709 100644
--- a/src/Terminal.ts
+++ b/src/Terminal.ts
@@ -25,10 +25,10 @@ import { IInputHandlingTerminal, ICompositionHelper, ITerminalOptions, ITerminal
import { IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types';
import { CompositionHelper } from 'browser/input/CompositionHelper';
import { Viewport } from 'browser/Viewport';
-import { rightClickHandler, moveTextAreaUnderMouseCursor, pasteHandler, copyHandler } from 'browser/Clipboard';
+import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard';
import { C0 } from 'common/data/EscapeSequences';
import { InputHandler } from './InputHandler';
-import { Renderer } from './renderer/Renderer';
+import { Renderer } from 'browser/renderer/Renderer';
import { Linkifier } from 'browser/Linkifier';
import { SelectionService } from 'browser/services/SelectionService';
import * as Browser from 'common/Platform';
@@ -38,48 +38,36 @@ import { SoundService } from 'browser/services/SoundService';
import { MouseZoneManager } from 'browser/MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm';
-import { DomRenderer } from './renderer/dom/DomRenderer';
-import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData } from 'common/Types';
+import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
+import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
-import { applyWindowsMode } from './WindowsMode';
+import { handleWindowsModeLineFeed } from 'common/WindowsMode';
import { ColorManager } from 'browser/ColorManager';
import { RenderService } from 'browser/services/RenderService';
-import { IOptionsService, IBufferService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services';
+import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services';
import { OptionsService } from 'common/services/OptionsService';
-import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService } from 'browser/services/Services';
+import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService } from 'browser/services/Services';
import { CharSizeService } from 'browser/services/CharSizeService';
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
import { Disposable } from 'common/Lifecycle';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
import { Attributes } from 'common/buffer/Constants';
import { MouseService } from 'browser/services/MouseService';
-import { IParams } from 'common/parser/Types';
+import { IParams, IFunctionIdentifier } from 'common/parser/Types';
import { CoreService } from 'common/services/CoreService';
import { LogService } from 'common/services/LogService';
import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport } from 'browser/Types';
import { DirtyRowService } from 'common/services/DirtyRowService';
import { InstantiationService } from 'common/services/InstantiationService';
+import { CoreMouseService } from 'common/services/CoreMouseService';
+import { WriteBuffer } from 'common/input/WriteBuffer';
+import { CoreBrowserService } from 'browser/services/CoreBrowserService';
// Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null;
-/**
- * The amount of write requests to queue before sending an XOFF signal to the
- * pty process. This number must be small in order for ^C and similar sequences
- * to be responsive.
- */
-const WRITE_BUFFER_PAUSE_THRESHOLD = 5;
-
-/**
- * The max number of ms to spend on writes before allowing the renderer to
- * catch up with a 0ms setTimeout. A value of < 33 to keep us close to
- * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS
- * depends on the time it takes for the renderer to draw the frame.
- */
-const WRITE_TIMEOUT_MS = 12;
-const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
export class Terminal extends Disposable implements ITerminal, IDisposable, IInputHandlingTerminal {
public textarea: HTMLTextAreaElement;
@@ -89,7 +77,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
/**
* The HTMLElement that the terminal is created in, set by Terminal.open.
*/
- private _parent: HTMLElement;
+ private _parent: HTMLElement | null;
private _document: Document;
private _viewportScrollArea: HTMLElement;
private _viewportElement: HTMLElement;
@@ -103,15 +91,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
// TODO: We should remove options once components adopt optionsService
public get options(): ITerminalOptions { return this.optionsService.options; }
- // TODO: This can be changed to an enum or boolean, 0 and 1 seem to be the only options
- public cursorState: number;
- public cursorHidden: boolean;
-
private _customKeyEventHandler: CustomKeyEventHandler;
// common services
private _bufferService: IBufferService;
private _coreService: ICoreService;
+ private _coreMouseService: ICoreMouseService;
private _dirtyRowService: IDirtyRowService;
private _instantiationService: IInstantiationService;
private _logService: ILogService;
@@ -139,16 +124,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
public charsets: ICharset[];
// mouse properties
- private _decLocator: boolean; // This is unstable and never set
- public x10Mouse: boolean;
- public vt200Mouse: boolean;
- private _vt300Mouse: boolean; // This is unstable and never set
- public normalMouse: boolean;
- public mouseEvents: boolean;
+ public mouseEvents: CoreMouseEventType = CoreMouseEventType.NONE;
public sendFocus: boolean;
- public utfMouse: boolean;
- public sgrMouse: boolean;
- public urxvtMouse: boolean;
// misc
public savedCols: number;
@@ -159,21 +136,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
public params: (string | number)[];
public currentParam: string | number;
- // user input states
- public writeBuffer: string[];
- public writeBufferUtf8: Uint8Array[];
- private _writeInProgress: boolean;
-
- /**
- * Whether _xterm.js_ sent XOFF in order to catch up with the pty process.
- * This is a distinct state from writeStopped so that if the user requested
- * XOFF via ^S that it will not automatically resume when the writeBuffer goes
- * below threshold.
- */
- private _xoffSentToCatchUp: boolean;
-
- /** Whether writing has been stopped as a result of XOFF */
- // private _writeStopped: boolean;
+ // write buffer
+ private _writeBuffer: WriteBuffer;
// Store if user went browsing history in scrollback
private _userScrolling: boolean;
@@ -205,6 +169,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
public get onCursorMove(): IEvent { return this._onCursorMove.event; }
private _onData = new EventEmitter();
public get onData(): IEvent { return this._onData.event; }
+ private _onBinary = new EventEmitter();
+ public get onBinary(): IEvent { return this._onBinary.event; }
private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>();
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; }
private _onLineFeed = new EventEmitter();
@@ -257,11 +223,16 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom());
this._instantiationService.setService(ICoreService, this._coreService);
this._coreService.onData(e => this._onData.fire(e));
+ this._coreService.onBinary(e => this._onBinary.fire(e));
+ this._coreMouseService = this._instantiationService.createInstance(CoreMouseService);
+ this._instantiationService.setService(ICoreMouseService, this._coreMouseService);
this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService);
this._instantiationService.setService(IDirtyRowService, this._dirtyRowService);
this._setupOptionsListeners();
this._setup();
+
+ this._writeBuffer = new WriteBuffer(data => this._inputHandler.parse(data));
}
public dispose(): void {
@@ -269,25 +240,17 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
return;
}
super.dispose();
- if (this._windowsMode) {
- this._windowsMode.dispose();
- this._windowsMode = undefined;
- }
- if (this._renderService) {
- this._renderService.dispose();
- }
+ this._windowsMode?.dispose();
+ this._windowsMode = undefined;
+ this._renderService?.dispose();
this._customKeyEventHandler = null;
this.write = () => {};
- if (this.element && this.element.parentNode) {
- this.element.parentNode.removeChild(this.element);
- }
+ this.element?.parentNode?.removeChild(this.element);
}
private _setup(): void {
this._parent = document ? document.body : null;
- this.cursorState = 0;
- this.cursorHidden = false;
this._customKeyEventHandler = null;
// modes
@@ -310,17 +273,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.params = [];
this.currentParam = 0;
- // user input states
- this.writeBuffer = [];
- this.writeBufferUtf8 = [];
- this._writeInProgress = false;
-
- this._xoffSentToCatchUp = false;
- // this._writeStopped = false;
this._userScrolling = false;
// Register input handler and refire/handle events
- this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService);
+ this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService);
this._inputHandler.onCursorMove(() => this._onCursorMove.fire());
this._inputHandler.onLineFeed(() => this._onLineFeed.fire());
this.register(this._inputHandler);
@@ -328,7 +284,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.linkifier = this.linkifier || new Linkifier(this._bufferService, this._logService);
if (this.options.windowsMode) {
- this._windowsMode = applyWindowsMode(this);
+ this._enableWindowsMode();
+ }
+ }
+
+ private _enableWindowsMode(): void {
+ if (!this._windowsMode) {
+ this._windowsMode = this.onLineFeed(handleWindowsModeLineFeed.bind(null, this._bufferService));
}
}
@@ -361,10 +323,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
}
}
- public get isFocused(): boolean {
- return document.activeElement === this.textarea && document.hasFocus();
- }
-
private _setupOptionsListeners(): void {
// TODO: These listeners should be owned by individual components
this.optionsService.onOptionChange(key => {
@@ -372,18 +330,15 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
case 'fontFamily':
case 'fontSize':
// When the font changes the size of the cells may change which requires a renderer clear
- if (this._renderService) {
- this._renderService.clear();
- }
- if (this._charSizeService) {
- this._charSizeService.measure();
- }
+ this._renderService?.clear();
+ this._charSizeService?.measure();
break;
case 'drawBoldTextInBrightColors':
case 'letterSpacing':
case 'lineHeight':
case 'fontWeight':
case 'fontWeightBold':
+ case 'minimumContrastRatio':
// When the font changes the size of the cells may change which requires a renderer clear
if (this._renderService) {
this._renderService.clear();
@@ -394,13 +349,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
case 'rendererType':
if (this._renderService) {
this._renderService.setRenderer(this._createRenderer());
+ this._renderService.onResize(this.cols, this.rows);
}
break;
case 'scrollback':
this.buffers.resize(this.cols, this.rows);
- if (this.viewport) {
- this.viewport.syncScrollArea();
- }
+ this.viewport?.syncScrollArea();
break;
case 'screenReaderMode':
if (this.optionsService.options.screenReaderMode) {
@@ -408,38 +362,20 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
}
} else {
- if (this._accessibilityManager) {
- this._accessibilityManager.dispose();
- this._accessibilityManager = null;
- }
+ this._accessibilityManager?.dispose();
+ this._accessibilityManager = null;
}
break;
case 'tabStopWidth': this.buffers.setupTabStops(); break;
case 'theme':
this._setTheme(this.optionsService.options.theme);
break;
- case 'scrollback':
- const newBufferLength = this.rows + this.optionsService.options.scrollback;
- if (this.buffer.lines.length > newBufferLength) {
- const amountToTrim = this.buffer.lines.length - newBufferLength;
- const needsRefresh = (this.buffer.ydisp - amountToTrim < 0);
- this.buffer.lines.trimStart(amountToTrim);
- this.buffer.ybase = Math.max(this.buffer.ybase - amountToTrim, 0);
- this.buffer.ydisp = Math.max(this.buffer.ydisp - amountToTrim, 0);
- if (needsRefresh) {
- this.refresh(0, this.rows - 1);
- }
- }
case 'windowsMode':
if (this.optionsService.options.windowsMode) {
- if (!this._windowsMode) {
- this._windowsMode = applyWindowsMode(this);
- }
+ this._enableWindowsMode();
} else {
- if (this._windowsMode) {
- this._windowsMode.dispose();
- this._windowsMode = undefined;
- }
+ this._windowsMode?.dispose();
+ this._windowsMode = undefined;
}
break;
}
@@ -497,7 +433,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
}
copyHandler(event, this._selectionService);
}));
- const pasteHandlerWrapper = (event: ClipboardEvent) => pasteHandler(event, this.textarea, this.bracketedPasteMode, e => this._coreService.triggerDataEvent(e, true));
+ const pasteHandlerWrapper = (event: ClipboardEvent) => handlePasteEvent(event, this.textarea, this.bracketedPasteMode, this._coreService);
this.register(addDisposableDomListener(this.textarea, 'paste', pasteHandlerWrapper));
this.register(addDisposableDomListener(this.element, 'paste', pasteHandlerWrapper));
@@ -555,6 +491,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
throw new Error('Terminal requires a parent element.');
}
+ if (!document.body.contains(parent)) {
+ this._logService.warn('Terminal.open was called on an element that was not attached to the DOM');
+ }
+
this._document = this._parent.ownerDocument;
// Create main element container
@@ -596,6 +536,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
this._helperContainer.appendChild(this.textarea);
+ const coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea);
+ this._instantiationService.setService(ICoreBrowserService, coreBrowserService);
+
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
this._instantiationService.setService(ICharSizeService, this._charSizeService);
@@ -607,9 +550,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
// Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment);
- this._theme = this.options.theme;
- this.options.theme = null;
+ this._theme = this.options.theme || this._theme;
+ this.options.theme = undefined;
this._colorManager = new ColorManager(document, this.options.allowTransparency);
+ this.optionsService.onOptionChange(e => this._colorManager.onOptionsChange(e));
this._colorManager.setTheme(this._theme);
const renderer = this._createRenderer();
@@ -643,7 +587,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.screenElement);
this._instantiationService.setService(ISelectionService, this._selectionService);
this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire()));
- this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService.onMouseDown(e)));
this.register(this._selectionService.onRedrawRequest(e => this._renderService.onSelectionChanged(e.start, e.end, e.columnSelectMode)));
this.register(this._selectionService.onLinuxMouseSelection(text => {
// If there's a new selection, put it into the textarea, focus and select it
@@ -664,14 +607,16 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.register(this.onScroll(() => this._mouseZoneManager.clearAll()));
this.linkifier.attachToDom(this.element, this._mouseZoneManager);
+ // This event listener must be registered aftre MouseZoneManager is created
+ this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService.onMouseDown(e)));
+
// apply mouse event classes set by escape codes before terminal was attached
- this.element.classList.toggle('enable-mouse-events', this.mouseEvents);
if (this.mouseEvents) {
this._selectionService.disable();
+ this.element.classList.add('enable-mouse-events');
} else {
this._selectionService.enable();
}
- this._inputHandler.setBrowserServices(this._selectionService);
if (this.options.screenReaderMode) {
// Note that this must be done *after* the renderer is created in order to
@@ -691,13 +636,12 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
// Listen for mouse events and translate
// them into terminal mouse protocols.
this.bindMouse();
-
}
private _createRenderer(): IRenderer {
switch (this.options.rendererType) {
- case 'canvas': return new Renderer(this._colorManager.colors, this, this._bufferService, this._charSizeService, this.optionsService);
- case 'dom': return new DomRenderer(this, this._colorManager.colors, this._charSizeService, this.optionsService);
+ case 'canvas': return this._instantiationService.createInstance(Renderer, this._colorManager.colors, this.screenElement, this.linkifier);
+ case 'dom': return this._instantiationService.createInstance(DomRenderer, this._colorManager.colors, this.element, this.screenElement, this._viewportElement, this.linkifier);
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
}
}
@@ -708,239 +652,194 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
*/
private _setTheme(theme: ITheme): void {
this._theme = theme;
- if (this._colorManager) {
- this._colorManager.setTheme(theme);
- }
- if (this._renderService) {
- this._renderService.setColors(this._colorManager.colors);
- }
- if (this.viewport) {
- this.viewport.onThemeChange(this._colorManager.colors);
- }
+ this._colorManager?.setTheme(theme);
+ this._renderService?.setColors(this._colorManager.colors);
+ this.viewport?.onThemeChange(this._colorManager.colors);
}
/**
- * XTerm mouse events
- * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
- * To better understand these
- * the xterm code is very helpful:
- * Relevant files:
- * button.c, charproc.c, misc.c
- * Relevant functions in xterm/button.c:
- * BtnCode, EmitButtonCode, EditorButton, SendMousePosition
+ * Bind certain mouse events to the terminal.
+ * By default only 3 button + wheel up/down is ativated. For higher buttons
+ * no mouse report will be created. Typically the standard actions will be active.
+ *
+ * There are several reasons not to enable support for higher buttons/wheel:
+ * - Button 4 and 5 are typically used for history back and forward navigation,
+ * there is no straight forward way to supress/intercept those standard actions.
+ * - Support for higher buttons does not work in some platform/browser combinations.
+ * - Left/right wheel was not tested.
+ * - Emulators vary in mouse button support, typically only 3 buttons and
+ * wheel up/down work reliable.
+ *
+ * TODO: Move mouse event code into its own file.
*/
public bindMouse(): void {
- const el = this.element;
const self = this;
- let pressed = 32;
+ const el = this.element;
- // mouseup, mousedown, wheel
- // left click: ^[[M 3<^[[M#3<
- // wheel up: ^[[M`3>
- function sendButton(ev: MouseEvent | WheelEvent): void {
- let button;
+ // send event to CoreMouseService
+ function sendEvent(ev: MouseEvent | WheelEvent): boolean {
let pos;
- // get the xterm-style button
- button = getButton(ev);
-
// get mouse coordinates
pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows);
- if (!pos) return;
-
- sendEvent(button, pos);
+ if (!pos) {
+ return false;
+ }
+ let but: CoreMouseButton;
+ let action: CoreMouseAction;
switch ((ev).overrideType || ev.type) {
- case 'mousedown':
- pressed = button;
+ case 'mousemove':
+ action = CoreMouseAction.MOVE;
+ if (ev.buttons === undefined) {
+ // buttons is not supported on macOS, try to get a value from button instead
+ but = CoreMouseButton.NONE;
+ if (ev.button !== undefined) {
+ but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
+ }
+ } else {
+ // according to MDN buttons only reports up to button 5 (AUX2)
+ but = ev.buttons & 1 ? CoreMouseButton.LEFT :
+ ev.buttons & 4 ? CoreMouseButton.MIDDLE :
+ ev.buttons & 2 ? CoreMouseButton.RIGHT :
+ CoreMouseButton.NONE; // fallback to NONE
+ }
break;
case 'mouseup':
- // keep it at the left
- // button, just in case.
- pressed = 32;
+ action = CoreMouseAction.UP;
+ but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
+ break;
+ case 'mousedown':
+ action = CoreMouseAction.DOWN;
+ but = ev.button < 3 ? ev.button : CoreMouseButton.NONE;
break;
case 'wheel':
- // nothing. don't
- // interfere with
- // `pressed`.
+ // only UP/DOWN wheel events are respected
+ if ((ev as WheelEvent).deltaY !== 0) {
+ action = (ev as WheelEvent).deltaY < 0 ? CoreMouseAction.UP : CoreMouseAction.DOWN;
+ }
+ but = CoreMouseButton.WHEEL;
break;
+ default:
+ // dont handle other event types by accident
+ return false;
}
+
+ // exit if we cannot determine valid button/action values
+ // do nothing for higher buttons than wheel
+ if (action === undefined || but === undefined || but > CoreMouseButton.WHEEL) {
+ return false;
+ }
+
+ return self._coreMouseService.triggerMouseEvent({
+ col: pos.x - 33, // FIXME: why -33 here?
+ row: pos.y - 33,
+ button: but,
+ action,
+ ctrl: ev.ctrlKey,
+ alt: ev.altKey,
+ shift: ev.shiftKey
+ });
}
- // motion example of a left click:
- // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7<
- function sendMove(ev: MouseEvent): void {
- let button = pressed;
- const pos = self._mouseService.getRawByteCoords(ev, self.screenElement, self.cols, self.rows);
- if (!pos) return;
-
- // buttons marked as motions
- // are incremented by 32
- button += 32;
-
- sendEvent(button, pos);
- }
-
- // encode button and
- // position to characters
- function encode(data: number[], ch: number): void {
- if (!self.utfMouse) {
- if (ch === 255) {
- data.push(0);
- return;
+ /**
+ * Event listener state handling.
+ * We listen to the onProtocolChange event of CoreMouseService and put
+ * requested listeners in `requestedEvents`. With this the listeners
+ * have all bits to do the event listener juggling.
+ * Note: 'mousedown' currently is "always on" and not managed
+ * by onProtocolChange.
+ */
+ const requestedEvents: {[key: string]: ((ev: Event) => void) | null} = {
+ mouseup: null,
+ wheel: null,
+ mousedrag: null,
+ mousemove: null
+ };
+ const eventListeners: {[key: string]: (ev: Event) => void} = {
+ mouseup: (ev: MouseEvent) => {
+ sendEvent(ev);
+ if (!ev.buttons) {
+ // if no other button is held remove global handlers
+ this._document.removeEventListener('mouseup', requestedEvents.mouseup);
+ if (requestedEvents.mousedrag) {
+ this._document.removeEventListener('mousemove', requestedEvents.mousedrag);
+ }
}
- if (ch > 127) ch = 127;
- data.push(ch);
+ return this.cancel(ev);
+ },
+ wheel: (ev: WheelEvent) => {
+ sendEvent(ev);
+ ev.preventDefault();
+ return this.cancel(ev);
+ },
+ mousedrag: (ev: MouseEvent) => {
+ // deal only with move while a button is held
+ if (ev.buttons) {
+ sendEvent(ev);
+ }
+ },
+ mousemove: (ev: MouseEvent) => {
+ // deal only with move without any button
+ if (!ev.buttons) {
+ sendEvent(ev);
+ }
+ }
+ };
+ this._coreMouseService.onProtocolChange(events => {
+ // apply global changes on events
+ this.mouseEvents = events;
+ if (events) {
+ if (this.optionsService.options.logLevel === 'debug') {
+ this._logService.debug('Binding to mouse events:', this._coreMouseService.explainEvents(events));
+ }
+ this.element.classList.add('enable-mouse-events');
+ this._selectionService.disable();
} else {
- if (ch > 2047) {
- data.push(2047);
- return;
- }
- data.push(ch);
- }
- }
-
- // send a mouse event:
- // regular/utf8: ^[[M Cb Cx Cy
- // urxvt: ^[[ Cb ; Cx ; Cy M
- // sgr: ^[[ Cb ; Cx ; Cy M/m
- // vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r
- // locator: CSI P e ; P b ; P r ; P c ; P p & w
- function sendEvent(button: number, pos: {x: number, y: number}): void {
- if (self._vt300Mouse) {
- // NOTE: Unstable.
- // http://www.vt100.net/docs/vt3xx-gp/chapter15.html
- button &= 3;
- pos.x -= 32;
- pos.y -= 32;
- let data = C0.ESC + '[24';
- if (button === 0) data += '1';
- else if (button === 1) data += '3';
- else if (button === 2) data += '5';
- else if (button === 3) return;
- else data += '0';
- data += '~[' + pos.x + ',' + pos.y + ']\r';
- self._coreService.triggerDataEvent(data, true);
- return;
+ this._logService.debug('Unbinding from mouse events.');
+ this.element.classList.remove('enable-mouse-events');
+ this._selectionService.enable();
}
- if (self._decLocator) {
- // NOTE: Unstable.
- button &= 3;
- pos.x -= 32;
- pos.y -= 32;
- if (button === 0) button = 2;
- else if (button === 1) button = 4;
- else if (button === 2) button = 6;
- else if (button === 3) button = 3;
- self._coreService.triggerDataEvent(C0.ESC + '['
- + button
- + ';'
- + (button === 3 ? 4 : 0)
- + ';'
- + pos.y
- + ';'
- + pos.x
- + ';'
- // Not sure what page is meant to be
- + (pos).page || 0
- + '&w', true);
- return;
+ // add/remove handlers from requestedEvents
+
+ if (!(events & CoreMouseEventType.MOVE)) {
+ el.removeEventListener('mousemove', requestedEvents.mousemove);
+ requestedEvents.mousemove = null;
+ } else if (!requestedEvents.mousemove) {
+ el.addEventListener('mousemove', eventListeners.mousemove);
+ requestedEvents.mousemove = eventListeners.mousemove;
}
- if (self.urxvtMouse) {
- pos.x -= 32;
- pos.y -= 32;
- pos.x++;
- pos.y++;
- self._coreService.triggerDataEvent(C0.ESC + '[' + button + ';' + pos.x + ';' + pos.y + 'M', true);
- return;
+ if (!(events & CoreMouseEventType.WHEEL)) {
+ el.removeEventListener('wheel', requestedEvents.wheel);
+ requestedEvents.wheel = null;
+ } else if (!requestedEvents.wheel) {
+ el.addEventListener('wheel', eventListeners.wheel);
+ requestedEvents.wheel = eventListeners.wheel;
}
- if (self.sgrMouse) {
- pos.x -= 32;
- pos.y -= 32;
- self._coreService.triggerDataEvent(C0.ESC + '[<'
- + (((button & 3) === 3 ? button & ~3 : button) - 32)
- + ';'
- + pos.x
- + ';'
- + pos.y
- + ((button & 3) === 3 ? 'm' : 'M'), true);
- return;
+ if (!(events & CoreMouseEventType.UP)) {
+ this._document.removeEventListener('mouseup', requestedEvents.mouseup);
+ requestedEvents.mouseup = null;
+ } else if (!requestedEvents.mouseup) {
+ requestedEvents.mouseup = eventListeners.mouseup;
}
- const data: number[] = [];
-
- encode(data, button);
- encode(data, pos.x);
- encode(data, pos.y);
-
- self._coreService.triggerDataEvent(C0.ESC + '[M' + String.fromCharCode.apply(String, data), true);
- }
-
- function getButton(ev: MouseEvent): number {
- let button;
- let shift;
- let meta;
- let ctrl;
- let mod;
-
- // two low bits:
- // 0 = left
- // 1 = middle
- // 2 = right
- // 3 = release
- // wheel up/down:
- // 1, and 2 - with 64 added
- switch ((ev).overrideType || ev.type) {
- case 'mousedown':
- button = ev.button !== null && ev.button !== undefined
- ? +ev.button
- : ev.which !== null && ev.which !== undefined
- ? ev.which - 1
- : null;
- break;
- case 'mouseup':
- button = 3;
- break;
- case 'DOMMouseScroll':
- button = ev.detail < 0
- ? 64
- : 65;
- break;
- case 'wheel':
- button = (ev).deltaY < 0
- ? 64
- : 65;
- break;
+ if (!(events & CoreMouseEventType.DRAG)) {
+ this._document.removeEventListener('mousemove', requestedEvents.mousedrag);
+ requestedEvents.mousedrag = null;
+ } else if (!requestedEvents.mousedrag) {
+ requestedEvents.mousedrag = eventListeners.mousedrag;
}
+ });
+ // force initial onProtocolChange so we dont miss early mouse requests
+ this._coreMouseService.activeProtocol = this._coreMouseService.activeProtocol;
- // next three bits are the modifiers:
- // 4 = shift, 8 = meta, 16 = control
- shift = ev.shiftKey ? 4 : 0;
- meta = ev.metaKey ? 8 : 0;
- ctrl = ev.ctrlKey ? 16 : 0;
- mod = shift | meta | ctrl;
-
- // no mods
- if (self.vt200Mouse) {
- // ctrl only
- mod &= ctrl;
- } else if (!self.normalMouse) {
- mod = 0;
- }
-
- // increment to SP
- button = (32 + (mod << 2)) + button;
-
- return button;
- }
-
+ /**
+ * "Always on" event listeners.
+ */
this.register(addDisposableDomListener(el, 'mousedown', (ev: MouseEvent) => {
-
- // Prevent the focus on the textarea from getting lost
- // and make sure we get focused on mousedown
ev.preventDefault();
this.focus();
@@ -951,60 +850,24 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
return;
}
- // send the button
- sendButton(ev);
+ sendEvent(ev);
- // fix for odd bug
- // if (this.vt200Mouse && !this.normalMouse) {
- if (this.vt200Mouse) {
- (ev).overrideType = 'mouseup';
- sendButton(ev);
- return this.cancel(ev);
+ // Register additional global handlers which should keep reporting outside
+ // of the terminal element.
+ // Note: Other emulators also do this for 'mousedown' while a button
+ // is held, we currently limit 'mousedown' to the terminal only.
+ if (requestedEvents.mouseup) {
+ this._document.addEventListener('mouseup', requestedEvents.mouseup);
}
-
- // TODO: All mouse handling should be pulled into its own file.
-
- // bind events
- let moveHandler: (event: MouseEvent) => void;
- if (this.normalMouse) {
- moveHandler = (event: MouseEvent) => {
- // Do nothing if normal mouse mode is on. This can happen if the mouse is held down when the
- // terminal exits normalMouse mode.
- if (!this.normalMouse) {
- return;
- }
- sendMove(event);
- };
- // TODO: these event listeners should be managed by the disposable, the Terminal reference may
- // be kept aroud if Terminal.dispose is fired when the mouse is down
- this._document.addEventListener('mousemove', moveHandler);
+ if (requestedEvents.mousedrag) {
+ this._document.addEventListener('mousemove', requestedEvents.mousedrag);
}
- // x10 compatibility mode can't send button releases
- const handler = (ev: MouseEvent) => {
- if (this.normalMouse && !this.x10Mouse) {
- sendButton(ev);
- }
- if (moveHandler) {
- // Even though this should only be attached when this.normalMouse is true, holding the
- // mouse button down when normalMouse changes can happen. Just always try to remove it.
- this._document.removeEventListener('mousemove', moveHandler);
- moveHandler = null;
- }
- this._document.removeEventListener('mouseup', handler);
- return this.cancel(ev);
- };
- this._document.addEventListener('mouseup', handler);
-
return this.cancel(ev);
}));
- // if (this.normalMouse) {
- // on(this.document, 'mousemove', sendMove);
- // }
-
this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
- if (!this.mouseEvents) {
+ if (!requestedEvents.wheel) {
// Convert wheel events into up/down events when the buffer does not have scrollback, this
// enables scrolling in apps hosted in the alt buffer such as vim or tmux.
if (!this.buffer.hasScrollback) {
@@ -1025,17 +888,15 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
}
return;
}
- if (this.x10Mouse || this._vt300Mouse || this._decLocator) return;
- sendButton(ev);
- ev.preventDefault();
}));
// allow wheel scrolling in
// the shell for example
this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
- if (this.mouseEvents) return;
- this.viewport.onWheel(ev);
- return this.cancel(ev);
+ if (requestedEvents.wheel) return;
+ if (!this.viewport.onWheel(ev)) {
+ return this.cancel(ev);
+ }
}));
this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => {
@@ -1046,11 +907,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => {
if (this.mouseEvents) return;
- this.viewport.onTouchMove(ev);
- return this.cancel(ev);
+ if (!this.viewport.onTouchMove(ev)) {
+ return this.cancel(ev);
+ }
}));
}
+
/**
* Tells the renderer to refresh terminal content between two rows (inclusive) at the next
* opportunity.
@@ -1058,9 +921,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
* @param end The row to end at (between start and this.rows - 1).
*/
public refresh(start: number, end: number): void {
- if (this._renderService) {
- this._renderService.refreshRows(start, end);
- }
+ this._renderService?.refreshRows(start, end);
}
/**
@@ -1069,9 +930,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
* @param end The row to end at (between start and this.rows - 1).
*/
private _queueLinkification(start: number, end: number): void {
- if (this.linkifier) {
- this.linkifier.linkifyRows(start, end);
- }
+ this.linkifier?.linkifyRows(start, end);
}
/**
@@ -1089,8 +948,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
* Display the cursor element
*/
public showCursor(): void {
- if (!this.cursorState) {
- this.cursorState = 1;
+ if (!this._coreService.isCursorInitialized) {
+ this._coreService.isCursorInitialized = true;
this.refresh(this.buffer.y, this.buffer.y);
}
}
@@ -1222,166 +1081,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
}
}
- /**
- * Writes raw utf8 bytes to the terminal.
- * @param data UintArray with UTF8 bytes to write to the terminal.
- */
- public writeUtf8(data: Uint8Array): void {
- // Ensure the terminal isn't disposed
- if (this._isDisposed) {
- return;
- }
-
- // Ignore falsy data values
- if (!data) {
- return;
- }
-
- this.writeBufferUtf8.push(data);
-
- // Send XOFF to pause the pty process if the write buffer becomes too large so
- // xterm.js can catch up before more data is sent. This is necessary in order
- // to keep signals such as ^C responsive.
- if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBufferUtf8.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
- // XOFF - stop pty pipe
- // XON will be triggered by emulator before processing data chunk
- this._coreService.triggerDataEvent(C0.DC3);
- this._xoffSentToCatchUp = true;
- }
-
- if (!this._writeInProgress && this.writeBufferUtf8.length > 0) {
- // Kick off a write which will write all data in sequence recursively
- this._writeInProgress = true;
- // Kick off an async innerWrite so more writes can come in while processing data
- setTimeout(() => {
- this._innerWriteUtf8();
- });
- }
- }
-
- protected _innerWriteUtf8(bufferOffset: number = 0): void {
- // Ensure the terminal isn't disposed
- if (this._isDisposed) {
- this.writeBufferUtf8 = [];
- }
-
- const startTime = Date.now();
- while (this.writeBufferUtf8.length > bufferOffset) {
- const data = this.writeBufferUtf8[bufferOffset];
- bufferOffset++;
-
- // If XOFF was sent in order to catch up with the pty process, resume it if
- // we reached the end of the writeBuffer to allow more data to come in.
- if (this._xoffSentToCatchUp && this.writeBufferUtf8.length === bufferOffset) {
- this._coreService.triggerDataEvent(C0.DC1);
- this._xoffSentToCatchUp = false;
- }
-
- this._inputHandler.parseUtf8(data);
-
- this.refresh(this._dirtyRowService.start, this._dirtyRowService.end);
-
- if (Date.now() - startTime >= WRITE_TIMEOUT_MS) {
- break;
- }
- }
- if (this.writeBufferUtf8.length > bufferOffset) {
- // Allow renderer to catch up before processing the next batch
- // trim already processed chunks if we are above threshold
- if (bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) {
- this.writeBufferUtf8 = this.writeBufferUtf8.slice(bufferOffset);
- bufferOffset = 0;
- }
- setTimeout(() => this._innerWriteUtf8(bufferOffset), 0);
- } else {
- this._writeInProgress = false;
- this.writeBufferUtf8 = [];
- }
- }
-
- /**
- * Writes text to the terminal.
- * @param data The text to write to the terminal.
- */
- public write(data: string): void {
- // Ensure the terminal isn't disposed
- if (this._isDisposed) {
- return;
- }
-
- // Ignore falsy data values (including the empty string)
- if (!data) {
- return;
- }
-
- this.writeBuffer.push(data);
-
- // Send XOFF to pause the pty process if the write buffer becomes too large so
- // xterm.js can catch up before more data is sent. This is necessary in order
- // to keep signals such as ^C responsive.
- if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
- // XOFF - stop pty pipe
- // XON will be triggered by emulator before processing data chunk
- this._coreService.triggerDataEvent(C0.DC3);
- this._xoffSentToCatchUp = true;
- }
-
- if (!this._writeInProgress && this.writeBuffer.length > 0) {
- // Kick off a write which will write all data in sequence recursively
- this._writeInProgress = true;
- // Kick off an async innerWrite so more writes can come in while processing data
- setTimeout(() => {
- this._innerWrite();
- });
- }
- }
-
- protected _innerWrite(bufferOffset: number = 0): void {
- // Ensure the terminal isn't disposed
- if (this._isDisposed) {
- this.writeBuffer = [];
- }
-
- const startTime = Date.now();
- while (this.writeBuffer.length > bufferOffset) {
- const data = this.writeBuffer[bufferOffset];
- bufferOffset++;
-
- // If XOFF was sent in order to catch up with the pty process, resume it if
- // we reached the end of the writeBuffer to allow more data to come in.
- if (this._xoffSentToCatchUp && this.writeBuffer.length === bufferOffset) {
- this._coreService.triggerDataEvent(C0.DC1);
- this._xoffSentToCatchUp = false;
- }
-
- this._inputHandler.parse(data);
-
- this.refresh(this._dirtyRowService.start, this._dirtyRowService.end);
-
- if (Date.now() - startTime >= WRITE_TIMEOUT_MS) {
- break;
- }
- }
- if (this.writeBuffer.length > bufferOffset) {
- // Allow renderer to catch up before processing the next batch
- // trim already processed chunks if we are above threshold
- if (bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) {
- this.writeBuffer = this.writeBuffer.slice(bufferOffset);
- bufferOffset = 0;
- }
- setTimeout(() => this._innerWrite(bufferOffset), 0);
- } else {
- this._writeInProgress = false;
- this.writeBuffer = [];
- }
- }
-
- /**
- * Writes text to the terminal, followed by a break line character (\n).
- * @param data The text to write to the terminal.
- */
- public writeln(data: string): void {
- this.write(data + '\r\n');
+ public paste(data: string): void {
+ paste(data, this.textarea, this.bracketedPasteMode, this._coreService);
}
/**
@@ -1397,9 +1098,19 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._customKeyEventHandler = customKeyEventHandler;
}
+ /** Add handler for ESC escape sequence. See xterm.d.ts for details. */
+ public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable {
+ return this._inputHandler.addEscHandler(id, callback);
+ }
+
+ /** Add handler for DCS escape sequence. See xterm.d.ts for details. */
+ public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
+ return this._inputHandler.addDcsHandler(id, callback);
+ }
+
/** Add handler for CSI escape sequence. See xterm.d.ts for details. */
- public addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable {
- return this._inputHandler.addCsiHandler(flag, callback);
+ public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
+ return this._inputHandler.addCsiHandler(id, callback);
}
/** Add handler for OSC escape sequence. See xterm.d.ts for details. */
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
@@ -1499,24 +1210,18 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
* Clears the current terminal selection.
*/
public clearSelection(): void {
- if (this._selectionService) {
- this._selectionService.clearSelection();
- }
+ this._selectionService?.clearSelection();
}
/**
* Selects all text within the terminal.
*/
public selectAll(): void {
- if (this._selectionService) {
- this._selectionService.selectAll();
- }
+ this._selectionService?.selectAll();
}
public selectLines(start: number, end: number): void {
- if (this._selectionService) {
- this._selectionService.selectLines(start, end);
- }
+ this._selectionService?.selectLines(start, end);
}
/**
@@ -1723,9 +1428,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._bufferService.resize(x, y);
this.buffers.setupTabStops(this.cols);
- if (this._charSizeService) {
- this._charSizeService.measure();
- }
+ this._charSizeService?.measure();
+
+ // Sync the scroll area to make sure scroll events don't fire and scroll the viewport to an
+ // invalid location
+ this.viewport.syncScrollArea(true);
this.refresh(0, this.rows - 1);
this._onResize.fire({ cols: x, rows: y });
@@ -1806,35 +1513,22 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.options.cols = this.cols;
const customKeyEventHandler = this._customKeyEventHandler;
const inputHandler = this._inputHandler;
- const cursorState = this.cursorState;
- const writeBuffer = this.writeBuffer;
- const writeBufferUtf8 = this.writeBufferUtf8;
- const writeInProgress = this._writeInProgress;
- const xoffSentToCatchUp = this._xoffSentToCatchUp;
const userScrolling = this._userScrolling;
this._setup();
this._bufferService.reset();
this._coreService.reset();
- if (this._selectionService) {
- this._selectionService.reset();
- }
+ this._coreMouseService.reset();
+ this._selectionService?.reset();
// reattach
this._customKeyEventHandler = customKeyEventHandler;
this._inputHandler = inputHandler;
- this.cursorState = cursorState;
- this.writeBuffer = writeBuffer;
- this.writeBufferUtf8 = writeBufferUtf8;
- this._writeInProgress = writeInProgress;
- this._xoffSentToCatchUp = xoffSentToCatchUp;
this._userScrolling = userScrolling;
// do a full screen refresh
this.refresh(0, this.rows - 1);
- if (this.viewport) {
- this.viewport.syncScrollArea();
- }
+ this.viewport?.syncScrollArea();
}
// TODO: Remove cancel function and cancelEvents option
@@ -1858,6 +1552,14 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
// return this.options.bellStyle === 'sound' ||
// this.options.bellStyle === 'both';
}
+
+ public write(data: string | Uint8Array, callback?: () => void): void {
+ this._writeBuffer.write(data, callback);
+ }
+
+ public writeSync(data: string | Uint8Array): void {
+ this._writeBuffer.writeSync(data);
+ }
}
/**
diff --git a/src/Terminal2.test.ts b/src/Terminal2.test.ts
index cbd790d5..f7046240 100644
--- a/src/Terminal2.test.ts
+++ b/src/Terminal2.test.ts
@@ -17,12 +17,6 @@ const ROWS = 25;
const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
const SKIP_FILES = [
- 't0070-DECSTBM_LF.in', // lineFeed not working correctly
- 't0071-DECSTBM_IND.in',
- 't0072-DECSTBM_NEL.in',
- 't0075-DECSTBM_CUU_CUD.in',
- 't0076-DECSTBM_IL_DL.in', // not working due to lineFeed
- 't0077-DECSTBM_quirks.in',
't0084-CBT.in',
't0101-NLM.in',
't0103-reverse_wrap.in',
@@ -40,9 +34,8 @@ if (os.platform() === 'darwin') {
// filter skipFilenames
const FILES = TESTFILES.filter(value => SKIP_FILES.indexOf(value.split('/').slice(-1)[0]) === -1);
-
describe('Escape Sequence Files', function(): void {
- this.timeout(20000);
+ this.timeout(1000);
let ptyTerm: any;
let slaveEnd: any;
@@ -50,6 +43,9 @@ describe('Escape Sequence Files', function(): void {
let customHandler: IDisposable | undefined;
before(() => {
+ if (process.platform === 'win32') {
+ return;
+ }
ptyTerm = (pty as any).open({cols: COLS, rows: ROWS});
slaveEnd = ptyTerm._slave;
term = new Terminal({cols: COLS, rows: ROWS});
@@ -57,12 +53,15 @@ describe('Escape Sequence Files', function(): void {
});
after(() => {
+ if (process.platform === 'win32') {
+ return;
+ }
ptyTerm._master.end();
ptyTerm._master.destroy();
});
FILES.forEach(filename => {
- it(filename.split('/').slice(-1)[0], async () => {
+ (process.platform === 'win32' ? it.skip : it)(filename.split('/').slice(-1)[0], async () => {
// reset terminal and handler
if (customHandler) {
customHandler.dispose();
diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts
index 0ee948d5..84c0d0b7 100644
--- a/src/TestUtils.test.ts
+++ b/src/TestUtils.test.ts
@@ -3,10 +3,10 @@
* @license MIT
*/
-import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types';
+import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
import { IInputHandlingTerminal, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions } from './Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
-import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset } from 'common/Types';
+import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, CoreMouseEventType } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
import * as Browser from 'common/Platform';
import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm';
@@ -15,14 +15,10 @@ import { AttributeData } from 'common/buffer/AttributeData';
import { IColorManager, IColorSet, ILinkMatcherOptions, ILinkifier, IViewport } from 'browser/Types';
import { IOptionsService } from 'common/services/Services';
import { EventEmitter } from 'common/EventEmitter';
-import { IParams } from 'common/parser/Types';
+import { IParams, IFunctionIdentifier } from 'common/parser/Types';
import { ISelectionService } from 'browser/services/Services';
export class TestTerminal extends Terminal {
- writeSync(data: string): void {
- this.writeBuffer.push(data);
- this._innerWrite();
- }
keyDown(ev: any): boolean { return this._keyDown(ev); }
keyPress(ev: any): boolean { return this._keyPress(ev); }
}
@@ -36,6 +32,7 @@ export class MockTerminal implements ITerminal {
onLineFeed: IEvent;
onSelectionChange: IEvent;
onData: IEvent;
+ onBinary: IEvent;
onTitleChange: IEvent;
onScroll: IEvent;
onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>;
@@ -68,17 +65,26 @@ export class MockTerminal implements ITerminal {
writeln(data: string): void {
throw new Error('Method not implemented.');
}
+ paste(data: string): void {
+ throw new Error('Method not implemented.');
+ }
open(parent: HTMLElement): void {
throw new Error('Method not implemented.');
}
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
throw new Error('Method not implemented.');
}
- addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable {
- throw new Error('Method not implemented.');
+ addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
+ throw new Error('Method not implemented.');
+ }
+ addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
+ throw new Error('Method not implemented.');
+ }
+ addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
+ throw new Error('Method not implemented.');
}
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
- throw new Error('Method not implemented.');
+ throw new Error('Method not implemented.');
}
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
throw new Error('Method not implemented.');
@@ -210,7 +216,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
x10Mouse: boolean;
vt200Mouse: boolean;
normalMouse: boolean;
- mouseEvents: boolean;
+ mouseEvents: CoreMouseEventType;
sendFocus: boolean;
utfMouse: boolean;
sgrMouse: boolean;
@@ -357,6 +363,7 @@ export class MockBuffer implements IBuffer {
}
export class MockRenderer implements IRenderer {
+ onRequestRefreshRows: IEvent;
onCanvasResize: IEvent<{ width: number; height: number; }>;
onRender: IEvent<{ start: number; end: number; }>;
dispose(): void {
@@ -401,13 +408,13 @@ export class MockViewport implements IViewport {
onThemeChange(colors: IColorSet): void {
throw new Error('Method not implemented.');
}
- onWheel(ev: WheelEvent): void {
+ onWheel(ev: WheelEvent): boolean {
throw new Error('Method not implemented.');
}
onTouchStart(ev: TouchEvent): void {
throw new Error('Method not implemented.');
}
- onTouchMove(ev: TouchEvent): void {
+ onTouchMove(ev: TouchEvent): boolean {
throw new Error('Method not implemented.');
}
syncScrollArea(): void { }
diff --git a/src/Types.d.ts b/src/Types.d.ts
index f96ba07d..f4d3a556 100644
--- a/src/Types.d.ts
+++ b/src/Types.d.ts
@@ -4,12 +4,12 @@
*/
import { ITerminalOptions as IPublicTerminalOptions, IDisposable, IMarker, ISelectionPosition } from 'xterm';
-import { ICharset, IAttributeData, CharData } from 'common/Types';
+import { ICharset, IAttributeData, CharData, CoreMouseEventType } from 'common/Types';
import { IEvent, IEventEmitter } from 'common/EventEmitter';
import { IColorSet, ILinkifier, ILinkMatcherOptions, IViewport } from 'browser/Types';
import { IOptionsService } from 'common/services/Services';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
-import { IParams } from 'common/parser/Types';
+import { IParams, IFunctionIdentifier } from 'common/parser/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
@@ -36,15 +36,8 @@ export interface IInputHandlingTerminal {
bracketedPasteMode: boolean;
curAttrData: IAttributeData;
savedCols: number;
- x10Mouse: boolean;
- vt200Mouse: boolean;
- normalMouse: boolean;
- mouseEvents: boolean;
+ mouseEvents: CoreMouseEventType;
sendFocus: boolean;
- utfMouse: boolean;
- sgrMouse: boolean;
- urxvtMouse: boolean;
- cursorHidden: boolean;
buffers: IBufferSet;
buffer: IBuffer;
@@ -79,8 +72,7 @@ export interface ICompositionHelper {
* Calls the parser and handles actions generated by the parser.
*/
export interface IInputHandler {
- parse(data: string): void;
- parseUtf8(data: Uint8Array): void;
+ parse(data: string | Uint8Array): void;
print(data: Uint32Array, start: number, end: number): void;
/** C0 BEL */ bell(): void;
@@ -92,7 +84,9 @@ export interface IInputHandler {
/** C0 SI */ shiftIn(): void;
/** CSI @ */ insertChars(params: IParams): void;
+ /** CSI SP @ */ scrollLeft(params: IParams): void;
/** CSI A */ cursorUp(params: IParams): void;
+ /** CSI SP A */ scrollRight(params: IParams): void;
/** CSI B */ cursorDown(params: IParams): void;
/** CSI C */ cursorForward(params: IParams): void;
/** CSI D */ cursorBackward(params: IParams): void;
@@ -113,7 +107,8 @@ export interface IInputHandler {
/** CSI ` */ charPosAbsolute(params: IParams): void;
/** CSI a */ hPositionRelative(params: IParams): void;
/** CSI b */ repeatPrecedingCharacter(params: IParams): void;
- /** CSI c */ sendDeviceAttributes(params: IParams, collect?: string): void;
+ /** CSI c */ sendDeviceAttributesPrimary(params: IParams): void;
+ /** CSI > c */ sendDeviceAttributesSecondary(params: IParams): void;
/** CSI d */ linePosAbsolute(params: IParams): void;
/** CSI e */ vPositionRelative(params: IParams): void;
/** CSI f */ hVPosition(params: IParams): void;
@@ -127,6 +122,8 @@ export interface IInputHandler {
/** CSI r */ setScrollRegion(params: IParams, collect?: string): void;
/** CSI s */ saveCursor(params: IParams): void;
/** CSI u */ restoreCursor(params: IParams): void;
+ /** CSI ' } */ insertColumns(params: IParams): void;
+ /** CSI ' ~ */ deleteColumns(params: IParams): void;
/** OSC 0
OSC 2 */ setTitle(data: string): void;
/** ESC E */ nextLine(): void;
@@ -156,12 +153,8 @@ export interface IInputHandler {
export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
screenElement: HTMLElement;
browser: IBrowser;
- writeBuffer: string[];
- cursorHidden: boolean;
- cursorState: number;
buffer: IBuffer;
buffers: IBufferSet;
- isFocused: boolean;
viewport: IViewport;
bracketedPasteMode: boolean;
optionsService: IOptionsService;
@@ -180,13 +173,14 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc
// Portions of the public API that are required by the internal Terminal
export interface IPublicTerminal extends IDisposable {
- textarea: HTMLTextAreaElement;
+ textarea: HTMLTextAreaElement | undefined;
rows: number;
cols: number;
buffer: IBuffer;
markers: IMarker[];
onCursorMove: IEvent;
onData: IEvent;
+ onBinary: IEvent;
onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>;
onLineFeed: IEvent;
onScroll: IEvent;
@@ -197,10 +191,11 @@ export interface IPublicTerminal extends IDisposable {
blur(): void;
focus(): void;
resize(columns: number, rows: number): void;
- writeln(data: string): void;
open(parent: HTMLElement): void;
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
- addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable;
+ addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable;
+ addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable;
+ addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): void;
@@ -221,8 +216,8 @@ export interface IPublicTerminal extends IDisposable {
scrollToBottom(): void;
scrollToLine(line: number): void;
clear(): void;
- write(data: string): void;
- writeUtf8(data: Uint8Array): void;
+ write(data: string | Uint8Array, callback?: () => void): void;
+ paste(data: string): void;
refresh(start: number, end: number): void;
reset(): void;
}
@@ -232,7 +227,7 @@ export interface IBufferAccessor {
}
export interface IElementAccessor {
- readonly element: HTMLElement;
+ readonly element: HTMLElement | undefined;
}
export interface ILinkifierAccessor {
diff --git a/src/browser/Clipboard.ts b/src/browser/Clipboard.ts
index f56868cc..a7c48bfb 100644
--- a/src/browser/Clipboard.ts
+++ b/src/browser/Clipboard.ts
@@ -4,6 +4,7 @@
*/
import { ISelectionService } from 'browser/services/Services';
+import { ICoreService } from 'common/services/Services';
/**
* Prepares text to be pasted into the terminal by normalizing the line endings
@@ -41,24 +42,21 @@ export function copyHandler(ev: ClipboardEvent, selectionService: ISelectionServ
* @param ev The original paste event to be handled
* @param term The terminal on which to apply the handled paste event
*/
-export function pasteHandler(ev: ClipboardEvent, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, triggerUserInput: (data: string) => void): void {
+export function handlePasteEvent(ev: ClipboardEvent, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, coreService: ICoreService): void {
ev.stopPropagation();
-
- let text: string;
-
- const dispatchPaste = function(text: string): void {
- text = prepareTextForTerminal(text);
- text = bracketTextForPaste(text, bracketedPasteMode);
- triggerUserInput(text);
- textarea.value = '';
- };
-
if (ev.clipboardData) {
- text = ev.clipboardData.getData('text/plain');
- dispatchPaste(text);
+ const text = ev.clipboardData.getData('text/plain');
+ paste(text, textarea, bracketedPasteMode, coreService);
}
}
+export function paste(text: string, textarea: HTMLTextAreaElement, bracketedPasteMode: boolean, coreService: ICoreService): void {
+ text = prepareTextForTerminal(text);
+ text = bracketTextForPaste(text, bracketedPasteMode);
+ coreService.triggerDataEvent(text, true);
+ textarea.value = '';
+}
+
/**
* Moves the textarea under the mouse cursor and focuses it.
* @param ev The original right click event to be handled.
@@ -84,12 +82,12 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
// Reset the terminal textarea's styling
// Timeout needs to be long enough for click event to be handled.
setTimeout(() => {
- textarea.style.position = null;
- textarea.style.width = null;
- textarea.style.height = null;
- textarea.style.left = null;
- textarea.style.top = null;
- textarea.style.zIndex = null;
+ textarea.style.position = '';
+ textarea.style.width = '';
+ textarea.style.height = '';
+ textarea.style.left = '';
+ textarea.style.top = '';
+ textarea.style.zIndex = '';
}, 200);
}
diff --git a/src/browser/Color.test.ts b/src/browser/Color.test.ts
new file mode 100644
index 00000000..673fffc8
--- /dev/null
+++ b/src/browser/Color.test.ts
@@ -0,0 +1,222 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { assert } from 'chai';
+import { blend, fromCss, toPaddedHex, toCss, toRgba, rgbRelativeLuminance, contrastRatio, ensureContrastRatioRgba } from 'browser/Color';
+
+describe('Color', () => {
+ describe('blend', () => {
+ it('should blend colors based on the alpha channel', () => {
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF00', rgba: 0xFFFFFF00 }), { css: '#000000', rgba: 0x000000FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF10', rgba: 0xFFFFFF10 }), { css: '#101010', rgba: 0x101010FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF20', rgba: 0xFFFFFF20 }), { css: '#202020', rgba: 0x202020FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF30', rgba: 0xFFFFFF30 }), { css: '#303030', rgba: 0x303030FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF40', rgba: 0xFFFFFF40 }), { css: '#404040', rgba: 0x404040FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF50', rgba: 0xFFFFFF50 }), { css: '#505050', rgba: 0x505050FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF60', rgba: 0xFFFFFF60 }), { css: '#606060', rgba: 0x606060FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF70', rgba: 0xFFFFFF70 }), { css: '#707070', rgba: 0x707070FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF80', rgba: 0xFFFFFF80 }), { css: '#808080', rgba: 0x808080FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF90', rgba: 0xFFFFFF90 }), { css: '#909090', rgba: 0x909090FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFA0', rgba: 0xFFFFFFA0 }), { css: '#a0a0a0', rgba: 0xA0A0A0FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFB0', rgba: 0xFFFFFFB0 }), { css: '#b0b0b0', rgba: 0xB0B0B0FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFC0', rgba: 0xFFFFFFC0 }), { css: '#c0c0c0', rgba: 0xC0C0C0FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFD0', rgba: 0xFFFFFFD0 }), { css: '#d0d0d0', rgba: 0xD0D0D0FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFE0', rgba: 0xFFFFFFE0 }), { css: '#e0e0e0', rgba: 0xE0E0E0FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFF0', rgba: 0xFFFFFFF0 }), { css: '#f0f0f0', rgba: 0xF0F0F0FF });
+ assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFFF', rgba: 0xFFFFFFFF }), { css: '#FFFFFFFF', rgba: 0xFFFFFFFF });
+ });
+ });
+
+ describe('fromCss', () => {
+ it('should covert a CSS string to an IColor', () => {
+ assert.deepEqual(fromCss('#000000'), { css: '#000000', rgba: 0x000000FF });
+ assert.deepEqual(fromCss('#101010'), { css: '#101010', rgba: 0x101010FF });
+ assert.deepEqual(fromCss('#202020'), { css: '#202020', rgba: 0x202020FF });
+ assert.deepEqual(fromCss('#303030'), { css: '#303030', rgba: 0x303030FF });
+ assert.deepEqual(fromCss('#404040'), { css: '#404040', rgba: 0x404040FF });
+ assert.deepEqual(fromCss('#505050'), { css: '#505050', rgba: 0x505050FF });
+ assert.deepEqual(fromCss('#606060'), { css: '#606060', rgba: 0x606060FF });
+ assert.deepEqual(fromCss('#707070'), { css: '#707070', rgba: 0x707070FF });
+ assert.deepEqual(fromCss('#808080'), { css: '#808080', rgba: 0x808080FF });
+ assert.deepEqual(fromCss('#909090'), { css: '#909090', rgba: 0x909090FF });
+ assert.deepEqual(fromCss('#a0a0a0'), { css: '#a0a0a0', rgba: 0xa0a0a0FF });
+ assert.deepEqual(fromCss('#b0b0b0'), { css: '#b0b0b0', rgba: 0xb0b0b0FF });
+ assert.deepEqual(fromCss('#c0c0c0'), { css: '#c0c0c0', rgba: 0xc0c0c0FF });
+ assert.deepEqual(fromCss('#d0d0d0'), { css: '#d0d0d0', rgba: 0xd0d0d0FF });
+ assert.deepEqual(fromCss('#e0e0e0'), { css: '#e0e0e0', rgba: 0xe0e0e0FF });
+ assert.deepEqual(fromCss('#f0f0f0'), { css: '#f0f0f0', rgba: 0xf0f0f0FF });
+ assert.deepEqual(fromCss('#ffffff'), { css: '#ffffff', rgba: 0xffffffFF });
+ });
+ });
+
+ describe('toPaddedHex', () => {
+ it('should convert numbers to 2-digit hex values', () => {
+ assert.equal(toPaddedHex(0x00), '00');
+ assert.equal(toPaddedHex(0x10), '10');
+ assert.equal(toPaddedHex(0x20), '20');
+ assert.equal(toPaddedHex(0x30), '30');
+ assert.equal(toPaddedHex(0x40), '40');
+ assert.equal(toPaddedHex(0x50), '50');
+ assert.equal(toPaddedHex(0x60), '60');
+ assert.equal(toPaddedHex(0x70), '70');
+ assert.equal(toPaddedHex(0x80), '80');
+ assert.equal(toPaddedHex(0x90), '90');
+ assert.equal(toPaddedHex(0xa0), 'a0');
+ assert.equal(toPaddedHex(0xb0), 'b0');
+ assert.equal(toPaddedHex(0xc0), 'c0');
+ assert.equal(toPaddedHex(0xd0), 'd0');
+ assert.equal(toPaddedHex(0xe0), 'e0');
+ assert.equal(toPaddedHex(0xf0), 'f0');
+ assert.equal(toPaddedHex(0xff), 'ff');
+ });
+ });
+
+ describe('toCss', () => {
+ it('should convert an rgb array to css hex string', () => {
+ assert.equal(toCss(0x00, 0x00, 0x00), '#000000');
+ assert.equal(toCss(0x10, 0x10, 0x10), '#101010');
+ assert.equal(toCss(0x20, 0x20, 0x20), '#202020');
+ assert.equal(toCss(0x30, 0x30, 0x30), '#303030');
+ assert.equal(toCss(0x40, 0x40, 0x40), '#404040');
+ assert.equal(toCss(0x50, 0x50, 0x50), '#505050');
+ assert.equal(toCss(0x60, 0x60, 0x60), '#606060');
+ assert.equal(toCss(0x70, 0x70, 0x70), '#707070');
+ assert.equal(toCss(0x80, 0x80, 0x80), '#808080');
+ assert.equal(toCss(0x90, 0x90, 0x90), '#909090');
+ assert.equal(toCss(0xa0, 0xa0, 0xa0), '#a0a0a0');
+ assert.equal(toCss(0xb0, 0xb0, 0xb0), '#b0b0b0');
+ assert.equal(toCss(0xc0, 0xc0, 0xc0), '#c0c0c0');
+ assert.equal(toCss(0xd0, 0xd0, 0xd0), '#d0d0d0');
+ assert.equal(toCss(0xe0, 0xe0, 0xe0), '#e0e0e0');
+ assert.equal(toCss(0xf0, 0xf0, 0xf0), '#f0f0f0');
+ assert.equal(toCss(0xff, 0xff, 0xff), '#ffffff');
+ });
+ });
+
+ describe('toRgba', () => {
+ it('should convert an rgb array to an rgba number', () => {
+ assert.equal(toRgba(0x00, 0x00, 0x00), 0x000000FF);
+ assert.equal(toRgba(0x10, 0x10, 0x10), 0x101010FF);
+ assert.equal(toRgba(0x20, 0x20, 0x20), 0x202020FF);
+ assert.equal(toRgba(0x30, 0x30, 0x30), 0x303030FF);
+ assert.equal(toRgba(0x40, 0x40, 0x40), 0x404040FF);
+ assert.equal(toRgba(0x50, 0x50, 0x50), 0x505050FF);
+ assert.equal(toRgba(0x60, 0x60, 0x60), 0x606060FF);
+ assert.equal(toRgba(0x70, 0x70, 0x70), 0x707070FF);
+ assert.equal(toRgba(0x80, 0x80, 0x80), 0x808080FF);
+ assert.equal(toRgba(0x90, 0x90, 0x90), 0x909090FF);
+ assert.equal(toRgba(0xa0, 0xa0, 0xa0), 0xa0a0a0FF);
+ assert.equal(toRgba(0xb0, 0xb0, 0xb0), 0xb0b0b0FF);
+ assert.equal(toRgba(0xc0, 0xc0, 0xc0), 0xc0c0c0FF);
+ assert.equal(toRgba(0xd0, 0xd0, 0xd0), 0xd0d0d0FF);
+ assert.equal(toRgba(0xe0, 0xe0, 0xe0), 0xe0e0e0FF);
+ assert.equal(toRgba(0xf0, 0xf0, 0xf0), 0xf0f0f0FF);
+ assert.equal(toRgba(0xff, 0xff, 0xff), 0xffffffFF);
+ });
+ it('should convert an rgba array to an rgba number', () => {
+ assert.equal(toRgba(0x00, 0x00, 0x00, 0x00), 0x00000000);
+ assert.equal(toRgba(0x10, 0x10, 0x10, 0x10), 0x10101010);
+ assert.equal(toRgba(0x20, 0x20, 0x20, 0x20), 0x20202020);
+ assert.equal(toRgba(0x30, 0x30, 0x30, 0x30), 0x30303030);
+ assert.equal(toRgba(0x40, 0x40, 0x40, 0x40), 0x40404040);
+ assert.equal(toRgba(0x50, 0x50, 0x50, 0x50), 0x50505050);
+ assert.equal(toRgba(0x60, 0x60, 0x60, 0x60), 0x60606060);
+ assert.equal(toRgba(0x70, 0x70, 0x70, 0x70), 0x70707070);
+ assert.equal(toRgba(0x80, 0x80, 0x80, 0x80), 0x80808080);
+ assert.equal(toRgba(0x90, 0x90, 0x90, 0x90), 0x90909090);
+ assert.equal(toRgba(0xa0, 0xa0, 0xa0, 0xa0), 0xa0a0a0a0);
+ assert.equal(toRgba(0xb0, 0xb0, 0xb0, 0xb0), 0xb0b0b0b0);
+ assert.equal(toRgba(0xc0, 0xc0, 0xc0, 0xc0), 0xc0c0c0c0);
+ assert.equal(toRgba(0xd0, 0xd0, 0xd0, 0xd0), 0xd0d0d0d0);
+ assert.equal(toRgba(0xe0, 0xe0, 0xe0, 0xe0), 0xe0e0e0e0);
+ assert.equal(toRgba(0xf0, 0xf0, 0xf0, 0xf0), 0xf0f0f0f0);
+ assert.equal(toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff);
+ });
+ });
+ describe('rgbRelativeLuminance', () => {
+ it('should calculate the relative luminance of the color', () => {
+ assert.equal(rgbRelativeLuminance(0x000000), 0);
+ assert.equal(rgbRelativeLuminance(0x101010).toFixed(4), '0.0052');
+ assert.equal(rgbRelativeLuminance(0x202020).toFixed(4), '0.0144');
+ assert.equal(rgbRelativeLuminance(0x303030).toFixed(4), '0.0296');
+ assert.equal(rgbRelativeLuminance(0x404040).toFixed(4), '0.0513');
+ assert.equal(rgbRelativeLuminance(0x505050).toFixed(4), '0.0802');
+ assert.equal(rgbRelativeLuminance(0x606060).toFixed(4), '0.1170');
+ assert.equal(rgbRelativeLuminance(0x707070).toFixed(4), '0.1620');
+ assert.equal(rgbRelativeLuminance(0x808080).toFixed(4), '0.2159');
+ assert.equal(rgbRelativeLuminance(0x909090).toFixed(4), '0.2789');
+ assert.equal(rgbRelativeLuminance(0xA0A0A0).toFixed(4), '0.3515');
+ assert.equal(rgbRelativeLuminance(0xB0B0B0).toFixed(4), '0.4342');
+ assert.equal(rgbRelativeLuminance(0xC0C0C0).toFixed(4), '0.5271');
+ assert.equal(rgbRelativeLuminance(0xD0D0D0).toFixed(4), '0.6308');
+ assert.equal(rgbRelativeLuminance(0xE0E0E0).toFixed(4), '0.7454');
+ assert.equal(rgbRelativeLuminance(0xF0F0F0).toFixed(4), '0.8714');
+ assert.equal(rgbRelativeLuminance(0xFFFFFF), 1);
+ });
+ });
+ describe('contrastRatio', () => {
+ it('should calculate the relative luminance of the color', () => {
+ assert.equal(contrastRatio(0, 0), 1);
+ assert.equal(contrastRatio(0, 0.5), 11);
+ assert.equal(contrastRatio(0, 1), 21);
+ });
+ it('should work regardless of the parameter order', () => {
+ assert.equal(contrastRatio(0, 1), 21);
+ assert.equal(contrastRatio(1, 0), 21);
+ });
+ });
+ describe('ensureContrastRatioRgba', () => {
+ it('should return undefined if the color already meets the contrast ratio (black bg)', () => {
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 1), undefined);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 2), undefined);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 3), undefined);
+ });
+ it('should return a color that meets the contrast ratio (black bg)', () => {
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 4), 0x707070ff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 5), 0x7f7f7fff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 6), 0x8c8c8cff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 7), 0x989898ff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 8), 0xa3a3a3ff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 9), 0xadadadff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 10), 0xb6b6b6ff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 11), 0xbebebeff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 12), 0xc5c5c5ff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 13), 0xd1d1d1ff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 14), 0xd6d6d6ff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 15), 0xdbdbdbff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 16), 0xe3e3e3ff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 17), 0xe9e9e9ff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 18), 0xeeeeeeff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 19), 0xf4f4f4ff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 20), 0xfafafaff);
+ assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 21), 0xffffffff);
+ });
+ it('should return undefined if the color already meets the contrast ratio (white bg)', () => {
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 1), undefined);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 2), undefined);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 3), undefined);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 4), undefined);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 5), undefined);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 6), undefined);
+ });
+ it('should return a color that meets the contrast ratio (white bg)', () => {
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 7), 0x565656ff);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 8), 0x4d4d4dff);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 9), 0x454545ff);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 10), 0x3e3e3eff);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 11), 0x373737ff);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 12), 0x313131ff);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 13), 0x313131ff);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 14), 0x272727ff);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 15), 0x232323ff);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 16), 0x1f1f1fff);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 17), 0x1b1b1bff);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 18), 0x151515ff);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 19), 0x101010ff);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 20), 0x080808ff);
+ assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 21), 0x000000ff);
+ });
+ });
+});
diff --git a/src/browser/Color.ts b/src/browser/Color.ts
new file mode 100644
index 00000000..d4cbfe0e
--- /dev/null
+++ b/src/browser/Color.ts
@@ -0,0 +1,165 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { IColor } from 'browser/Types';
+
+export function blend(bg: IColor, fg: IColor): IColor {
+ const a = (fg.rgba & 0xFF) / 255;
+ if (a === 1) {
+ return {
+ css: fg.css,
+ rgba: fg.rgba
+ };
+ }
+ const fgR = (fg.rgba >> 24) & 0xFF;
+ const fgG = (fg.rgba >> 16) & 0xFF;
+ const fgB = (fg.rgba >> 8) & 0xFF;
+ const bgR = (bg.rgba >> 24) & 0xFF;
+ const bgG = (bg.rgba >> 16) & 0xFF;
+ const bgB = (bg.rgba >> 8) & 0xFF;
+ const r = bgR + Math.round((fgR - bgR) * a);
+ const g = bgG + Math.round((fgG - bgG) * a);
+ const b = bgB + Math.round((fgB - bgB) * a);
+ const css = toCss(r, g, b);
+ const rgba = toRgba(r, g, b);
+ return { css, rgba };
+}
+
+export function fromCss(css: string): IColor {
+ return {
+ css,
+ rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
+ };
+}
+
+export function toPaddedHex(c: number): string {
+ const s = c.toString(16);
+ return s.length < 2 ? '0' + s : s;
+}
+
+export function toCss(r: number, g: number, b: number): string {
+ return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`;
+}
+
+export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number {
+ // >>> 0 forces an unsigned int
+ return (r << 24 | g << 16 | b << 8 | a) >>> 0;
+}
+
+/**
+ * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio
+ * between two colors.
+ * @param rgb The color to use.
+ * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
+ */
+export function rgbRelativeLuminance(rgb: number): number {
+ return rgbRelativeLuminance2(
+ (rgb >> 16) & 0xFF,
+ (rgb >> 8 ) & 0xFF,
+ (rgb ) & 0xFF);
+}
+
+/**
+ * Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio
+ * between two colors.
+ * @param r The red channel (0x00 to 0xFF).
+ * @param g The green channel (0x00 to 0xFF).
+ * @param b The blue channel (0x00 to 0xFF).
+ * @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
+ */
+export function rgbRelativeLuminance2(r: number, g: number, b: number): number {
+ const rs = r / 255;
+ const gs = g / 255;
+ const bs = b / 255;
+ const rr = rs <= 0.03928 ? rs / 12.92 : Math.pow((rs + 0.055) / 1.055, 2.4);
+ const rg = gs <= 0.03928 ? gs / 12.92 : Math.pow((gs + 0.055) / 1.055, 2.4);
+ const rb = bs <= 0.03928 ? bs / 12.92 : Math.pow((bs + 0.055) / 1.055, 2.4);
+ return rr * 0.2126 + rg * 0.7152 + rb * 0.0722;
+}
+
+/**
+ * Gets the contrast ratio between two relative luminance values.
+ * @param l1 The first relative luminance.
+ * @param l2 The first relative luminance.
+ * @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef
+ */
+export function contrastRatio(l1: number, l2: number): number {
+ if (l1 < l2) {
+ return (l2 + 0.05) / (l1 + 0.05);
+ }
+ return (l1 + 0.05) / (l2 + 0.05);
+}
+
+function rgbaToColor(r: number, g: number, b: number): IColor {
+ return {
+ css: toCss(r, g, b),
+ rgba: toRgba(r, g, b)
+ };
+}
+
+export function ensureContrastRatioRgba(bgRgba: number, fgRgba: number, ratio: number): number | undefined {
+ const bgL = rgbRelativeLuminance(bgRgba >> 8);
+ const fgL = rgbRelativeLuminance(fgRgba >> 8);
+ const cr = contrastRatio(bgL, fgL);
+ if (cr < ratio) {
+ if (fgL < bgL) {
+ return reduceLuminance(bgRgba, fgRgba, ratio);
+ }
+ return increaseLuminance(bgRgba, fgRgba, ratio);
+ }
+ return undefined;
+}
+
+export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined {
+ const result = ensureContrastRatioRgba(bg.rgba, fg.rgba, ratio);
+ if (!result) {
+ return undefined;
+ }
+ return rgbaToColor(
+ (result >> 24 & 0xFF),
+ (result >> 16 & 0xFF),
+ (result >> 8 & 0xFF)
+ );
+}
+
+export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): number {
+ // This is a naive but fast approach to reducing luminance as converting to
+ // HSL and back is expensive
+ const bgR = (bgRgba >> 24) & 0xFF;
+ const bgG = (bgRgba >> 16) & 0xFF;
+ const bgB = (bgRgba >> 8) & 0xFF;
+ let fgR = (fgRgba >> 24) & 0xFF;
+ let fgG = (fgRgba >> 16) & 0xFF;
+ let fgB = (fgRgba >> 8) & 0xFF;
+ let cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
+ while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) {
+ // Reduce by 10% until the ratio is hit
+ fgR -= Math.max(0, Math.ceil(fgR * 0.1));
+ fgG -= Math.max(0, Math.ceil(fgG * 0.1));
+ fgB -= Math.max(0, Math.ceil(fgB * 0.1));
+ cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
+ }
+ return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
+}
+
+export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number {
+ // This is a naive but fast approach to increasing luminance as converting to
+ // HSL and back is expensive
+ const bgR = (bgRgba >> 24) & 0xFF;
+ const bgG = (bgRgba >> 16) & 0xFF;
+ const bgB = (bgRgba >> 8) & 0xFF;
+ let fgR = (fgRgba >> 24) & 0xFF;
+ let fgG = (fgRgba >> 16) & 0xFF;
+ let fgB = (fgRgba >> 8) & 0xFF;
+ let cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
+ while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) {
+ // Increase by 10% until the ratio is hit
+ fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1));
+ fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1));
+ fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1));
+ cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
+ }
+ return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
+}
diff --git a/src/browser/ColorContrastCache.test.ts b/src/browser/ColorContrastCache.test.ts
new file mode 100644
index 00000000..17e5df38
--- /dev/null
+++ b/src/browser/ColorContrastCache.test.ts
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { assert } from 'chai';
+import { ColorContrastCache } from 'browser/ColorContrastCache';
+
+describe('ColorContrastCache', () => {
+ let cache: ColorContrastCache;
+
+ beforeEach(() => {
+ cache = new ColorContrastCache();
+ });
+
+ it('should save and get color values', () => {
+ assert.strictEqual(cache.getColor(0x01, 0x00), undefined);
+ cache.setColor(0x01, 0x01, null);
+ assert.strictEqual(cache.getColor(0x01, 0x01), null);
+ cache.setColor(0x01, 0x02, { css: '#030303', rgba: 0x030303ff});
+ assert.deepEqual(cache.getColor(0x01, 0x02), { css: '#030303', rgba: 0x030303ff});
+ });
+
+ it('should save and get css values', () => {
+ assert.strictEqual(cache.getCss(0x01, 0x00), undefined);
+ cache.setCss(0x01, 0x01, null);
+ assert.strictEqual(cache.getCss(0x01, 0x01), null);
+ cache.setCss(0x01, 0x02, '#030303');
+ assert.deepEqual(cache.getCss(0x01, 0x02), '#030303');
+ });
+
+ it('should clear all values on clear', () => {
+ cache.setColor(0x01, 0x01, null);
+ cache.setColor(0x01, 0x02, { css: '#030303', rgba: 0x030303ff});
+ cache.setCss(0x01, 0x01, null);
+ cache.setCss(0x01, 0x02, '#030303');
+ cache.clear();
+ assert.strictEqual(cache.getColor(0x01, 0x01), undefined);
+ assert.strictEqual(cache.getColor(0x01, 0x02), undefined);
+ assert.strictEqual(cache.getCss(0x01, 0x01), undefined);
+ assert.strictEqual(cache.getCss(0x01, 0x02), undefined);
+ });
+});
diff --git a/src/browser/ColorContrastCache.ts b/src/browser/ColorContrastCache.ts
new file mode 100644
index 00000000..b96b66cc
--- /dev/null
+++ b/src/browser/ColorContrastCache.ts
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2017 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { IColor, IColorContrastCache } from 'browser/Types';
+
+export class ColorContrastCache implements IColorContrastCache {
+ private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {};
+ private _rgba: { [bg: number]: { [fg: number]: string | null | undefined } | undefined } = {};
+
+ public clear(): void {
+ this._color = {};
+ this._rgba = {};
+ }
+
+ public setCss(bg: number, fg: number, value: string | null): void {
+ if (!this._rgba[bg]) {
+ this._rgba[bg] = {};
+ }
+ this._rgba[bg]![fg] = value;
+ }
+
+ public getCss(bg: number, fg: number): string | null | undefined {
+ return this._rgba[bg] ? this._rgba[bg]![fg] : undefined;
+ }
+
+ public setColor(bg: number, fg: number, value: IColor | null): void {
+ if (!this._color[bg]) {
+ this._color[bg] = {};
+ }
+ this._color[bg]![fg] = value;
+ }
+
+ public getColor(bg: number, fg: number): IColor | null | undefined {
+ return this._color[bg] ? this._color[bg]![fg] : undefined;
+ }
+}
diff --git a/src/browser/ColorManager.test.ts b/src/browser/ColorManager.test.ts
index a213c616..39a8f8c2 100644
--- a/src/browser/ColorManager.test.ts
+++ b/src/browser/ColorManager.test.ts
@@ -34,7 +34,7 @@ describe('ColorManager', () => {
describe('constructor', () => {
it('should fill all colors with values', () => {
for (const key of Object.keys(cm.colors)) {
- if (key !== 'ansi') {
+ if (key !== 'ansi' && key !== 'contrastCache') {
// A #rrggbb or rgba(...)
assert.ok((cm.colors)[key].css.length >= 7);
}
diff --git a/src/browser/ColorManager.ts b/src/browser/ColorManager.ts
index 70d21a7a..92994cfc 100644
--- a/src/browser/ColorManager.ts
+++ b/src/browser/ColorManager.ts
@@ -3,16 +3,18 @@
* @license MIT
*/
-import { IColorManager, IColor, IColorSet } from 'browser/Types';
+import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types';
import { ITheme } from 'common/services/Services';
+import { fromCss, toCss, blend, toRgba } from 'browser/Color';
+import { ColorContrastCache } from 'browser/ColorContrastCache';
-const DEFAULT_FOREGROUND = fromHex('#ffffff');
-const DEFAULT_BACKGROUND = fromHex('#000000');
-const DEFAULT_CURSOR = fromHex('#ffffff');
-const DEFAULT_CURSOR_ACCENT = fromHex('#000000');
+const DEFAULT_FOREGROUND = fromCss('#ffffff');
+const DEFAULT_BACKGROUND = fromCss('#000000');
+const DEFAULT_CURSOR = fromCss('#ffffff');
+const DEFAULT_CURSOR_ACCENT = fromCss('#000000');
const DEFAULT_SELECTION = {
css: 'rgba(255, 255, 255, 0.3)',
- rgba: 0xFFFFFF77
+ rgba: 0xFFFFFF4D
};
// An IIFE to generate DEFAULT_ANSI_COLORS. Do not mutate DEFAULT_ANSI_COLORS, instead make a copy
@@ -20,23 +22,23 @@ const DEFAULT_SELECTION = {
export const DEFAULT_ANSI_COLORS = (() => {
const colors = [
// dark:
- fromHex('#2e3436'),
- fromHex('#cc0000'),
- fromHex('#4e9a06'),
- fromHex('#c4a000'),
- fromHex('#3465a4'),
- fromHex('#75507b'),
- fromHex('#06989a'),
- fromHex('#d3d7cf'),
+ fromCss('#2e3436'),
+ fromCss('#cc0000'),
+ fromCss('#4e9a06'),
+ fromCss('#c4a000'),
+ fromCss('#3465a4'),
+ fromCss('#75507b'),
+ fromCss('#06989a'),
+ fromCss('#d3d7cf'),
// bright:
- fromHex('#555753'),
- fromHex('#ef2929'),
- fromHex('#8ae234'),
- fromHex('#fce94f'),
- fromHex('#729fcf'),
- fromHex('#ad7fa8'),
- fromHex('#34e2e2'),
- fromHex('#eeeeec')
+ fromCss('#555753'),
+ fromCss('#ef2929'),
+ fromCss('#8ae234'),
+ fromCss('#fce94f'),
+ fromCss('#729fcf'),
+ fromCss('#ad7fa8'),
+ fromCss('#34e2e2'),
+ fromCss('#eeeeec')
];
// Fill in the remaining 240 ANSI colors.
@@ -47,37 +49,23 @@ export const DEFAULT_ANSI_COLORS = (() => {
const g = v[(i / 6) % 6 | 0];
const b = v[i % 6];
colors.push({
- css: `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`,
- // Use >>> 0 to force a conversion to an unsigned int
- rgba: ((r << 24) | (g << 16) | (b << 8) | 0xFF) >>> 0
+ css: toCss(r, g, b),
+ rgba: toRgba(r, g, b)
});
}
// Generate greys (232-255)
for (let i = 0; i < 24; i++) {
const c = 8 + i * 10;
- const ch = toPaddedHex(c);
colors.push({
- css: `#${ch}${ch}${ch}`,
- rgba: ((c << 24) | (c << 16) | (c << 8) | 0xFF) >>> 0
+ css: toCss(c, c, c),
+ rgba: toRgba(c, c, c)
});
}
return colors;
})();
-function fromHex(css: string): IColor {
- return {
- css,
- rgba: parseInt(css.slice(1), 16) << 8 | 0xFF
- };
-}
-
-function toPaddedHex(c: number): string {
- const s = c.toString(16);
- return s.length < 2 ? '0' + s : s;
-}
-
/**
* Manages the source of truth for a terminal's colors.
*/
@@ -85,6 +73,7 @@ export class ColorManager implements IColorManager {
public colors: IColorSet;
private _ctx: CanvasRenderingContext2D;
private _litmusColor: CanvasGradient;
+ private _contrastCache: IColorContrastCache;
constructor(document: Document, public allowTransparency: boolean) {
const canvas = document.createElement('canvas');
@@ -97,16 +86,25 @@ export class ColorManager implements IColorManager {
this._ctx = ctx;
this._ctx.globalCompositeOperation = 'copy';
this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1);
+ this._contrastCache = new ColorContrastCache();
this.colors = {
foreground: DEFAULT_FOREGROUND,
background: DEFAULT_BACKGROUND,
cursor: DEFAULT_CURSOR,
cursorAccent: DEFAULT_CURSOR_ACCENT,
selection: DEFAULT_SELECTION,
- ansi: DEFAULT_ANSI_COLORS.slice()
+ selectionOpaque: blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
+ ansi: DEFAULT_ANSI_COLORS.slice(),
+ contrastCache: this._contrastCache
};
}
+ public onOptionsChange(key: string): void {
+ if (key === 'minimumContrastRatio') {
+ this._contrastCache.clear();
+ }
+ }
+
/**
* Sets the terminal's theme.
* @param theme The theme to use. If a partial theme is provided then default
@@ -118,6 +116,7 @@ export class ColorManager implements IColorManager {
this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true);
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
this.colors.selection = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
+ this.colors.selectionOpaque = blend(this.colors.background, this.colors.selection);
this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
@@ -134,6 +133,8 @@ export class ColorManager implements IColorManager {
this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]);
this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]);
this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
+ // Clear our the cache
+ this._contrastCache.clear();
}
private _parseColor(
@@ -184,7 +185,7 @@ export class ColorManager implements IColorManager {
return {
css,
- rgba: (data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]) >>> 0
+ rgba: toRgba(data[0], data[1], data[2], data[3])
};
}
}
diff --git a/src/browser/Linkifier.ts b/src/browser/Linkifier.ts
index a5454ac5..58aca70e 100644
--- a/src/browser/Linkifier.ts
+++ b/src/browser/Linkifier.ts
@@ -211,7 +211,7 @@ export class Linkifier implements ILinkifier {
*/
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher): void {
// clone regex to do a global search on text
- const rex = new RegExp(matcher.regex.source, matcher.regex.flags + 'g');
+ const rex = new RegExp(matcher.regex.source, (matcher.regex.flags || '') + 'g');
let match;
let stringIndex = -1;
while ((match = rex.exec(text)) !== null) {
@@ -306,7 +306,9 @@ export class Linkifier implements ILinkifier {
e => {
this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
if (matcher.hoverTooltipCallback) {
- matcher.hoverTooltipCallback(e, uri);
+ // Note that IViewportRange use 1-based coordinates to align with escape sequences such
+ // as CUP which use 1,1 as the default for row/col
+ matcher.hoverTooltipCallback(e, uri, { start: { x: x1, y: y1 }, end: { x: x2, y: y2 } });
}
},
() => {
diff --git a/src/browser/MouseZoneManager.ts b/src/browser/MouseZoneManager.ts
index 7eb7c5f8..d697c133 100644
--- a/src/browser/MouseZoneManager.ts
+++ b/src/browser/MouseZoneManager.ts
@@ -175,11 +175,9 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
// Find the active zone, prevent event propagation if found to prevent other
// components from handling the mouse event.
const zone = this._findZoneEventAt(e);
- if (zone) {
- if (zone.willLinkActivate(e)) {
- e.preventDefault();
- e.stopImmediatePropagation();
- }
+ if (zone?.willLinkActivate(e)) {
+ e.preventDefault();
+ e.stopImmediatePropagation();
}
}
diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts
index 3f3a7637..31d00af8 100644
--- a/src/browser/Types.d.ts
+++ b/src/browser/Types.d.ts
@@ -8,6 +8,7 @@ import { IDisposable } from 'common/Types';
export interface IColorManager {
colors: IColorSet;
+ onOptionsChange(key: string): void;
}
export interface IColor {
@@ -21,7 +22,18 @@ export interface IColorSet {
cursor: IColor;
cursorAccent: IColor;
selection: IColor;
+ /** The selection blended on top of background. */
+ selectionOpaque: IColor;
ansi: IColor[];
+ contrastCache: IColorContrastCache;
+}
+
+export interface IColorContrastCache {
+ clear(): void;
+ setCss(bg: number, fg: number, value: string | null): void;
+ getCss(bg: number, fg: number): string | null | undefined;
+ setColor(bg: number, fg: number, value: IColor | null): void;
+ getColor(bg: number, fg: number): IColor | null | undefined;
}
export interface IPartialColorSet {
@@ -35,22 +47,33 @@ export interface IPartialColorSet {
export interface IViewport extends IDisposable {
scrollBarWidth: number;
- syncScrollArea(): void;
+ syncScrollArea(immediate?: boolean): void;
getLinesScrolled(ev: WheelEvent): number;
- onWheel(ev: WheelEvent): void;
+ onWheel(ev: WheelEvent): boolean;
onTouchStart(ev: TouchEvent): void;
- onTouchMove(ev: TouchEvent): void;
+ onTouchMove(ev: TouchEvent): boolean;
onThemeChange(colors: IColorSet): void;
}
+export interface IViewportRange {
+ start: IViewportRangePosition;
+ end: IViewportRangePosition;
+}
+
+export interface IViewportRangePosition {
+ x: number;
+ y: number;
+}
+
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
+export type LinkMatcherHoverTooltipCallback = (event: MouseEvent, uri: string, position: IViewportRange) => void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export interface ILinkMatcher {
id: number;
regex: RegExp;
handler: LinkMatcherHandler;
- hoverTooltipCallback?: LinkMatcherHandler;
+ hoverTooltipCallback?: LinkMatcherHoverTooltipCallback;
hoverLeaveCallback?: () => void;
matchIndex?: number;
validationCallback?: LinkMatcherValidationCallback;
@@ -96,7 +119,7 @@ export interface ILinkMatcherOptions {
/**
* A callback that fires when the mouse hovers over a link.
*/
- tooltipCallback?: LinkMatcherHandler;
+ tooltipCallback?: LinkMatcherHoverTooltipCallback;
/**
* A callback that fires when the mouse leaves a link that was hovered.
*/
diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts
index 9625588d..b88f381d 100644
--- a/src/browser/Viewport.ts
+++ b/src/browser/Viewport.ts
@@ -7,7 +7,7 @@ import { Disposable } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { IColorSet, IViewport } from 'browser/Types';
import { ICharSizeService, IRenderService } from 'browser/services/Services';
-import { IBufferService } from 'common/services/Services';
+import { IBufferService, IOptionsService } from 'common/services/Services';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
@@ -37,6 +37,7 @@ export class Viewport extends Disposable implements IViewport {
private readonly _viewportElement: HTMLElement,
private readonly _scrollArea: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
+ @IOptionsService private readonly _optionsService: IOptionsService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IRenderService private readonly _renderService: IRenderService
) {
@@ -60,7 +61,14 @@ export class Viewport extends Disposable implements IViewport {
* Refreshes row height, setting line-height, viewport height and scroll area height if
* necessary.
*/
- private _refresh(): void {
+ private _refresh(immediate: boolean): void {
+ if (immediate) {
+ this._innerRefresh();
+ if (this._refreshAnimationFrame !== null) {
+ cancelAnimationFrame(this._refreshAnimationFrame);
+ }
+ return;
+ }
if (this._refreshAnimationFrame === null) {
this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh());
}
@@ -88,40 +96,39 @@ export class Viewport extends Disposable implements IViewport {
this._refreshAnimationFrame = null;
}
-
/**
* Updates dimensions and synchronizes the scroll area if necessary.
*/
- public syncScrollArea(): void {
+ public syncScrollArea(immediate: boolean = false): void {
// If buffer height changed
if (this._lastRecordedBufferLength !== this._bufferService.buffer.lines.length) {
this._lastRecordedBufferLength = this._bufferService.buffer.lines.length;
- this._refresh();
+ this._refresh(immediate);
return;
}
// If viewport height changed
if (this._lastRecordedViewportHeight !== this._renderService.dimensions.canvasHeight) {
- this._refresh();
+ this._refresh(immediate);
return;
}
// If the buffer position doesn't match last scroll top
const newScrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight;
if (this._lastScrollTop !== newScrollTop) {
- this._refresh();
+ this._refresh(immediate);
return;
}
// If element's scroll top changed, this can happen when hiding the element
if (this._lastScrollTop !== this._viewportElement.scrollTop) {
- this._refresh();
+ this._refresh(immediate);
return;
}
// If row height changed
if (this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
- this._refresh();
+ this._refresh(immediate);
return;
}
}
@@ -152,20 +159,36 @@ export class Viewport extends Disposable implements IViewport {
this._scrollLines(diff, true);
}
+ /**
+ * Handles bubbling of scroll event in case the viewport has reached top or bottom
+ * @param ev The scroll event.
+ * @param amount The amount scrolled
+ */
+ private _bubbleScroll(ev: Event, amount: number): boolean {
+ const scrollPosFromTop = this._viewportElement.scrollTop + this._lastRecordedViewportHeight;
+ if ((amount < 0 && this._viewportElement.scrollTop !== 0) ||
+ (amount > 0 && scrollPosFromTop < this._lastRecordedBufferHeight)) {
+ if (ev.cancelable) {
+ ev.preventDefault();
+ }
+ return false;
+ }
+ return true;
+ }
+
/**
* Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual
* scrolling to `onScroll`, this event needs to be attached manually by the consumer of
* `Viewport`.
* @param ev The mouse wheel event.
*/
- public onWheel(ev: WheelEvent): void {
+ public onWheel(ev: WheelEvent): boolean {
const amount = this._getPixelsScrolled(ev);
if (amount === 0) {
- return;
+ return false;
}
this._viewportElement.scrollTop += amount;
- // Prevent the page from scrolling when the terminal scrolls
- ev.preventDefault();
+ return this._bubbleScroll(ev, amount);
}
private _getPixelsScrolled(ev: WheelEvent): number {
@@ -175,7 +198,7 @@ export class Viewport extends Disposable implements IViewport {
}
// Fallback to WheelEvent.DOM_DELTA_PIXEL
- let amount = ev.deltaY;
+ let amount = this._applyScrollModifier(ev.deltaY, ev);
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
amount *= this._currentRowHeight;
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
@@ -196,7 +219,7 @@ export class Viewport extends Disposable implements IViewport {
}
// Fallback to WheelEvent.DOM_DELTA_LINE
- let amount = ev.deltaY;
+ let amount = this._applyScrollModifier(ev.deltaY, ev);
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
amount /= this._currentRowHeight + 0.0; // Prevent integer division
this._wheelPartialScroll += amount;
@@ -208,6 +231,18 @@ export class Viewport extends Disposable implements IViewport {
return amount;
}
+ private _applyScrollModifier(amount: number, ev: WheelEvent): number {
+ const modifier = this._optionsService.options.fastScrollModifier;
+ // Multiply the scroll speed when the modifier is down
+ if ((modifier === 'alt' && ev.altKey) ||
+ (modifier === 'ctrl' && ev.ctrlKey) ||
+ (modifier === 'shift' && ev.shiftKey)) {
+ return amount * this._optionsService.options.fastScrollSensitivity * this._optionsService.options.scrollSensitivity;
+ }
+
+ return amount * this._optionsService.options.scrollSensitivity;
+ }
+
/**
* Handles the touchstart event, recording the touch occurred.
* @param ev The touch event.
@@ -220,13 +255,13 @@ export class Viewport extends Disposable implements IViewport {
* Handles the touchmove event, scrolling the viewport if the position shifted.
* @param ev The touch event.
*/
- public onTouchMove(ev: TouchEvent): void {
+ public onTouchMove(ev: TouchEvent): boolean {
const deltaY = this._lastTouchY - ev.touches[0].pageY;
this._lastTouchY = ev.touches[0].pageY;
if (deltaY === 0) {
- return;
+ return false;
}
this._viewportElement.scrollTop += deltaY;
- ev.preventDefault();
+ return this._bubbleScroll(ev, deltaY);
}
}
diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts
index e9ad5b3a..599ab114 100644
--- a/src/browser/renderer/BaseRenderLayer.ts
+++ b/src/browser/renderer/BaseRenderLayer.ts
@@ -5,16 +5,17 @@
import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types';
import { ICellData } from 'common/Types';
-import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
+import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
import { IGlyphIdentifier } from 'browser/renderer/atlas/Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache';
import { AttributeData } from 'common/buffer/AttributeData';
-import { IColorSet } from 'browser/Types';
+import { IColorSet, IColor } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
+import { toCss, ensureContrastRatioRgba } from 'browser/Color';
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
@@ -60,9 +61,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
public dispose(): void {
this._container.removeChild(this._canvas);
- if (this._charAtlas) {
- this._charAtlas.dispose();
- }
+ this._charAtlas?.dispose();
}
private _initCanvas(): void {
@@ -264,13 +263,14 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param bold Whether the text is bold.
*/
protected _drawChars(cell: ICellData, x: number, y: number): void {
+ const contrastColor = this._getContrastColor(cell);
// skip cache right away if we draw in RGB
// Note: to avoid bad runtime JoinedCellData will be skipped
// in the cache handler itself (atlasDidDraw == false) and
// fall through to uncached later down below
- if (cell.isFgRGB() || cell.isBgRGB()) {
- this._drawUncachedChars(cell, x, y);
+ if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) {
+ this._drawUncachedChars(cell, x, y, contrastColor);
return;
}
@@ -284,7 +284,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor();
}
- const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
+ const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8;
fg += drawInBrightColor ? 8 : 0;
this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR;
@@ -316,21 +316,29 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param x The column to draw at.
* @param y The row to draw at.
*/
- private _drawUncachedChars(cell: ICellData, x: number, y: number): void {
+ private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void {
this._ctx.save();
this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
this._ctx.textBaseline = 'middle';
if (cell.isInverse()) {
- if (cell.isBgDefault()) {
+ if (fgOverride) {
+ this._ctx.fillStyle = fgOverride.css;
+ } else if (cell.isBgDefault()) {
this._ctx.fillStyle = this._colors.background.css;
} else if (cell.isBgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else {
- this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css;
+ let bg = cell.getBgColor();
+ if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
+ bg += 8;
+ }
+ this._ctx.fillStyle = this._colors.ansi[bg].css;
}
} else {
- if (cell.isFgDefault()) {
+ if (fgOverride) {
+ this._ctx.fillStyle = fgOverride.css;
+ } else if (cell.isFgDefault()) {
this._ctx.fillStyle = this._colors.foreground.css;
} else if (cell.isFgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
@@ -381,5 +389,88 @@ export abstract class BaseRenderLayer implements IRenderLayer {
return `${fontStyle} ${fontWeight} ${this._optionsService.options.fontSize * window.devicePixelRatio}px ${this._optionsService.options.fontFamily}`;
}
+
+ private _getContrastColor(cell: CellData): IColor | undefined {
+ if (this._optionsService.options.minimumContrastRatio === 1) {
+ return undefined;
+ }
+
+ // Try get from cache first
+ const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg);
+ if (adjustedColor !== undefined) {
+ return adjustedColor || undefined;
+ }
+
+ let fgColor = cell.getFgColor();
+ let fgColorMode = cell.getFgColorMode();
+ let bgColor = cell.getBgColor();
+ let bgColorMode = cell.getBgColorMode();
+ const isInverse = !!cell.isInverse();
+ const isBold = !!cell.isInverse();
+ if (isInverse) {
+ const temp = fgColor;
+ fgColor = bgColor;
+ bgColor = temp;
+ const temp2 = fgColorMode;
+ fgColorMode = bgColorMode;
+ bgColorMode = temp2;
+ }
+
+ const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, isInverse);
+ const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold);
+ const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._optionsService.options.minimumContrastRatio);
+
+ if (!result) {
+ this._colors.contrastCache.setColor(cell.bg, cell.fg, null);
+ return undefined;
+ }
+
+ const color: IColor = {
+ css: toCss(
+ (result >> 24) & 0xFF,
+ (result >> 16) & 0xFF,
+ (result >> 8) & 0xFF
+ ),
+ rgba: result
+ };
+ this._colors.contrastCache.setColor(cell.bg, cell.fg, color);
+
+ return color;
+ }
+
+ private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number {
+ switch (bgColorMode) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ return this._colors.ansi[bgColor].rgba;
+ case Attributes.CM_RGB:
+ return bgColor << 8;
+ case Attributes.CM_DEFAULT:
+ default:
+ if (inverse) {
+ return this._colors.foreground.rgba;
+ }
+ return this._colors.background.rgba;
+ }
+ }
+
+ private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number {
+ switch (fgColorMode) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ if (this._optionsService.options.drawBoldTextInBrightColors && bold && fgColor < 8) {
+ fgColor += 8;
+ }
+ return this._colors.ansi[fgColor].rgba;
+ case Attributes.CM_RGB:
+ return fgColor << 8;
+ case Attributes.CM_DEFAULT:
+ default:
+ if (inverse) {
+ return this._colors.background.rgba;
+ }
+ return this._colors.foreground.rgba;
+ }
+ }
}
diff --git a/src/browser/renderer/CharacterJoinerRegistry.ts b/src/browser/renderer/CharacterJoinerRegistry.ts
index a5abe80f..5385b76b 100644
--- a/src/browser/renderer/CharacterJoinerRegistry.ts
+++ b/src/browser/renderer/CharacterJoinerRegistry.ts
@@ -303,7 +303,6 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
// current range
ranges[i - 1][1] = Math.max(newRange[1], range[1]);
ranges.splice(i, 1);
- inRange = false;
return ranges;
}
diff --git a/src/renderer/CursorRenderLayer.ts b/src/browser/renderer/CursorRenderLayer.ts
similarity index 75%
rename from src/renderer/CursorRenderLayer.ts
rename to src/browser/renderer/CursorRenderLayer.ts
index f847c5f6..0ca1b97a 100644
--- a/src/renderer/CursorRenderLayer.ts
+++ b/src/browser/renderer/CursorRenderLayer.ts
@@ -3,13 +3,14 @@
* @license MIT
*/
-import { IRenderDimensions } from 'browser/renderer/Types';
-import { BaseRenderLayer } from '../browser/renderer/BaseRenderLayer';
-import { ITerminal } from '../Types';
+import { IRenderDimensions, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
+import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { IColorSet } from 'browser/Types';
-import { IBufferService, IOptionsService } from 'common/services/Services';
+import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
+import { IEventEmitter } from 'common/EventEmitter';
+import { ICoreBrowserService } from 'browser/services/Services';
interface ICursorState {
x: number;
@@ -27,25 +28,27 @@ const BLINK_INTERVAL = 600;
export class CursorRenderLayer extends BaseRenderLayer {
private _state: ICursorState;
private _cursorRenderers: {[key: string]: (x: number, y: number, cell: ICellData) => void};
- private _cursorBlinkStateManager: CursorBlinkStateManager;
+ private _cursorBlinkStateManager: CursorBlinkStateManager | undefined;
private _cell: ICellData = new CellData();
constructor(
container: HTMLElement,
zIndex: number,
colors: IColorSet,
- private _terminal: ITerminal,
rendererId: number,
+ private _onRequestRefreshRowsEvent: IEventEmitter,
readonly bufferService: IBufferService,
- readonly optionsService: IOptionsService
+ readonly optionsService: IOptionsService,
+ private readonly _coreService: ICoreService,
+ private readonly _coreBrowserService: ICoreBrowserService
) {
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService);
this._state = {
- x: null,
- y: null,
- isFocused: null,
- style: null,
- width: null
+ x: 0,
+ y: 0,
+ isFocused: false,
+ style: '',
+ width: 0
};
this._cursorRenderers = {
'bar': this._renderBarCursor.bind(this),
@@ -59,11 +62,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
super.resize(dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
- x: null,
- y: null,
- isFocused: null,
- style: null,
- width: null
+ x: 0,
+ y: 0,
+ isFocused: false,
+ style: '',
+ width: 0
};
}
@@ -71,7 +74,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._clearCursor();
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.dispose();
- this._cursorBlinkStateManager = null;
+ this._cursorBlinkStateManager = undefined;
this.onOptionsChanged();
}
}
@@ -80,33 +83,31 @@ export class CursorRenderLayer extends BaseRenderLayer {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.pause();
}
- this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y);
+ this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onFocus(): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.resume();
} else {
- this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y);
+ this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
}
public onOptionsChanged(): void {
if (this._optionsService.options.cursorBlink) {
if (!this._cursorBlinkStateManager) {
- this._cursorBlinkStateManager = new CursorBlinkStateManager(this._terminal.isFocused, () => {
+ this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => {
this._render(true);
});
}
} else {
- if (this._cursorBlinkStateManager) {
- this._cursorBlinkStateManager.dispose();
- this._cursorBlinkStateManager = null;
- }
- // Request a refresh from the terminal as management of rendering is being
- // moved back to the terminal
- this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y);
+ this._cursorBlinkStateManager?.dispose();
+ this._cursorBlinkStateManager = undefined;
}
+ // Request a refresh from the terminal as management of rendering is being
+ // moved back to the terminal
+ this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onCursorMove(): void {
@@ -125,7 +126,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _render(triggeredByAnimationFrame: boolean): void {
// Don't draw the cursor if it's hidden
- if (!this._terminal.cursorState || this._terminal.cursorHidden) {
+ if (!this._coreService.isCursorInitialized || this._coreService.isCursorHidden) {
this._clearCursor();
return;
}
@@ -139,21 +140,26 @@ export class CursorRenderLayer extends BaseRenderLayer {
return;
}
- this._bufferService.buffer.lines.get(cursorY).loadCell(this._bufferService.buffer.x, this._cell);
+ this._bufferService.buffer.lines.get(cursorY)!.loadCell(this._bufferService.buffer.x, this._cell);
if (this._cell.content === undefined) {
return;
}
- if (!this._terminal.isFocused) {
+ if (!this._coreBrowserService.isFocused) {
this._clearCursor();
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
- this._renderBlurCursor(this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
+ const cursorStyle = this._optionsService.options.cursorStyle;
+ if (cursorStyle && cursorStyle !== 'block') {
+ this._cursorRenderers[cursorStyle](this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
+ } else {
+ this._renderBlurCursor(this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
+ }
this._ctx.restore();
this._state.x = this._bufferService.buffer.x;
this._state.y = viewportRelativeCursorY;
this._state.isFocused = false;
- this._state.style = this._optionsService.options.cursorStyle;
+ this._state.style = cursorStyle;
this._state.width = this._cell.getWidth();
return;
}
@@ -168,7 +174,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
// The cursor is already in the correct spot, don't redraw
if (this._state.x === this._bufferService.buffer.x &&
this._state.y === viewportRelativeCursorY &&
- this._state.isFocused === this._terminal.isFocused &&
+ this._state.isFocused === this._coreBrowserService.isFocused &&
this._state.style === this._optionsService.options.cursorStyle &&
this._state.width === this._cell.getWidth()) {
return;
@@ -191,11 +197,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
if (this._state) {
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
this._state = {
- x: null,
- y: null,
- isFocused: null,
- style: null,
- width: null
+ x: 0,
+ y: 0,
+ isFocused: false,
+ style: '',
+ width: 0
};
}
}
@@ -234,16 +240,16 @@ export class CursorRenderLayer extends BaseRenderLayer {
class CursorBlinkStateManager {
public isCursorVisible: boolean;
- private _animationFrame: number;
- private _blinkStartTimeout: number;
- private _blinkInterval: number;
+ private _animationFrame: number | undefined;
+ private _blinkStartTimeout: number | undefined;
+ private _blinkInterval: number | undefined;
/**
* The time at which the animation frame was restarted, this is used on the
* next render to restart the timers so they don't need to restart the timers
* multiple times over a short period.
*/
- private _animationTimeRestarted: number;
+ private _animationTimeRestarted: number | undefined;
constructor(
isFocused: boolean,
@@ -260,15 +266,15 @@ class CursorBlinkStateManager {
public dispose(): void {
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
- this._blinkInterval = null;
+ this._blinkInterval = undefined;
}
if (this._blinkStartTimeout) {
window.clearTimeout(this._blinkStartTimeout);
- this._blinkStartTimeout = null;
+ this._blinkStartTimeout = undefined;
}
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
- this._animationFrame = null;
+ this._animationFrame = undefined;
}
}
@@ -283,7 +289,7 @@ class CursorBlinkStateManager {
if (!this._animationFrame) {
this._animationFrame = window.requestAnimationFrame(() => {
this._renderCallback();
- this._animationFrame = null;
+ this._animationFrame = undefined;
});
}
}
@@ -303,7 +309,7 @@ class CursorBlinkStateManager {
// started
if (this._animationTimeRestarted) {
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
- this._animationTimeRestarted = null;
+ this._animationTimeRestarted = undefined;
if (time > 0) {
this._restartInterval(time);
return;
@@ -314,7 +320,7 @@ class CursorBlinkStateManager {
this.isCursorVisible = false;
this._animationFrame = window.requestAnimationFrame(() => {
this._renderCallback();
- this._animationFrame = null;
+ this._animationFrame = undefined;
});
// Setup the blink interval
@@ -324,7 +330,7 @@ class CursorBlinkStateManager {
// calc time diff
// Make restart interval do a setTimeout initially?
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
- this._animationTimeRestarted = null;
+ this._animationTimeRestarted = undefined;
this._restartInterval(time);
return;
}
@@ -333,7 +339,7 @@ class CursorBlinkStateManager {
this.isCursorVisible = !this.isCursorVisible;
this._animationFrame = window.requestAnimationFrame(() => {
this._renderCallback();
- this._animationFrame = null;
+ this._animationFrame = undefined;
});
}, BLINK_INTERVAL);
}, timeToStart);
@@ -343,20 +349,20 @@ class CursorBlinkStateManager {
this.isCursorVisible = true;
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
- this._blinkInterval = null;
+ this._blinkInterval = undefined;
}
if (this._blinkStartTimeout) {
window.clearTimeout(this._blinkStartTimeout);
- this._blinkStartTimeout = null;
+ this._blinkStartTimeout = undefined;
}
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
- this._animationFrame = null;
+ this._animationFrame = undefined;
}
}
public resume(): void {
- this._animationTimeRestarted = null;
+ this._animationTimeRestarted = undefined;
this._restartInterval();
this.restartBlinkAnimation();
}
diff --git a/src/renderer/Renderer.ts b/src/browser/renderer/Renderer.ts
similarity index 64%
rename from src/renderer/Renderer.ts
rename to src/browser/renderer/Renderer.ts
index d3977af3..26b464cb 100644
--- a/src/renderer/Renderer.ts
+++ b/src/browser/renderer/Renderer.ts
@@ -3,18 +3,18 @@
* @license MIT
*/
-import { TextRenderLayer } from '../browser/renderer/TextRenderLayer';
-import { SelectionRenderLayer } from '../browser/renderer/SelectionRenderLayer';
-import { CursorRenderLayer } from './CursorRenderLayer';
-import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry } from 'browser/renderer/Types';
-import { ITerminal } from '../Types';
-import { LinkRenderLayer } from '../browser/renderer/LinkRenderLayer';
+import { TextRenderLayer } from 'browser/renderer/TextRenderLayer';
+import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer';
+import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer';
+import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
+import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer';
import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry';
import { Disposable } from 'common/Lifecycle';
-import { IColorSet } from 'browser/Types';
-import { ICharSizeService } from 'browser/services/Services';
-import { IBufferService, IOptionsService } from 'common/services/Services';
+import { IColorSet, ILinkifier } from 'browser/Types';
+import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
+import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache';
+import { EventEmitter, IEvent } from 'common/EventEmitter';
let nextRendererId = 1;
@@ -27,36 +27,42 @@ export class Renderer extends Disposable implements IRenderer {
public dimensions: IRenderDimensions;
+ private _onRequestRefreshRows = new EventEmitter();
+ public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; }
+
constructor(
private _colors: IColorSet,
- private readonly _terminal: ITerminal,
- readonly bufferService: IBufferService,
- private readonly _charSizeService: ICharSizeService,
- readonly optionsService: IOptionsService
+ private readonly _screenElement: HTMLElement,
+ private readonly _linkifier: ILinkifier,
+ @IBufferService private readonly _bufferService: IBufferService,
+ @ICharSizeService private readonly _charSizeService: ICharSizeService,
+ @IOptionsService private readonly _optionsService: IOptionsService,
+ @ICoreService readonly coreService: ICoreService,
+ @ICoreBrowserService readonly coreBrowserService: ICoreBrowserService
) {
super();
- const allowTransparency = this._terminal.options.allowTransparency;
- this._characterJoinerRegistry = new CharacterJoinerRegistry(bufferService);
+ const allowTransparency = this._optionsService.options.allowTransparency;
+ this._characterJoinerRegistry = new CharacterJoinerRegistry(this._bufferService);
this._renderLayers = [
- new TextRenderLayer(this._terminal.screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, bufferService, optionsService),
- new SelectionRenderLayer(this._terminal.screenElement, 1, this._colors, this._id, bufferService, optionsService),
- new LinkRenderLayer(this._terminal.screenElement, 2, this._colors, this._id, this._terminal.linkifier, bufferService, optionsService),
- new CursorRenderLayer(this._terminal.screenElement, 3, this._colors, this._terminal, this._id, bufferService, optionsService)
+ new TextRenderLayer(this._screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, this._bufferService, _optionsService),
+ new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, _optionsService),
+ new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, this._linkifier, this._bufferService, _optionsService),
+ new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRefreshRows, this._bufferService, _optionsService, coreService, coreBrowserService)
];
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
+ scaledCharWidth: 0,
+ scaledCharHeight: 0,
+ scaledCellWidth: 0,
+ scaledCellHeight: 0,
+ scaledCharLeft: 0,
+ scaledCharTop: 0,
+ scaledCanvasWidth: 0,
+ scaledCanvasHeight: 0,
+ canvasWidth: 0,
+ canvasHeight: 0,
+ actualCellWidth: 0,
+ actualCellHeight: 0
};
this._devicePixelRatio = window.devicePixelRatio;
this._updateDimensions();
@@ -74,7 +80,7 @@ export class Renderer extends Disposable implements IRenderer {
// and the terminal needs to refreshed
if (this._devicePixelRatio !== window.devicePixelRatio) {
this._devicePixelRatio = window.devicePixelRatio;
- this.onResize(this._terminal.cols, this._terminal.rows);
+ this.onResize(this._bufferService.cols, this._bufferService.rows);
}
}
@@ -96,12 +102,12 @@ export class Renderer extends Disposable implements IRenderer {
this._renderLayers.forEach(l => l.resize(this.dimensions));
// Resize the screen
- this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
- this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
+ this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
+ this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
}
public onCharSizeChanged(): void {
- this.onResize(this._terminal.cols, this._terminal.rows);
+ this.onResize(this._bufferService.cols, this._bufferService.rows);
}
public onBlur(): void {
@@ -163,23 +169,23 @@ export class Renderer extends Disposable implements IRenderer {
// will be floored because since lineHeight can never be lower then 1, there
// is a guarentee that the scaled line height will always be larger than
// scaled char height.
- this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight);
+ this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight);
// Calculate the y coordinate within a cell that text should draw from in
// order to draw in the center of a cell.
- this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
+ this.dimensions.scaledCharTop = this._optionsService.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
// Calculate the scaled cell width, taking the letterSpacing into account.
- this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing);
+ this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing);
// Calculate the x coordinate with a cell that text should draw from in
// order to draw in the center of a cell.
- this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2);
+ this.dimensions.scaledCharLeft = Math.floor(this._optionsService.options.letterSpacing / 2);
// Recalculate the canvas dimensions; scaled* define the actual number of
// pixel in the canvas
- this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight;
- this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth;
+ this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight;
+ this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth;
// The the size of the canvas on the page. It's very important that this
// rounds to nearest integer and not ceils as browsers often set
@@ -194,8 +200,8 @@ export class Renderer extends Disposable implements IRenderer {
// account window.devicePixelRatio. ICharSizeService.width/height by itself
// is insufficient when the page is not at 100% zoom level as it's measured
// in CSS pixels, but the actual char size on the canvas can differ.
- this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
- this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
+ this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
+ this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
diff --git a/src/browser/renderer/TextRenderLayer.ts b/src/browser/renderer/TextRenderLayer.ts
index 330400ca..fee261f6 100644
--- a/src/browser/renderer/TextRenderLayer.ts
+++ b/src/browser/renderer/TextRenderLayer.ts
@@ -227,7 +227,11 @@ export class TextRenderLayer extends BaseRenderLayer {
} else if (cell.isBgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else {
- this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css;
+ let bg = cell.getBgColor();
+ if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
+ bg += 8;
+ }
+ this._ctx.fillStyle = this._colors.ansi[bg].css;
}
} else {
if (cell.isFgDefault()) {
diff --git a/src/browser/renderer/Types.d.ts b/src/browser/renderer/Types.d.ts
index 7f84dfd4..b4c321a1 100644
--- a/src/browser/renderer/Types.d.ts
+++ b/src/browser/renderer/Types.d.ts
@@ -5,6 +5,7 @@
import { IDisposable } from 'common/Types';
import { IColorSet } from 'browser/Types';
+import { IEvent } from 'common/EventEmitter';
export type CharacterJoinerHandler = (text: string) => [number, number][];
@@ -23,6 +24,11 @@ export interface IRenderDimensions {
actualCellHeight: number;
}
+export interface IRequestRefreshRowsEvent {
+ start: number;
+ end: number;
+}
+
/**
* Note that IRenderer implementations should emit the refresh event after
* rendering rows to the screen.
@@ -30,6 +36,8 @@ export interface IRenderDimensions {
export interface IRenderer extends IDisposable {
readonly dimensions: IRenderDimensions;
+ readonly onRequestRefreshRows: IEvent;
+
dispose(): void;
setColors(colors: IColorSet): void;
onDevicePixelRatioChange(): void;
diff --git a/src/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts
similarity index 70%
rename from src/renderer/dom/DomRenderer.ts
rename to src/browser/renderer/dom/DomRenderer.ts
index 22b50349..23c5a3a1 100644
--- a/src/renderer/dom/DomRenderer.ts
+++ b/src/browser/renderer/dom/DomRenderer.ts
@@ -3,14 +3,14 @@
* @license MIT
*/
-import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types';
-import { ITerminal } from '../../Types';
+import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { Disposable } from 'common/Lifecycle';
-import { IColorSet, ILinkifierEvent } from 'browser/Types';
+import { IColorSet, ILinkifierEvent, ILinkifier } from 'browser/Types';
import { ICharSizeService } from 'browser/services/Services';
-import { IOptionsService } from 'common/services/Services';
+import { IOptionsService, IBufferService } from 'common/services/Services';
+import { EventEmitter, IEvent } from 'common/EventEmitter';
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
const ROW_CONTAINER_CLASS = 'xterm-rows';
@@ -30,19 +30,26 @@ export class DomRenderer extends Disposable implements IRenderer {
private _rowFactory: DomRendererRowFactory;
private _terminalClass: number = nextTerminalId++;
- private _themeStyleElement: HTMLStyleElement;
- private _dimensionsStyleElement: HTMLStyleElement;
+ private _themeStyleElement!: HTMLStyleElement;
+ private _dimensionsStyleElement!: HTMLStyleElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[] = [];
private _selectionContainer: HTMLElement;
public dimensions: IRenderDimensions;
+ private _onRequestRefreshRows = new EventEmitter();
+ public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; }
+
constructor(
- private _terminal: ITerminal,
private _colors: IColorSet,
- private _charSizeService: ICharSizeService,
- private _optionsService: IOptionsService
+ private readonly _element: HTMLElement,
+ private readonly _screenElement: HTMLElement,
+ private readonly _viewportElement: HTMLElement,
+ private readonly _linkifier: ILinkifier,
+ @ICharSizeService private readonly _charSizeService: ICharSizeService,
+ @IOptionsService private readonly _optionsService: IOptionsService,
+ @IBufferService private readonly _bufferService: IBufferService
) {
super();
@@ -50,60 +57,60 @@ export class DomRenderer extends Disposable implements IRenderer {
this._rowContainer.classList.add(ROW_CONTAINER_CLASS);
this._rowContainer.style.lineHeight = 'normal';
this._rowContainer.setAttribute('aria-hidden', 'true');
- this._refreshRowElements(this._terminal.cols, this._terminal.rows);
+ this._refreshRowElements(this._bufferService.cols, this._bufferService.rows);
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
+ scaledCharWidth: 0,
+ scaledCharHeight: 0,
+ scaledCellWidth: 0,
+ scaledCellHeight: 0,
+ scaledCharLeft: 0,
+ scaledCharTop: 0,
+ scaledCanvasWidth: 0,
+ scaledCanvasHeight: 0,
+ canvasWidth: 0,
+ canvasHeight: 0,
+ actualCellWidth: 0,
+ actualCellHeight: 0
};
this._updateDimensions();
this._injectCss();
- this._rowFactory = new DomRendererRowFactory(document, this._optionsService);
+ this._rowFactory = new DomRendererRowFactory(document, this._optionsService, this._colors);
- this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
- this._terminal.screenElement.appendChild(this._rowContainer);
- this._terminal.screenElement.appendChild(this._selectionContainer);
+ this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
+ this._screenElement.appendChild(this._rowContainer);
+ this._screenElement.appendChild(this._selectionContainer);
- this._terminal.linkifier.onLinkHover(e => this._onLinkHover(e));
- this._terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e));
+ this._linkifier.onLinkHover(e => this._onLinkHover(e));
+ this._linkifier.onLinkLeave(e => this._onLinkLeave(e));
}
public dispose(): void {
- this._terminal.element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
- this._terminal.screenElement.removeChild(this._rowContainer);
- this._terminal.screenElement.removeChild(this._selectionContainer);
- this._terminal.screenElement.removeChild(this._themeStyleElement);
- this._terminal.screenElement.removeChild(this._dimensionsStyleElement);
+ this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
+ this._screenElement.removeChild(this._rowContainer);
+ this._screenElement.removeChild(this._selectionContainer);
+ this._screenElement.removeChild(this._themeStyleElement);
+ this._screenElement.removeChild(this._dimensionsStyleElement);
super.dispose();
}
private _updateDimensions(): void {
this.dimensions.scaledCharWidth = this._charSizeService.width * window.devicePixelRatio;
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
- this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing);
- this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight);
+ this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing);
+ this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight);
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.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._bufferService.cols;
+ this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._bufferService.rows;
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
- this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
- this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
+ this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
+ this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
this._rowElements.forEach(element => {
element.style.width = `${this.dimensions.canvasWidth}px`;
@@ -115,7 +122,7 @@ export class DomRenderer extends Disposable implements IRenderer {
if (!this._dimensionsStyleElement) {
this._dimensionsStyleElement = document.createElement('style');
- this._terminal.screenElement.appendChild(this._dimensionsStyleElement);
+ this._screenElement.appendChild(this._dimensionsStyleElement);
}
const styles =
@@ -128,9 +135,9 @@ export class DomRenderer extends Disposable implements IRenderer {
this._dimensionsStyleElement.innerHTML = styles;
- this._selectionContainer.style.height = (this._terminal)._viewportElement.style.height;
- this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
- this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
+ this._selectionContainer.style.height = this._viewportElement.style.height;
+ this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
+ this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
}
public setColors(colors: IColorSet): void {
@@ -141,7 +148,7 @@ export class DomRenderer extends Disposable implements IRenderer {
private _injectCss(): void {
if (!this._themeStyleElement) {
this._themeStyleElement = document.createElement('style');
- this._terminal.screenElement.appendChild(this._themeStyleElement);
+ this._screenElement.appendChild(this._themeStyleElement);
}
// Base CSS
@@ -149,23 +156,29 @@ export class DomRenderer extends Disposable implements IRenderer {
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
` color: ${this._colors.foreground.css};` +
` background-color: ${this._colors.background.css};` +
- ` font-family: ${this._terminal.options.fontFamily};` +
- ` font-size: ${this._terminal.options.fontSize}px;` +
+ ` font-family: ${this._optionsService.options.fontFamily};` +
+ ` font-size: ${this._optionsService.options.fontSize}px;` +
`}`;
// Text styles
styles +=
`${this._terminalSelector} span:not(.${BOLD_CLASS}) {` +
- ` font-weight: ${this._terminal.options.fontWeight};` +
+ ` font-weight: ${this._optionsService.options.fontWeight};` +
`}` +
`${this._terminalSelector} span.${BOLD_CLASS} {` +
- ` font-weight: ${this._terminal.options.fontWeightBold};` +
+ ` font-weight: ${this._optionsService.options.fontWeightBold};` +
`}` +
`${this._terminalSelector} span.${ITALIC_CLASS} {` +
` font-style: italic;` +
`}`;
// Blink animation
styles +=
- `@keyframes blink {` +
+ `@keyframes blink_box_shadow {` +
+ ` 50% {` +
+ ` box-shadow: none;` +
+ ` }` +
+ `}`;
+ styles +=
+ `@keyframes blink_block {` +
` 0% {` +
` background-color: ${this._colors.cursor.css};` +
` color: ${this._colors.cursorAccent.css};` +
@@ -177,21 +190,24 @@ export class DomRenderer extends Disposable implements IRenderer {
`}`;
// Cursor
styles +=
- `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` +
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
` outline: 1px solid ${this._colors.cursor.css};` +
` outline-offset: -1px;` +
`}` +
- `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS} {` +
- ` animation: blink 1s step-end infinite;` +
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` +
+ ` animation: blink_box_shadow 1s step-end infinite;` +
+ `}` +
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
+ ` animation: blink_block 1s step-end infinite;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
` background-color: ${this._colors.cursor.css};` +
` color: ${this._colors.cursorAccent.css};` +
`}` +
- `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
` box-shadow: 1px 0 0 ${this._colors.cursor.css} inset;` +
`}` +
- `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
+ `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` +
`}`;
// Selection
@@ -233,7 +249,7 @@ export class DomRenderer extends Disposable implements IRenderer {
}
// Remove excess elements
while (this._rowElements.length > rows) {
- this._rowContainer.removeChild(this._rowElements.pop());
+ this._rowContainer.removeChild(this._rowElements.pop()!);
}
}
@@ -266,13 +282,13 @@ export class DomRenderer extends Disposable implements IRenderer {
}
// Translate from buffer position to viewport position
- const viewportStartRow = start[1] - this._terminal.buffer.ydisp;
- const viewportEndRow = end[1] - this._terminal.buffer.ydisp;
+ const viewportStartRow = start[1] - this._bufferService.buffer.ydisp;
+ const viewportEndRow = end[1] - this._bufferService.buffer.ydisp;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
- const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1);
+ const viewportCappedEndRow = Math.min(viewportEndRow, this._bufferService.rows - 1);
// No need to draw the selection
- if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) {
+ if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
return;
}
@@ -286,15 +302,15 @@ export class DomRenderer extends Disposable implements IRenderer {
} else {
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
- const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
+ const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._bufferService.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));
+ documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._bufferService.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;
+ const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
}
}
@@ -324,7 +340,6 @@ export class DomRenderer extends Disposable implements IRenderer {
// Force a refresh
this._updateDimensions();
this._injectCss();
- this._terminal.refresh(0, this._terminal.rows - 1);
}
public clear(): void {
@@ -332,20 +347,18 @@ export class DomRenderer extends Disposable implements IRenderer {
}
public renderRows(start: number, end: number): void {
- const terminal = this._terminal;
-
- const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y;
- const cursorX = this._terminal.buffer.x;
- const cursorBlink = this._terminal.options.cursorBlink;
+ const cursorAbsoluteY = this._bufferService.buffer.ybase + this._bufferService.buffer.y;
+ const cursorX = this._bufferService.buffer.x;
+ const cursorBlink = this._optionsService.options.cursorBlink;
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);
- const cursorStyle = terminal.options.cursorStyle;
- rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, terminal.cols));
+ const row = y + this._bufferService.buffer.ydisp;
+ const lineData = this._bufferService.buffer.lines.get(row);
+ const cursorStyle = this._optionsService.options.cursorStyle;
+ rowElement.appendChild(this._rowFactory.createRow(lineData!, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols));
}
}
diff --git a/src/browser/renderer/dom/DomRendererRowFactory.test.ts b/src/browser/renderer/dom/DomRendererRowFactory.test.ts
index b76d644d..c71945cf 100644
--- a/src/browser/renderer/dom/DomRendererRowFactory.test.ts
+++ b/src/browser/renderer/dom/DomRendererRowFactory.test.ts
@@ -11,6 +11,7 @@ import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockOptionsService } from 'common/TestUtils.test';
+import { fromCss } from 'browser/Color';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
@@ -19,7 +20,30 @@ describe('DomRendererRowFactory', () => {
beforeEach(() => {
dom = new jsdom.JSDOM('');
- rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }));
+ rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), {
+ background: fromCss('#010101'),
+ foreground: fromCss('#020202'),
+ ansi: [
+ // dark:
+ fromCss('#2e3436'),
+ fromCss('#cc0000'),
+ fromCss('#4e9a06'),
+ fromCss('#c4a000'),
+ fromCss('#3465a4'),
+ fromCss('#75507b'),
+ fromCss('#06989a'),
+ fromCss('#d3d7cf'),
+ // bright:
+ fromCss('#555753'),
+ fromCss('#ef2929'),
+ fromCss('#8ae234'),
+ fromCss('#fce94f'),
+ fromCss('#729fcf'),
+ fromCss('#ad7fa8'),
+ fromCss('#34e2e2'),
+ fromCss('#eeeeec')
+ ]
+ } as any);
lineData = createEmptyLineData(2);
});
@@ -142,7 +166,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
- 'a'
+ 'a'
);
});
@@ -153,7 +177,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
- 'a'
+ 'a'
);
});
@@ -163,7 +187,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
- 'a'
+ 'a'
);
});
@@ -188,7 +212,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
- 'a'
+ 'a'
);
});
@@ -199,7 +223,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment),
- 'a'
+ 'a'
);
});
});
diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts
index 316645f8..cffc5624 100644
--- a/src/browser/renderer/dom/DomRendererRowFactory.ts
+++ b/src/browser/renderer/dom/DomRendererRowFactory.ts
@@ -5,10 +5,11 @@
import { IBufferLine } from 'common/Types';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
-import { AttributeData } from 'common/buffer/AttributeData';
-import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants';
+import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
-import { ITerminalOptions, IOptionsService } from 'common/services/Services';
+import { IOptionsService } from 'common/services/Services';
+import { ensureContrastRatio } from 'browser/Color';
+import { IColorSet, IColor } from 'browser/Types';
export const BOLD_CLASS = 'xterm-bold';
export const DIM_CLASS = 'xterm-dim';
@@ -24,11 +25,16 @@ export class DomRendererRowFactory {
private _workCell: CellData = new CellData();
constructor(
- private _document: Document,
- private _optionsService: IOptionsService
+ private readonly _document: Document,
+ private readonly _optionsService: IOptionsService,
+ private _colors: IColorSet
) {
}
+ public setColors(colors: IColorSet): void {
+ this._colors = colors;
+ }
+
public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
const fragment = this._document.createDocumentFragment();
@@ -97,36 +103,90 @@ export class DomRendererRowFactory {
charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR;
- const swapColor = this._workCell.isInverse();
-
- // fg
- if (this._workCell.isFgRGB()) {
- let style = charElement.getAttribute('style') || '';
- style += `${swapColor ? 'background-' : ''}color:rgb(${(AttributeData.toColorRGB(this._workCell.getFgColor())).join(',')});`;
- charElement.setAttribute('style', style);
- } else if (this._workCell.isFgPalette()) {
- let fg = this._workCell.getFgColor();
- if (this._workCell.isBold() && fg < 8 && !swapColor && this._optionsService.options.drawBoldTextInBrightColors) {
- fg += 8;
- }
- charElement.classList.add(`xterm-${swapColor ? 'b' : 'f'}g-${fg}`);
- } else if (swapColor) {
- charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
+ let fg = this._workCell.getFgColor();
+ let fgColorMode = this._workCell.getFgColorMode();
+ let bg = this._workCell.getBgColor();
+ let bgColorMode = this._workCell.getBgColorMode();
+ const isInverse = !!this._workCell.isInverse();
+ if (isInverse) {
+ const temp = fg;
+ fg = bg;
+ bg = temp;
+ const temp2 = fgColorMode;
+ fgColorMode = bgColorMode;
+ bgColorMode = temp2;
}
- // bg
- if (this._workCell.isBgRGB()) {
- let style = charElement.getAttribute('style') || '';
- style += `${swapColor ? '' : 'background-'}color:rgb(${(AttributeData.toColorRGB(this._workCell.getBgColor())).join(',')});`;
- charElement.setAttribute('style', style);
- } else if (this._workCell.isBgPalette()) {
- charElement.classList.add(`xterm-${swapColor ? 'f' : 'b'}g-${this._workCell.getBgColor()}`);
- } else if (swapColor) {
- charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
+ // Foreground
+ switch (fgColorMode) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ if (this._workCell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) {
+ fg += 8;
+ }
+ if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg])) {
+ charElement.classList.add(`xterm-fg-${fg}`);
+ }
+ break;
+ case Attributes.CM_RGB:
+ charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}color:#${padStart(fg.toString(16), '0', 6)};`);
+ break;
+ case Attributes.CM_DEFAULT:
+ default:
+ if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground)) {
+ if (isInverse) {
+ charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
+ }
+ }
+ }
+
+ // Background
+ switch (bgColorMode) {
+ case Attributes.CM_P16:
+ case Attributes.CM_P256:
+ charElement.classList.add(`xterm-bg-${bg}`);
+ break;
+ case Attributes.CM_RGB:
+ charElement.setAttribute('style', `${charElement.getAttribute('style') || ''}background-color:#${padStart(bg.toString(16), '0', 6)};`);
+ break;
+ case Attributes.CM_DEFAULT:
+ default:
+ if (isInverse) {
+ charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
+ }
}
fragment.appendChild(charElement);
}
return fragment;
}
+
+ private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor): boolean {
+ if (this._optionsService.options.minimumContrastRatio === 1) {
+ return false;
+ }
+
+ // Try get from cache first
+ let adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg);
+
+ // Calculate and store in cache
+ if (adjustedColor === undefined) {
+ adjustedColor = ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio);
+ this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null);
+ }
+
+ if (adjustedColor) {
+ element.setAttribute('style', `${element.getAttribute('style') || ''}color:${adjustedColor.css}`);
+ return true;
+ }
+
+ return false;
+ }
+}
+
+function padStart(text: string, padChar: string, length: number): string {
+ while (text.length < length) {
+ text = padChar + text;
+ }
+ return text;
}
diff --git a/src/browser/services/CoreBrowserService.ts b/src/browser/services/CoreBrowserService.ts
new file mode 100644
index 00000000..e585f44a
--- /dev/null
+++ b/src/browser/services/CoreBrowserService.ts
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { ICoreBrowserService } from './Services';
+
+export class CoreBrowserService implements ICoreBrowserService {
+ serviceBrand: any;
+
+ constructor(
+ private _textarea: HTMLTextAreaElement
+ ) {
+ }
+
+ public get isFocused(): boolean {
+ return document.activeElement === this._textarea && document.hasFocus();
+ }
+}
diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts
index c0b3c0e5..2949c2c5 100644
--- a/src/browser/services/RenderService.ts
+++ b/src/browser/services/RenderService.ts
@@ -51,6 +51,9 @@ export class RenderService extends Disposable implements IRenderService {
this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged()));
this.register(charSizeService.onCharSizeChange(() => this.onCharSizeChanged()));
+ // No need to register this as renderer is explicitly disposed in RenderService.dispose
+ this._renderer.onRequestRefreshRows(e => this.refreshRows(e.start, e.end));
+
// dprchange should handle this case, we need this as well for browsers that don't support the
// matchMedia query.
this.register(addDisposableDomListener(window, 'resize', () => this.onDevicePixelRatioChange()));
@@ -92,6 +95,7 @@ export class RenderService extends Disposable implements IRenderService {
public changeOptions(): void {
this._renderer.onOptionsChanged();
+ this.refreshRows(0, this._rowCount - 1);
this._fireOnCanvasResize();
}
@@ -105,12 +109,14 @@ export class RenderService extends Disposable implements IRenderService {
public dispose(): void {
this._renderer.dispose();
+ super.dispose();
}
public setRenderer(renderer: IRenderer): void {
- // TODO: RenderCoordinator should be the only one to dispose the renderer
+ // TODO: RenderService should be the only one to dispose the renderer
this._renderer.dispose();
this._renderer = renderer;
+ this._renderer.onRequestRefreshRows(e => this.refreshRows(e.start, e.end));
this.refreshRows(0, this._rowCount - 1);
}
diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts
index e4c703eb..4eb6550c 100644
--- a/src/browser/services/SelectionService.ts
+++ b/src/browser/services/SelectionService.ts
@@ -210,7 +210,7 @@ export class SelectionService implements ISelectionService {
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = buffer.lines.get(i);
const lineText = buffer.translateBufferLineToString(i, true);
- if (bufferLine!.isWrapped) {
+ if (bufferLine && bufferLine.isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
@@ -221,7 +221,7 @@ export class SelectionService implements ISelectionService {
if (start[1] !== end[1]) {
const bufferLine = buffer.lines.get(end[1]);
const lineText = buffer.translateBufferLineToString(end[1], true, 0, end[0]);
- if (bufferLine!.isWrapped) {
+ if (bufferLine && bufferLine!.isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
@@ -563,9 +563,10 @@ export class SelectionService implements ISelectionService {
// to be sent to the pty.
event.stopImmediatePropagation();
- // Something went wrong
+ // Do nothing if there is no selection start, this can happen if the first
+ // click in the terminal is an incremental click
if (!this._model.selectionStart) {
- throw new Error('Selection start position was not set before mousemove event');
+ return;
}
// Record the previous position so we know whether to redraw the selection
@@ -663,7 +664,7 @@ export class SelectionService implements ISelectionService {
this._removeMouseDownListeners();
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) {
- if (event.altKey) {
+ if (event.altKey && this._bufferService.buffer.ybase === this._bufferService.buffer.ydisp) {
const coordinates = this._mouseService.getCoords(
event,
this._element,
@@ -687,9 +688,7 @@ export class SelectionService implements ISelectionService {
// reverseIndex) and delete in a splice is only ever used when the same
// number of elements was just added. Given this is could actually be
// beneficial to leave the selection as is for these cases.
- if (this._trimListener) {
- this._trimListener.dispose();
- }
+ this._trimListener.dispose();
this._trimListener = e.activeBuffer.lines.onTrim(amount => this._onTrim(amount));
}
diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts
index 2170a04e..785fec34 100644
--- a/src/browser/services/Services.ts
+++ b/src/browser/services/Services.ts
@@ -23,6 +23,13 @@ export interface ICharSizeService {
measure(): void;
}
+export const ICoreBrowserService = createDecorator('CoreBrowserService');
+export interface ICoreBrowserService {
+ serviceBrand: any;
+
+ readonly isFocused: boolean;
+}
+
export const IMouseService = createDecorator('MouseService');
export interface IMouseService {
serviceBrand: any;
diff --git a/src/common/EventEmitter.ts b/src/common/EventEmitter.ts
index 34ac190f..5991e338 100644
--- a/src/common/EventEmitter.ts
+++ b/src/common/EventEmitter.ts
@@ -16,11 +16,13 @@ export interface IEvent {
export interface IEventEmitter {
event: IEvent;
fire(data: T): void;
+ dispose(): void;
}
export class EventEmitter implements IEventEmitter {
private _listeners: IListener[] = [];
private _event?: IEvent;
+ private _disposed: boolean = false;
public get event(): IEvent {
if (!this._event) {
@@ -28,10 +30,12 @@ export class EventEmitter implements IEventEmitter {
this._listeners.push(listener);
const disposable = {
dispose: () => {
- for (let i = 0; i < this._listeners.length; i++) {
- if (this._listeners[i] === listener) {
- this._listeners.splice(i, 1);
- return;
+ if (!this._disposed) {
+ for (let i = 0; i < this._listeners.length; i++) {
+ if (this._listeners[i] === listener) {
+ this._listeners.splice(i, 1);
+ return;
+ }
}
}
}
@@ -51,4 +55,11 @@ export class EventEmitter implements IEventEmitter {
queue[i].call(undefined, data);
}
}
+
+ public dispose(): void {
+ if (this._listeners) {
+ this._listeners.length = 0;
+ }
+ this._disposed = true;
+ }
}
diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts
index ec36be2f..7e47d4b0 100644
--- a/src/common/TestUtils.test.ts
+++ b/src/common/TestUtils.test.ts
@@ -3,13 +3,13 @@
* @license MIT
*/
-import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService } from 'common/services/Services';
+import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService } from 'common/services/Services';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { clone } from 'common/Clone';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
import { BufferSet } from 'common/buffer/BufferSet';
-import { IDecPrivateModes } from 'common/Types';
+import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType } from 'common/Types';
export class MockBufferService implements IBufferService {
serviceBrand: any;
@@ -29,13 +29,31 @@ export class MockBufferService implements IBufferService {
reset(): void {}
}
+export class MockCoreMouseService implements ICoreMouseService {
+ activeEncoding: string = '';
+ activeProtocol: string = '';
+ addEncoding(name: string): void {}
+ addProtocol(name: string): void {}
+ reset(): void {}
+ triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; }
+ onProtocolChange: IEvent = new EventEmitter().event;
+ explainEvents(events: CoreMouseEventType): {[event: string]: boolean} {
+ throw new Error('Method not implemented.');
+ }
+}
+
export class MockCoreService implements ICoreService {
+ isCursorInitialized: boolean = false;
+ isCursorHidden: boolean = false;
+ isFocused: boolean = false;
serviceBrand: any;
decPrivateModes: IDecPrivateModes = {} as any;
onData: IEvent = new EventEmitter().event;
onUserInput: IEvent = new EventEmitter().event;
+ onBinary: IEvent = new EventEmitter().event;
reset(): void {}
triggerDataEvent(data: string, wasUserInput?: boolean): void {}
+ triggerBinaryEvent(data: string): void {}
}
export class MockDirtyRowService implements IDirtyRowService {
diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts
index b25b50a5..2dcc704b 100644
--- a/src/common/Types.d.ts
+++ b/src/common/Types.d.ts
@@ -158,3 +158,99 @@ export interface IRowRange {
start: number;
end: number;
}
+
+/**
+ * Interface for mouse events in the core.
+ */
+export const enum CoreMouseButton {
+ LEFT = 0,
+ MIDDLE = 1,
+ RIGHT = 2,
+ NONE = 3,
+ WHEEL = 4,
+ // additional buttons 1..8
+ // untested!
+ AUX1 = 8,
+ AUX2 = 9,
+ AUX3 = 10,
+ AUX4 = 11,
+ AUX5 = 12,
+ AUX6 = 13,
+ AUX7 = 14,
+ AUX8 = 15
+}
+
+export const enum CoreMouseAction {
+ UP = 0, // buttons, wheel
+ DOWN = 1, // buttons, wheel
+ LEFT = 2, // wheel only
+ RIGHT = 3, // wheel only
+ MOVE = 32 // buttons only
+}
+
+export interface ICoreMouseEvent {
+ /** column (zero based). */
+ col: number;
+ /** row (zero based). */
+ row: number;
+ /**
+ * Button the action occured. Due to restrictions of the tracking protocols
+ * it is not possible to report multiple buttons at once.
+ * Wheel is treated as a button.
+ * There are invalid combinations of buttons and actions possible
+ * (like move + wheel), those are silently ignored by the CoreMouseService.
+ */
+ button: CoreMouseButton;
+ action: CoreMouseAction;
+ /**
+ * Modifier states.
+ * Protocols will add/ignore those based on specific restrictions.
+ */
+ ctrl?: boolean;
+ alt?: boolean;
+ shift?: boolean;
+}
+
+/**
+ * CoreMouseEventType
+ * To be reported to the browser component which events a mouse
+ * protocol wants to be catched and forwarded as an ICoreMouseEvent
+ * to CoreMouseService.
+ */
+export const enum CoreMouseEventType {
+ NONE = 0,
+ /** any mousedown event */
+ DOWN = 1,
+ /** any mouseup event */
+ UP = 2,
+ /** any mousemove event while a button is held */
+ DRAG = 4,
+ /** any mousemove event without a button */
+ MOVE = 8,
+ /** any wheel event */
+ WHEEL = 16
+}
+
+/**
+ * Mouse protocol interface.
+ * A mouse protocol can be registered and activated at the CoreMouseService.
+ * `events` should contain a list of needed events as a hint for the browser component
+ * to install/remove the appropriate event handlers.
+ * `restrict` applies further protocol specific restrictions like not allowed
+ * modifiers or filtering invalid event types.
+ */
+export interface ICoreMouseProtocol {
+ events: CoreMouseEventType;
+ restrict: (e: ICoreMouseEvent) => boolean;
+}
+
+/**
+ * CoreMouseEncoding
+ * The tracking encoding can be registered and activated at the CoreMouseService.
+ * If a ICoreMouseEvent passes all procotol restrictions it will be encoded
+ * with the active encoding and sent out.
+ * Note: Returning an empty string will supress sending a mouse report,
+ * which can be used to skip creating falsey reports in limited encodings
+ * (DEFAULT only supports up to 223 1-based as coord value).
+ */
+export type CoreMouseEncoding = (event: ICoreMouseEvent) => string;
diff --git a/src/WindowsMode.ts b/src/common/WindowsMode.ts
similarity index 67%
rename from src/WindowsMode.ts
rename to src/common/WindowsMode.ts
index 0838cae2..ff0e7591 100644
--- a/src/WindowsMode.ts
+++ b/src/common/WindowsMode.ts
@@ -3,11 +3,10 @@
* @license MIT
*/
-import { IDisposable } from 'xterm';
-import { ITerminal } from './Types';
import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
+import { IBufferService } from 'common/services/Services';
-export function applyWindowsMode(terminal: ITerminal): IDisposable {
+export function handleWindowsModeLineFeed(bufferService: IBufferService): void {
// Winpty does not support wraparound mode which means that lines will never
// be marked as wrapped. This causes issues for things like copying a line
// retaining the wrapped new line characters or if consumers are listening
@@ -18,11 +17,11 @@ export function applyWindowsMode(terminal: ITerminal): IDisposable {
// space. This is certainly not without its problems, but generally on
// Windows when text reaches the end of the terminal it's likely going to be
// wrapped.
- return terminal.onLineFeed(() => {
- const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1);
- const lastChar = line.get(terminal.cols - 1);
+ const line = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y - 1);
+ const lastChar = line?.get(bufferService.cols - 1);
- const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y);
+ const nextLine = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y);
+ if (nextLine && lastChar) {
nextLine.isWrapped = (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE);
- });
+ }
}
diff --git a/src/common/buffer/Buffer.test.ts b/src/common/buffer/Buffer.test.ts
index af5abeca..35aade4a 100644
--- a/src/common/buffer/Buffer.test.ts
+++ b/src/common/buffer/Buffer.test.ts
@@ -1163,237 +1163,4 @@ describe('Buffer', () => {
assert.equal(str3, '😁a');
});
});
- // describe('stringIndexToBufferIndex', () => {
- // let terminal: TestTerminal;
-
- // beforeEach(() => {
- // terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
- // });
-
- // it('multiline ascii', () => {
- // const input = 'This is ASCII text spanning multiple lines.';
- // terminal.writeSync(input);
- // const s = terminal.buffer.iterator(true).next().content;
- // assert.equal(input, s);
- // for (let i = 0; i < input.length; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
- // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
- // }
- // });
-
- // it('combining e\u0301 in a sentence', () => {
- // const input = 'Sitting in the cafe\u0301 drinking coffee.';
- // terminal.writeSync(input);
- // const s = terminal.buffer.iterator(true).next().content;
- // assert.equal(input, s);
- // for (let i = 0; i < 19; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
- // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
- // }
- // // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index
- // assert.deepEqual(
- // terminal.buffer.stringIndexToBufferIndex(0, 18),
- // terminal.buffer.stringIndexToBufferIndex(0, 19));
- // // after the combining char every string index has an offset of -1
- // for (let i = 19; i < input.length; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
- // assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
- // }
- // });
-
- // it('multiline combining e\u0301', () => {
- // const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301';
- // terminal.writeSync(input);
- // const s = terminal.buffer.iterator(true).next().content;
- // assert.equal(input, s);
- // // every buffer cell index contains 2 string indices
- // for (let i = 0; i < input.length; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
- // assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
- // }
- // });
-
- // it('surrogate char in a sentence', () => {
- // const input = 'The 𝄞 is a clef widely used in modern notation.';
- // terminal.writeSync(input);
- // const s = terminal.buffer.iterator(true).next().content;
- // assert.equal(input, s);
- // for (let i = 0; i < 5; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
- // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
- // }
- // // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index
- // assert.deepEqual(
- // terminal.buffer.stringIndexToBufferIndex(0, 4),
- // terminal.buffer.stringIndexToBufferIndex(0, 5));
- // // after the combining char every string index has an offset of -1
- // for (let i = 5; i < input.length; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
- // assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
- // }
- // });
-
- // it('multiline surrogate char', () => {
- // const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞';
- // terminal.writeSync(input);
- // const s = terminal.buffer.iterator(true).next().content;
- // assert.equal(input, s);
- // // every buffer cell index contains 2 string indices
- // for (let i = 0; i < input.length; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
- // assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
- // }
- // });
-
- // it('surrogate char with combining', () => {
- // // eye of Ra with acute accent - string length of 3
- // const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.';
- // terminal.writeSync(input);
- // const s = terminal.buffer.iterator(true).next().content;
- // assert.equal(input, s);
- // // index 0..2 should map to 0
- // assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1));
- // assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2));
- // for (let i = 2; i < input.length; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
- // assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex);
- // }
- // });
-
- // it('multiline surrogate with combining', () => {
- // const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301';
- // terminal.writeSync(input);
- // const s = terminal.buffer.iterator(true).next().content;
- // assert.equal(input, s);
- // // every buffer cell index contains 3 string indices
- // for (let i = 0; i < input.length; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
- // assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex);
- // }
- // });
-
- // it('fullwidth chars', () => {
- // const input = 'These 123 are some fat numbers.';
- // terminal.writeSync(input);
- // const s = terminal.buffer.iterator(true).next().content;
- // assert.equal(input, s);
- // for (let i = 0; i < 6; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
- // assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
- // }
- // // string index 6, 7, 8 take 2 cells
- // assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7));
- // assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8));
- // // rest of the string has offset of +3
- // for (let i = 9; i < input.length; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
- // assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex);
- // }
- // });
-
- // it('multiline fullwidth chars', () => {
- // const input = '12345678901234567890';
- // terminal.writeSync(input);
- // const s = terminal.buffer.iterator(true).next().content;
- // assert.equal(input, s);
- // for (let i = 9; i < input.length; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
- // assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex);
- // }
- // });
-
- // it('fullwidth combining with emoji - match emoji cell', () => {
- // const input = 'Lots of ¥\u0301 make me 😃.';
- // terminal.writeSync(input);
- // const s = terminal.buffer.iterator(true).next().content;
- // assert.equal(input, s);
- // const stringIndex = s.match(/😃/).index;
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex);
- // assert(terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars(), '😃');
- // });
-
- // it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => {
- // const input = 'a12345678901234567890';
- // // the 'a' at the beginning moves all fullwidth chars one to the right
- // // now the end of the line contains a dangling empty cell since
- // // the next fullwidth char has to wrap early
- // // the dangling last cell is wrongly added in the string
- // // --> fixable after resolving #1685
- // terminal.writeSync(input);
- // const s = terminal.buffer.iterator(true).next().content;
- // assert.equal(input, s);
- // for (let i = 10; i < input.length; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
- // const j = (i - 0) << 1;
- // assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex);
- // }
- // });
-
- // it('test fully wrapped buffer up to last char', () => {
- // const input = Array(6).join('1234567890');
- // terminal.writeSync(input);
- // const s = terminal.buffer.iterator(true).next().content;
- // assert.equal(input, s);
- // for (let i = 0; i < input.length; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
- // assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars());
- // }
- // });
-
- // it('test fully wrapped buffer up to last char with full width odd', () => {
- // const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'
- // + 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301';
- // terminal.writeSync(input);
- // const s = terminal.buffer.iterator(true).next().content;
- // assert.equal(input, s);
- // for (let i = 0; i < input.length; ++i) {
- // const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true);
- // assert.equal(
- // (!(i % 3))
- // ? input[i]
- // : (i % 3 === 1)
- // ? input.substr(i, 2)
- // : input.substr(i - 1, 2),
- // terminal.buffer.lines.get(bufferIndex[0]).loadCell(bufferIndex[1], new CellData()).getChars());
- // }
- // });
-
- // it('should handle \t in lines correctly', () => {
- // const input = '\thttps://google.de';
- // terminal.writeSync(input);
- // const s = terminal.buffer.iterator(true).next().content;
- // assert.equal(s, Array(optionsService.options.tabStopWidth + 1).join(' ') + 'https://google.de');
- // });
- // });
- // describe('BufferStringIterator', function(): void {
- // it('iterator does not overflow buffer limits', function(): void {
- // const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
- // const data = [
- // 'aaaaaaaaaa',
- // 'aaaaaaaaa\n',
- // 'aaaaaaaaaa',
- // 'aaaaaaaaa\n',
- // 'aaaaaaaaaa',
- // 'aaaaaaaaaa',
- // 'aaaaaaaaaa',
- // 'aaaaaaaaa\n',
- // 'aaaaaaaaaa',
- // 'aaaaaaaaaa'
- // ];
- // terminal.writeSync(data.join(''));
- // // brute force test with insane values
- // expect(() => {
- // for (let overscan = 0; overscan < 20; ++overscan) {
- // for (let start = -10; start < 20; ++start) {
- // for (let end = -10; end < 20; ++end) {
- // const it = terminal.buffer.iterator(false, start, end, overscan, overscan);
- // while (it.hasNext()) {
- // it.next();
- // }
- // }
- // }
- // }
- // }).to.not.throw();
- // });
- // });
});
diff --git a/src/common/buffer/Buffer.ts b/src/common/buffer/Buffer.ts
index 1b3e5d86..152bab7a 100644
--- a/src/common/buffer/Buffer.ts
+++ b/src/common/buffer/Buffer.ts
@@ -167,19 +167,25 @@ export class Buffer implements IBuffer {
if (this._rows < newRows) {
for (let y = this._rows; y < newRows; y++) {
if (this.lines.length < newRows + this.ybase) {
- if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {
- // There is room above the buffer and there are no empty elements below the line,
- // scroll up
- this.ybase--;
- addToY++;
- if (this.ydisp > 0) {
- // Viewport is at the top of the buffer, must increase downwards
- this.ydisp--;
- }
- } else {
- // Add a blank line if there is no buffer left at the top to scroll to, or if there
- // are blank lines after the cursor
+ if (this._optionsService.options.windowsMode) {
+ // Just add the new missing rows on Windows as conpty reprints the screen with it's
+ // view of the world. Once a line enters scrollback for conpty it remains there
this.lines.push(new BufferLine(newCols, nullCell));
+ } else {
+ if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {
+ // There is room above the buffer and there are no empty elements below the line,
+ // scroll up
+ this.ybase--;
+ addToY++;
+ if (this.ydisp > 0) {
+ // Viewport is at the top of the buffer, must increase downwards
+ this.ydisp--;
+ }
+ } else {
+ // Add a blank line if there is no buffer left at the top to scroll to, or if there
+ // are blank lines after the cursor
+ this.lines.push(new BufferLine(newCols, nullCell));
+ }
}
}
}
diff --git a/src/common/buffer/BufferLine.ts b/src/common/buffer/BufferLine.ts
index 8e742be3..1e95e004 100644
--- a/src/common/buffer/BufferLine.ts
+++ b/src/common/buffer/BufferLine.ts
@@ -5,7 +5,7 @@
import { CharData, IBufferLine, ICellData } from 'common/Types';
import { stringFromCodePoint } from 'common/input/TextDecoder';
-import { DEFAULT_COLOR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants';
+import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { AttributeData } from 'common/buffer/AttributeData';
diff --git a/src/common/buffer/Marker.ts b/src/common/buffer/Marker.ts
index 7f52ac99..9a4256d3 100644
--- a/src/common/buffer/Marker.ts
+++ b/src/common/buffer/Marker.ts
@@ -29,6 +29,7 @@ export class Marker extends Disposable implements IMarker {
return;
}
this.isDisposed = true;
+ this.line = -1;
// Emit before super.dispose such that dispose listeners get a change to react
this._onDispose.fire();
}
diff --git a/src/common/buffer/Types.d.ts b/src/common/buffer/Types.d.ts
index 532230ec..e229d69c 100644
--- a/src/common/buffer/Types.d.ts
+++ b/src/common/buffer/Types.d.ts
@@ -40,7 +40,7 @@ export interface IBuffer {
nextStop(x?: number): number;
prevStop(x?: number): number;
getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine;
- stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[];
+ stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight?: boolean): number[];
iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator;
getNullCell(attr?: IAttributeData): ICellData;
getWhitespaceCell(attr?: IAttributeData): ICellData;
diff --git a/src/common/input/Keyboard.test.ts b/src/common/input/Keyboard.test.ts
index 409a3192..a304923e 100644
--- a/src/common/input/Keyboard.test.ts
+++ b/src/common/input/Keyboard.test.ts
@@ -108,6 +108,12 @@ describe('Keyboard', () => {
it('should return \\x1b[5C for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: false }).key, '\x1b[1;5C'); // CSI 5 C
});
+ it('should return \\x1b[5D for alt+up', () => {
+ assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: false }).key, '\x1b[1;5A'); // CSI 5 D
+ });
+ it('should return \\x1b[5C for alt+down', () => {
+ assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: false }).key, '\x1b[1;5B'); // CSI 5 C
+ });
it('should return \\x1ba for alt+a', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: false }).key, '\x1ba');
});
@@ -120,6 +126,12 @@ describe('Keyboard', () => {
it('should return \\x1bf for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: true }).key, '\x1bf'); // CSI 5 C
});
+ it('should return \\x1bb for alt+up', () => {
+ assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: true }).key, '\x1b[1;3A'); // CSI 5 D
+ });
+ it('should return \\x1bf for alt+down', () => {
+ assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: true }).key, '\x1b[1;3B'); // CSI 5 C
+ });
it('should return undefined for alt+a', () => {
assert.strictEqual(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true }).key, undefined), { isMac: true };
});
diff --git a/src/common/input/Keyboard.ts b/src/common/input/Keyboard.ts
index 3cc0eb76..1bf378c1 100644
--- a/src/common/input/Keyboard.ts
+++ b/src/common/input/Keyboard.ts
@@ -113,13 +113,16 @@ export function evaluateKeyboardEvent(
break;
case 37:
// left-arrow
+ if (ev.metaKey) {
+ break;
+ }
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D';
// HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards
// http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux
if (result.key === C0.ESC + '[1;3D') {
- result.key = isMac ? C0.ESC + 'b' : C0.ESC + '[1;5D';
+ result.key = C0.ESC + (isMac ? 'b' : '[1;5D');
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OD';
@@ -129,13 +132,16 @@ export function evaluateKeyboardEvent(
break;
case 39:
// right-arrow
+ if (ev.metaKey) {
+ break;
+ }
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C';
// HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward
// http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux
if (result.key === C0.ESC + '[1;3C') {
- result.key = isMac ? C0.ESC + 'f' : C0.ESC + '[1;5C';
+ result.key = C0.ESC + (isMac ? 'f' : '[1;5C');
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OC';
@@ -145,11 +151,15 @@ export function evaluateKeyboardEvent(
break;
case 38:
// up-arrow
+ if (ev.metaKey) {
+ break;
+ }
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A';
// HACK: Make Alt + up-arrow behave like Ctrl + up-arrow
// http://unix.stackexchange.com/a/108106
- if (result.key === C0.ESC + '[1;3A') {
+ // macOS uses different escape sequences than linux
+ if (!isMac && result.key === C0.ESC + '[1;3A') {
result.key = C0.ESC + '[1;5A';
}
} else if (applicationCursorMode) {
@@ -160,11 +170,15 @@ export function evaluateKeyboardEvent(
break;
case 40:
// down-arrow
+ if (ev.metaKey) {
+ break;
+ }
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B';
// HACK: Make Alt + down-arrow behave like Ctrl + down-arrow
// http://unix.stackexchange.com/a/108106
- if (result.key === C0.ESC + '[1;3B') {
+ // macOS uses different escape sequences than linux
+ if (!isMac && result.key === C0.ESC + '[1;3B') {
result.key = C0.ESC + '[1;5B';
}
} else if (applicationCursorMode) {
@@ -316,23 +330,18 @@ export function evaluateKeyboardEvent(
if (ev.keyCode >= 65 && ev.keyCode <= 90) {
result.key = String.fromCharCode(ev.keyCode - 64);
} else if (ev.keyCode === 32) {
- // NUL
- result.key = String.fromCharCode(0);
+ result.key = C0.NUL;
} else if (ev.keyCode >= 51 && ev.keyCode <= 55) {
// escape, file sep, group sep, record sep, unit sep
result.key = String.fromCharCode(ev.keyCode - 51 + 27);
} else if (ev.keyCode === 56) {
- // delete
- result.key = String.fromCharCode(127);
+ result.key = C0.DEL;
} else if (ev.keyCode === 219) {
- // ^[ - Control Sequence Introducer (CSI)
- result.key = String.fromCharCode(27);
+ result.key = C0.ESC;
} else if (ev.keyCode === 220) {
- // ^\ - String Terminator (ST)
- result.key = String.fromCharCode(28);
+ result.key = C0.FS;
} else if (ev.keyCode === 221) {
- // ^] - Operating System Command (OSC)
- result.key = String.fromCharCode(29);
+ result.key = C0.GS;
}
} else if ((!isMac || macOptionIsMeta) && ev.altKey && !ev.metaKey) {
// On macOS this is a third level shift when !macOptionIsMeta. Use instead.
diff --git a/src/common/input/TextDecoder.test.ts b/src/common/input/TextDecoder.test.ts
index cda74a18..92b0e03a 100644
--- a/src/common/input/TextDecoder.test.ts
+++ b/src/common/input/TextDecoder.test.ts
@@ -7,6 +7,7 @@ import { assert } from 'chai';
import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32, utf32ToString } from 'common/input/TextDecoder';
import { encode } from 'utf8';
+
// convert UTF32 codepoints to string
function toString(data: Uint32Array, length: number): string {
if ((String as any).fromCodePoint) {
@@ -214,6 +215,16 @@ describe('text encodings', () => {
}
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
});
+ it('test break after 3 bytes - issue #2495', () => {
+ const decoder = new Utf8ToUtf32();
+ const target = new Uint32Array(5);
+ const utf8Data = fromByteString('\xf0\xa0\x9c\x8e');
+ let written = decoder.decode(utf8Data.slice(0, 3), target);
+ assert.equal(written, 0);
+ written = decoder.decode(utf8Data.slice(3), target);
+ assert.equal(written, 1);
+ assert(toString(target, written), '𠜎');
+ });
});
});
});
diff --git a/src/common/input/TextDecoder.ts b/src/common/input/TextDecoder.ts
index 7e141e02..397d25a7 100644
--- a/src/common/input/TextDecoder.ts
+++ b/src/common/input/TextDecoder.ts
@@ -194,7 +194,7 @@ export class Utf8ToUtf32 {
target[size++] = cp;
}
} else {
- if (codepoint < 0x010000 || codepoint > 0x10FFFF) {
+ if (cp < 0x010000 || cp > 0x10FFFF) {
// illegal codepoint
} else {
target[size++] = cp;
diff --git a/src/common/input/WriteBuffer.test.ts b/src/common/input/WriteBuffer.test.ts
new file mode 100644
index 00000000..ab9433da
--- /dev/null
+++ b/src/common/input/WriteBuffer.test.ts
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { assert } from 'chai';
+import { WriteBuffer } from './WriteBuffer';
+
+declare let Buffer: any;
+
+function toBytes(s: string): Uint8Array {
+ return Buffer.from(s);
+}
+
+function fromBytes(bytes: Uint8Array): string {
+ return bytes.toString();
+}
+
+describe('WriteBuffer', () => {
+ let wb: WriteBuffer;
+ let stack: (string | Uint8Array)[] = [];
+ let cbStack: string[] = [];
+ beforeEach(() => {
+ stack = [];
+ cbStack = [];
+ wb = new WriteBuffer(data => { stack.push(data); });
+ });
+ describe('write input', () => {
+ it('string', done => {
+ wb.write('a._');
+ wb.write('b.x', () => { cbStack.push('b'); });
+ wb.write('c._');
+ wb.write('d.x', () => { cbStack.push('d'); });
+ wb.write('e', () => {
+ assert.deepEqual(stack, ['a._', 'b.x', 'c._', 'd.x', 'e']);
+ assert.deepEqual(cbStack, ['b', 'd']);
+ done();
+ });
+ });
+ it('bytes', done => {
+ wb.write(toBytes('a._'));
+ wb.write(toBytes('b.x'), () => { cbStack.push('b'); });
+ wb.write(toBytes('c._'));
+ wb.write(toBytes('d.x'), () => { cbStack.push('d'); });
+ wb.write(toBytes('e'), () => {
+ assert.deepEqual(stack.map(val => typeof val === 'string' ? '' : fromBytes(val)), ['a._', 'b.x', 'c._', 'd.x', 'e']);
+ assert.deepEqual(cbStack, ['b', 'd']);
+ done();
+ });
+ });
+ it('string/bytes mixed', done => {
+ wb.write('a._');
+ wb.write('b.x', () => { cbStack.push('b'); });
+ wb.write(toBytes('c._'));
+ wb.write(toBytes('d.x'), () => { cbStack.push('d'); });
+ wb.write(toBytes('e'), () => {
+ assert.deepEqual(stack.map(val => typeof val === 'string' ? val : fromBytes(val)), ['a._', 'b.x', 'c._', 'd.x', 'e']);
+ assert.deepEqual(cbStack, ['b', 'd']);
+ done();
+ });
+ });
+ it('write callback works for empty chunks', done => {
+ wb.write('a', () => { cbStack.push('a'); });
+ wb.write('', () => { cbStack.push('b'); });
+ wb.write(toBytes('c'), () => { cbStack.push('c'); });
+ wb.write(new Uint8Array(0), () => { cbStack.push('d'); });
+ wb.write('e', () => {
+ assert.deepEqual(stack.map(val => typeof val === 'string' ? val : fromBytes(val)), ['a', '', 'c', '', 'e']);
+ assert.deepEqual(cbStack, ['a', 'b', 'c', 'd']);
+ done();
+ });
+ });
+ it('writeSync', done => {
+ wb.write('a', () => { cbStack.push('a'); });
+ wb.write('b', () => { cbStack.push('b'); });
+ wb.write('c', () => { cbStack.push('c'); });
+ wb.writeSync('d');
+ assert.deepEqual(stack, ['a', 'b', 'c', 'd']);
+ assert.deepEqual(cbStack, ['a', 'b', 'c']);
+ wb.write('x', () => { cbStack.push('x'); });
+ wb.write('', () => {
+ assert.deepEqual(stack, ['a', 'b', 'c', 'd', 'x', '']);
+ assert.deepEqual(cbStack, ['a', 'b', 'c', 'x']);
+ done();
+ });
+ });
+ });
+});
diff --git a/src/common/input/WriteBuffer.ts b/src/common/input/WriteBuffer.ts
new file mode 100644
index 00000000..c7d82458
--- /dev/null
+++ b/src/common/input/WriteBuffer.ts
@@ -0,0 +1,110 @@
+
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+declare const setTimeout: (handler: () => void, timeout?: number) => void;
+
+/**
+ * Safety watermark to avoid memory exhaustion and browser engine crash on fast data input.
+ * Enable flow control to avoid this limit and make sure that your backend correctly
+ * propagates this to the underlying pty. (see docs for further instructions)
+ * Since this limit is meant as a safety parachute to prevent browser crashs,
+ * it is set to a very high number. Typically xterm.js gets unresponsive with
+ * a 100 times lower number (>500 kB).
+ */
+const DISCARD_WATERMARK = 50000000; // ~50 MB
+
+/**
+ * The max number of ms to spend on writes before allowing the renderer to
+ * catch up with a 0ms setTimeout. A value of < 33 to keep us close to
+ * 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS
+ * depends on the time it takes for the renderer to draw the frame.
+ */
+const WRITE_TIMEOUT_MS = 12;
+
+/**
+ * Threshold of max held chunks in the write buffer, that were already processed.
+ * This is a tradeoff between extensive write buffer shifts (bad runtime) and high
+ * memory consumption by data thats not used anymore.
+ */
+const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
+
+export class WriteBuffer {
+ private _writeBuffer: (string | Uint8Array)[] = [];
+ private _callbacks: ((() => void) | undefined)[] = [];
+ private _pendingData = 0;
+ private _bufferOffset = 0;
+
+ constructor(private _action: (data: string | Uint8Array) => void) { }
+
+ public writeSync(data: string | Uint8Array): void {
+ // force sync processing on pending data chunks to avoid in-band data scrambling
+ // does the same as innerWrite but without event loop
+ if (this._writeBuffer.length) {
+ for (let i = this._bufferOffset; i < this._writeBuffer.length; ++i) {
+ const data = this._writeBuffer[i];
+ const cb = this._callbacks[i];
+ this._action(data);
+ if (cb) cb();
+ }
+ // reset all to avoid reprocessing of chunks with scheduled innerWrite call
+ this._writeBuffer = [];
+ this._callbacks = [];
+ this._pendingData = 0;
+ // stop scheduled innerWrite by offset > length condition
+ this._bufferOffset = 0x7FFFFFFF;
+ }
+ // handle current data chunk
+ this._action(data);
+ }
+
+ public write(data: string | Uint8Array, callback?: () => void): void {
+ if (this._pendingData > DISCARD_WATERMARK) {
+ throw new Error('write data discarded, use flow control to avoid losing data');
+ }
+
+ // schedule chunk processing for next event loop run
+ if (!this._writeBuffer.length) {
+ this._bufferOffset = 0;
+ setTimeout(() => this._innerWrite());
+ }
+
+ this._pendingData += data.length;
+ this._writeBuffer.push(data);
+ this._callbacks.push(callback);
+ }
+
+ protected _innerWrite(): void {
+ const startTime = Date.now();
+ while (this._writeBuffer.length > this._bufferOffset) {
+ const data = this._writeBuffer[this._bufferOffset];
+ const cb = this._callbacks[this._bufferOffset];
+ this._bufferOffset++;
+
+ this._action(data);
+ this._pendingData -= data.length;
+ if (cb) cb();
+
+ if (Date.now() - startTime >= WRITE_TIMEOUT_MS) {
+ break;
+ }
+ }
+ if (this._writeBuffer.length > this._bufferOffset) {
+ // Allow renderer to catch up before processing the next batch
+ // trim already processed chunks if we are above threshold
+ if (this._bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) {
+ this._writeBuffer = this._writeBuffer.slice(this._bufferOffset);
+ this._callbacks = this._callbacks.slice(this._bufferOffset);
+ this._bufferOffset = 0;
+ }
+ setTimeout(() => this._innerWrite(), 0);
+ } else {
+ this._writeBuffer = [];
+ this._callbacks = [];
+ this._pendingData = 0;
+ this._bufferOffset = 0;
+ }
+ }
+}
diff --git a/src/common/parser/Constants.ts b/src/common/parser/Constants.ts
index 55e4a005..85156c3e 100644
--- a/src/common/parser/Constants.ts
+++ b/src/common/parser/Constants.ts
@@ -43,3 +43,16 @@ export const enum ParserAction {
DCS_PUT = 13,
DCS_UNHOOK = 14
}
+
+/**
+ * Internal states of OscParser.
+ */
+export const enum OscState {
+ START = 0,
+ ID = 1,
+ PAYLOAD = 2,
+ ABORT = 3
+}
+
+// payload limit for OSC and DCS
+export const PAYLOAD_LIMIT = 10000000;
diff --git a/src/common/parser/DcsParser.test.ts b/src/common/parser/DcsParser.test.ts
new file mode 100644
index 00000000..4d6dce11
--- /dev/null
+++ b/src/common/parser/DcsParser.test.ts
@@ -0,0 +1,253 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+import { assert } from 'chai';
+import { DcsParser, DcsHandler } from 'common/parser/DcsParser';
+import { IDcsHandler, IParams, IFunctionIdentifier } from 'common/parser/Types';
+import { utf32ToString, StringToUtf32 } from 'common/input/TextDecoder';
+import { Params } from 'common/parser/Params';
+import { PAYLOAD_LIMIT } from 'common/parser/Constants';
+
+function toUtf32(s: string): Uint32Array {
+ const utf32 = new Uint32Array(s.length);
+ const decoder = new StringToUtf32();
+ const length = decoder.decode(s, utf32);
+ return utf32.subarray(0, length);
+}
+
+function identifier(id: IFunctionIdentifier): number {
+ let res = 0;
+ if (id.prefix) {
+ if (id.prefix.length > 1) {
+ throw new Error('only one byte as prefix supported');
+ }
+ res = id.prefix.charCodeAt(0);
+ if (res && 0x3c > res || res > 0x3f) {
+ throw new Error('prefix must be in range 0x3c .. 0x3f');
+ }
+ }
+ if (id.intermediates) {
+ if (id.intermediates.length > 2) {
+ throw new Error('only two bytes as intermediates are supported');
+ }
+ for (let i = 0; i < id.intermediates.length; ++i) {
+ const intermediate = id.intermediates.charCodeAt(i);
+ if (0x20 > intermediate || intermediate > 0x2f) {
+ throw new Error('intermediate must be in range 0x20 .. 0x2f');
+ }
+ res <<= 8;
+ res |= intermediate;
+ }
+ }
+ if (id.final.length !== 1) {
+ throw new Error('final must be a single byte');
+ }
+ const finalCode = id.final.charCodeAt(0);
+ if (0x40 > finalCode || finalCode > 0x7e) {
+ throw new Error('final must be in range 0x40 .. 0x7e');
+ }
+ res <<= 8;
+ res |= finalCode;
+
+ return res;
+}
+
+class TestHandler implements IDcsHandler {
+ constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
+ hook(params: IParams): void {
+ this.output.push([this.msg, 'HOOK', params.toArray()]);
+ }
+ put(data: Uint32Array, start: number, end: number): void {
+ this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
+ }
+ unhook(success: boolean): void | boolean {
+ this.output.push([this.msg, 'UNHOOK', success]);
+ if (this.returnFalse) {
+ return false;
+ }
+ }
+}
+
+describe('DcsParser', () => {
+ let parser: DcsParser;
+ let reports: any[] = [];
+ beforeEach(() => {
+ reports = [];
+ parser = new DcsParser();
+ parser.setHandlerFallback((id, action, data) => {
+ if (action === 'HOOK') {
+ data = data.toArray();
+ }
+ reports.push([id, action, data]);
+ });
+ });
+ describe('handler registration', () => {
+ it('setDcsHandler', () => {
+ parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
+ parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
+ let data = toUtf32('Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32('the mouse!');
+ parser.put(data, 0, data.length);
+ parser.unhook(true);
+ assert.deepEqual(reports, [
+ // messages from TestHandler
+ ['th', 'HOOK', [1, 2, 3]],
+ ['th', 'PUT', 'Here comes'],
+ ['th', 'PUT', 'the mouse!'],
+ ['th', 'UNHOOK', true]
+ ]);
+ });
+ it('clearDcsHandler', () => {
+ parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th'));
+ parser.clearHandler(identifier({intermediates: '+', final: 'p'}));
+ parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
+ let data = toUtf32('Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32('the mouse!');
+ parser.put(data, 0, data.length);
+ parser.unhook(true);
+ assert.deepEqual(reports, [
+ // messages from fallback handler
+ [identifier({intermediates: '+', final: 'p'}), 'HOOK', [1, 2, 3]],
+ [identifier({intermediates: '+', final: 'p'}), 'PUT', 'Here comes'],
+ [identifier({intermediates: '+', final: 'p'}), 'PUT', 'the mouse!'],
+ [identifier({intermediates: '+', final: 'p'}), 'UNHOOK', true]
+ ]);
+ });
+ it('addDcsHandler', () => {
+ parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
+ parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2'));
+ parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
+ let data = toUtf32('Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32('the mouse!');
+ parser.put(data, 0, data.length);
+ parser.unhook(true);
+ assert.deepEqual(reports, [
+ ['th2', 'HOOK', [1, 2, 3]],
+ ['th1', 'HOOK', [1, 2, 3]],
+ ['th2', 'PUT', 'Here comes'],
+ ['th1', 'PUT', 'Here comes'],
+ ['th2', 'PUT', 'the mouse!'],
+ ['th1', 'PUT', 'the mouse!'],
+ ['th2', 'UNHOOK', true],
+ ['th1', 'UNHOOK', false] // false due being already handled by th2!
+ ]);
+ });
+ it('addDcsHandler with return false', () => {
+ parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
+ parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
+ parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
+ let data = toUtf32('Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32('the mouse!');
+ parser.put(data, 0, data.length);
+ parser.unhook(true);
+ assert.deepEqual(reports, [
+ ['th2', 'HOOK', [1, 2, 3]],
+ ['th1', 'HOOK', [1, 2, 3]],
+ ['th2', 'PUT', 'Here comes'],
+ ['th1', 'PUT', 'Here comes'],
+ ['th2', 'PUT', 'the mouse!'],
+ ['th1', 'PUT', 'the mouse!'],
+ ['th2', 'UNHOOK', true],
+ ['th1', 'UNHOOK', true] // true since th2 indicated to keep bubbling
+ ]);
+ });
+ it('dispose handlers', () => {
+ parser.setHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th1'));
+ const dispo = parser.addHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 'th2', true));
+ dispo.dispose();
+ parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
+ let data = toUtf32('Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32('the mouse!');
+ parser.put(data, 0, data.length);
+ parser.unhook(true);
+ assert.deepEqual(reports, [
+ ['th1', 'HOOK', [1, 2, 3]],
+ ['th1', 'PUT', 'Here comes'],
+ ['th1', 'PUT', 'the mouse!'],
+ ['th1', 'UNHOOK', true]
+ ]);
+ });
+ });
+ describe('DcsHandlerFactory', () => {
+ it('should be called once on end(true)', () => {
+ parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
+ parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
+ let data = toUtf32('Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32(' the mouse!');
+ parser.put(data, 0, data.length);
+ parser.unhook(true);
+ assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]);
+ });
+ it('should not be called on end(false)', () => {
+ parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
+ parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
+ let data = toUtf32('Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32(' the mouse!');
+ parser.put(data, 0, data.length);
+ parser.unhook(false);
+ assert.deepEqual(reports, []);
+ });
+ it('should be disposable', () => {
+ parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data])));
+ const dispo = parser.addHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['two', params.toArray(), data])));
+ parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
+ let data = toUtf32('Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32(' the mouse!');
+ parser.put(data, 0, data.length);
+ parser.unhook(true);
+ assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]);
+ dispo.dispose();
+ parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
+ data = toUtf32('some other');
+ parser.put(data, 0, data.length);
+ data = toUtf32(' data');
+ parser.put(data, 0, data.length);
+ parser.unhook(true);
+ assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]);
+ });
+ it('should respect return false', () => {
+ parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push(['one', params.toArray(), data])));
+ parser.addHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => { reports.push(['two', params.toArray(), data]); return false; }));
+ parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
+ let data = toUtf32('Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32(' the mouse!');
+ parser.put(data, 0, data.length);
+ parser.unhook(true);
+ assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]);
+ });
+ it('should work up to payload limit', function(): void {
+ this.timeout(10000);
+ parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
+ parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
+ const data = toUtf32('A'.repeat(1000));
+ for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
+ parser.put(data, 0, data.length);
+ }
+ parser.unhook(true);
+ assert.deepEqual(reports, [[[1, 2, 3], 'A'.repeat(PAYLOAD_LIMIT)]]);
+ });
+ it('should abort for payload limit +1', function(): void {
+ this.timeout(10000);
+ parser.setHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler((data, params) => reports.push([params.toArray(), data])));
+ parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
+ let data = toUtf32('A'.repeat(1000));
+ for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
+ parser.put(data, 0, data.length);
+ }
+ data = toUtf32('A');
+ parser.put(data, 0, data.length);
+ parser.unhook(true);
+ assert.deepEqual(reports, []);
+ });
+ });
+});
diff --git a/src/common/parser/DcsParser.ts b/src/common/parser/DcsParser.ts
new file mode 100644
index 00000000..4622c4ad
--- /dev/null
+++ b/src/common/parser/DcsParser.ts
@@ -0,0 +1,146 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { IDisposable } from 'common/Types';
+import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType } from 'common/parser/Types';
+import { utf32ToString } from 'common/input/TextDecoder';
+import { Params } from 'common/parser/Params';
+import { PAYLOAD_LIMIT } from 'common/parser/Constants';
+
+const EMPTY_HANDLERS: IDcsHandler[] = [];
+
+export class DcsParser implements IDcsParser {
+ private _handlers: IHandlerCollection = Object.create(null);
+ private _active: IDcsHandler[] = EMPTY_HANDLERS;
+ private _ident: number = 0;
+ private _handlerFb: DcsFallbackHandlerType = () => {};
+
+ public dispose(): void {
+ this._handlers = Object.create(null);
+ this._handlerFb = () => {};
+ }
+
+ public addHandler(ident: number, handler: IDcsHandler): IDisposable {
+ if (this._handlers[ident] === undefined) {
+ this._handlers[ident] = [];
+ }
+ const handlerList = this._handlers[ident];
+ handlerList.push(handler);
+ return {
+ dispose: () => {
+ const handlerIndex = handlerList.indexOf(handler);
+ if (handlerIndex !== -1) {
+ handlerList.splice(handlerIndex, 1);
+ }
+ }
+ };
+ }
+
+ public setHandler(ident: number, handler: IDcsHandler): void {
+ this._handlers[ident] = [handler];
+ }
+
+ public clearHandler(ident: number): void {
+ if (this._handlers[ident]) delete this._handlers[ident];
+ }
+
+ public setHandlerFallback(handler: DcsFallbackHandlerType): void {
+ this._handlerFb = handler;
+ }
+
+ public reset(): void {
+ if (this._active.length) {
+ this.unhook(false);
+ }
+ this._active = EMPTY_HANDLERS;
+ this._ident = 0;
+ }
+
+ public hook(ident: number, params: IParams): void {
+ // always reset leftover handlers
+ this.reset();
+ this._ident = ident;
+ this._active = this._handlers[ident] || EMPTY_HANDLERS;
+ if (!this._active.length) {
+ this._handlerFb(this._ident, 'HOOK', params);
+ } else {
+ for (let j = this._active.length - 1; j >= 0; j--) {
+ this._active[j].hook(params);
+ }
+ }
+ }
+
+ public put(data: Uint32Array, start: number, end: number): void {
+ if (!this._active.length) {
+ this._handlerFb(this._ident, 'PUT', utf32ToString(data, start, end));
+ } else {
+ for (let j = this._active.length - 1; j >= 0; j--) {
+ this._active[j].put(data, start, end);
+ }
+ }
+ }
+
+ public unhook(success: boolean): void {
+ if (!this._active.length) {
+ this._handlerFb(this._ident, 'UNHOOK', success);
+ } else {
+ let j = this._active.length - 1;
+ for (; j >= 0; j--) {
+ if (this._active[j].unhook(success) !== false) {
+ break;
+ }
+ }
+ j--;
+ // cleanup left over handlers
+ for (; j >= 0; j--) {
+ this._active[j].unhook(false);
+ }
+ }
+ this._active = EMPTY_HANDLERS;
+ this._ident = 0;
+ }
+}
+
+/**
+ * Convenient class to create a DCS handler from a single callback function.
+ * Note: The payload is currently limited to 50 MB (hardcoded).
+ */
+export class DcsHandler implements IDcsHandler {
+ private _data = '';
+ private _params: IParams | undefined;
+ private _hitLimit: boolean = false;
+
+ constructor(private _handler: (data: string, params: IParams) => any) {}
+
+ public hook(params: IParams): void {
+ this._params = params.clone();
+ this._data = '';
+ this._hitLimit = false;
+ }
+
+ public put(data: Uint32Array, start: number, end: number): void {
+ if (this._hitLimit) {
+ return;
+ }
+ this._data += utf32ToString(data, start, end);
+ if (this._data.length > PAYLOAD_LIMIT) {
+ this._data = '';
+ this._hitLimit = true;
+ }
+ }
+
+ public unhook(success: boolean): any {
+ let ret;
+ if (this._hitLimit) {
+ ret = false;
+ } else if (success) {
+ ret = this._handler(this._data, this._params ? this._params : new Params());
+ }
+ this._params = undefined;
+ this._data = '';
+ this._hitLimit = false;
+ return ret;
+ }
+}
diff --git a/src/common/parser/EscapeSequenceParser.test.ts b/src/common/parser/EscapeSequenceParser.test.ts
index 90ec0ee0..fa6feb5c 100644
--- a/src/common/parser/EscapeSequenceParser.test.ts
+++ b/src/common/parser/EscapeSequenceParser.test.ts
@@ -3,12 +3,15 @@
* @license MIT
*/
-import { IDcsHandler, IParsingState, IParams, ParamsArray } from 'common/parser/Types';
+import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier } from 'common/parser/Types';
import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser';
import * as chai from 'chai';
-import { StringToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
+import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder';
import { ParserState } from 'common/parser/Constants';
import { Params } from 'common/parser/Params';
+import { OscHandler } from 'common/parser/OscParser';
+import { IDisposable } from 'common/Types';
+import { DcsHandler } from 'common/parser/DcsParser';
function r(a: number, b: number): string[] {
@@ -20,13 +23,46 @@ function r(a: number, b: number): string[] {
return arr;
}
+class MockOscPutParser implements IOscParser {
+ private _fallback: OscFallbackHandlerType = () => {};
+ public data = '';
+ public reset(): void {
+ this.data = '';
+ }
+ public put(data: Uint32Array, start: number, end: number): void {
+ this.data += utf32ToString(data, start, end);
+ }
+ public dispose(): void { }
+ public start(): void { }
+ public end(success: boolean): void {
+ this.data += `, success: ${success}`;
+ const id = parseInt(this.data.slice(0, this.data.indexOf(';')));
+ if (!isNaN(id)) {
+ this._fallback(id, 'END', this.data.slice(this.data.indexOf(';') + 1));
+ }
+ }
+ addHandler(ident: number, handler: IOscHandler): IDisposable {
+ throw new Error('not implemented');
+ }
+ setHandler(ident: number, handler: IOscHandler): void {
+ throw new Error('not implemented');
+ }
+ clearHandler(ident: number): void {
+ throw new Error('not implemented');
+ }
+ setHandlerFallback(handler: OscFallbackHandlerType): void {
+ this._fallback = handler;
+ }
+}
+const oscPutParser = new MockOscPutParser();
+
// derived parser with access to internal states
class TestEscapeSequenceParser extends EscapeSequenceParser {
public get osc(): string {
- return this._osc;
+ return (this._oscParser as MockOscPutParser).data;
}
public set osc(value: string) {
- this._osc = value;
+ (this._oscParser as MockOscPutParser).data = value;
}
public get params(): ParamsArray {
return this._params.toArray();
@@ -38,13 +74,20 @@ class TestEscapeSequenceParser extends EscapeSequenceParser {
return this._params;
}
public get collect(): string {
- return this._collect;
+ return this.identToString(this._collect);
}
public set collect(value: string) {
- this._collect = value;
+ this._collect = 0;
+ for (let i = 0; i < value.length; ++i) {
+ this._collect <<= 8;
+ this._collect |= value.charCodeAt(i);
+ }
}
- public mockActiveDcsHandler(): void {
- this._activeDcsHandler = this._dcsHandlerFb;
+ public mockOscParser(): void {
+ this._oscParser = oscPutParser;
+ }
+ public identifier(id: IFunctionIdentifier): number {
+ return this._identifier(id);
}
}
@@ -76,34 +119,17 @@ const testTerminal: any = {
actionESC: function (collect: string, flag: string): void {
this.calls.push(['esc', collect, flag]);
},
- actionDCSHook: function (collect: string, params: IParams, flag: string): void {
- this.calls.push(['dcs hook', collect, params.toArray(), flag]);
+ actionDCSHook: function (params: IParams): void {
+ this.calls.push(['dcs hook', params.toArray()]);
},
- actionDCSPrint: function (data: Uint32Array, start: number, end: number): void {
- let s = '';
- for (let i = start; i < end; ++i) {
- s += stringFromCodePoint(data[i]);
- }
+ actionDCSPrint: function (s: string): void {
this.calls.push(['dcs put', s]);
},
- actionDCSUnhook: function (): void {
- this.calls.push(['dcs unhook']);
+ actionDCSUnhook: function (success: boolean): void {
+ this.calls.push(['dcs unhook', success]);
}
};
-// dcs handler to map dcs actions into the test object `testTerminal`
-class DcsTest implements IDcsHandler {
- hook(collect: string, params: IParams, flag: number): void {
- testTerminal.actionDCSHook(collect, params, String.fromCharCode(flag));
- }
- put(data: Uint32Array, start: number, end: number): void {
- testTerminal.actionDCSPrint(data, start, end);
- }
- unhook(): void {
- testTerminal.actionDCSUnhook();
- }
-}
-
const states: number[] = [
ParserState.GROUND,
ParserState.ESCAPE,
@@ -124,21 +150,35 @@ let state: any;
// parser with Uint8Array based transition table
const testParser = new TestEscapeSequenceParser();
+testParser.mockOscParser();
testParser.setPrintHandler(testTerminal.print.bind(testTerminal));
-testParser.setCsiHandlerFallback((collect: string, params: IParams, flag: number) => {
- testTerminal.actionCSI(collect, params, String.fromCharCode(flag));
+testParser.setCsiHandlerFallback((ident: number, params: IParams) => {
+ const id = testParser.identToString(ident);
+ testTerminal.actionCSI(id.slice(0, -1), params, id.slice(-1));
});
-testParser.setEscHandlerFallback((collect: string, flag: number) => {
- testTerminal.actionESC(collect, String.fromCharCode(flag));
+testParser.setEscHandlerFallback((ident: number) => {
+ const id = testParser.identToString(ident);
+ testTerminal.actionESC(id.slice(0, -1), id.slice(-1));
});
testParser.setExecuteHandlerFallback((code: number) => {
testTerminal.actionExecute(String.fromCharCode(code));
});
-testParser.setOscHandlerFallback((identifier: number, data: string) => {
+testParser.setOscHandlerFallback((identifier, action, data) => {
if (identifier === -1) testTerminal.actionOSC(data); // handle error condition silently
- else testTerminal.actionOSC('' + identifier + ';' + data);
+ else if (action === 'END') testTerminal.actionOSC('' + identifier + ';' + data); // collect only data at END
+});
+testParser.setDcsHandlerFallback((collectAndFlag, action, payload) => {
+ switch (action) {
+ case 'HOOK':
+ testTerminal.actionDCSHook(payload);
+ break;
+ case 'PUT':
+ testTerminal.actionDCSPrint(payload);
+ break;
+ case 'UNHOOK':
+ testTerminal.actionDCSUnhook(payload);
+ }
});
-testParser.setDcsHandlerFallback(new DcsTest());
// translate string based parse calls into typed array based
@@ -217,7 +257,8 @@ describe('EscapeSequenceParser', function (): void {
'\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', '\x99', '\x9a'
];
const exceptions: { [key: number]: { [key: string]: any[] } } = {
- 8: { '\x18': [], '\x1a': [] } // simply abort osc state
+ 8: { '\x18': [], '\x1a': [] }, // abort OSC_STRING
+ 13: { '\x18': [['dcs unhook', false]], '\x1a': [['dcs unhook', false]] } // abort DCS_PASSTHROUGH
};
parser.reset();
testTerminal.clear();
@@ -241,12 +282,10 @@ describe('EscapeSequenceParser', function (): void {
parser.reset();
for (state in states) {
parser.currentState = state;
- parser.osc = '#';
parser.params = [23];
parser.collect = '#';
parse(parser, '\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();
@@ -358,24 +397,20 @@ describe('EscapeSequenceParser', function (): void {
parser.reset();
// C0
parser.currentState = ParserState.ESCAPE;
- parser.osc = '#';
parser.params = [123];
parser.collect = '#';
parse(parser, '[');
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 = '#';
parse(parser, '\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();
@@ -909,7 +944,7 @@ describe('EscapeSequenceParser', function (): void {
parser.currentState = ParserState.DCS_ENTRY;
parse(parser, collect[i]);
chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH);
- testTerminal.compare([['dcs hook', '', [0], collect[i]]]);
+ testTerminal.compare([['dcs hook', [0]]]);
parser.reset();
testTerminal.clear();
}
@@ -922,7 +957,7 @@ describe('EscapeSequenceParser', function (): void {
parser.currentState = ParserState.DCS_PARAM;
parse(parser, collect[i]);
chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH);
- testTerminal.compare([['dcs hook', '', [0], collect[i]]]);
+ testTerminal.compare([['dcs hook', [0]]]);
parser.reset();
testTerminal.clear();
}
@@ -935,7 +970,7 @@ describe('EscapeSequenceParser', function (): void {
parser.currentState = ParserState.DCS_INTERMEDIATE;
parse(parser, collect[i]);
chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH);
- testTerminal.compare([['dcs hook', '', [0], collect[i]]]);
+ testTerminal.compare([['dcs hook', [0]]]);
parser.reset();
testTerminal.clear();
}
@@ -949,7 +984,6 @@ describe('EscapeSequenceParser', function (): void {
puts = puts.concat(r(0x20, 0x7f));
for (let i = 0; i < puts.length; ++i) {
parser.currentState = ParserState.DCS_PASSTHROUGH;
- parser.mockActiveDcsHandler();
parse(parser, puts[i]);
chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH);
testTerminal.compare([['dcs put', puts[i]]]);
@@ -990,33 +1024,33 @@ describe('EscapeSequenceParser', function (): void {
});
it('OSC', function (): void {
test('\x1b]0;abc123€öäü\x07', [
- ['osc', '0;abc123€öäü']
+ ['osc', '0;abc123€öäü, success: true']
], null);
});
it('single DCS', function (): void {
test('\x1bP1;2;3+$aäbc;däe\x9c', [
- ['dcs hook', '+$', [1, 2, 3], 'a'],
+ ['dcs hook', [1, 2, 3]],
['dcs put', 'äbc;däe'],
- ['dcs unhook']
+ ['dcs unhook', true]
], null);
});
it('multi DCS', function (): void {
test('\x1bP1;2;3+$abc;de', [
- ['dcs hook', '+$', [1, 2, 3], 'a'],
+ ['dcs hook', [1, 2, 3]],
['dcs put', 'bc;de']
], null);
testTerminal.clear();
test('abc\x9c', [
['dcs put', 'abc'],
- ['dcs unhook']
+ ['dcs unhook', true]
], true);
});
it('print + DCS(C1)', function (): void {
test('abc\x901;2;3+$abc;de\x9c', [
['print', 'abc'],
- ['dcs hook', '+$', [1, 2, 3], 'a'],
+ ['dcs hook', [1, 2, 3]],
['dcs put', 'bc;de'],
- ['dcs unhook']
+ ['dcs unhook', true]
], null);
});
it('print + PM(C1) + print', function (): void {
@@ -1026,9 +1060,9 @@ describe('EscapeSequenceParser', function (): void {
], null);
});
it('print + OSC(C1) + print', function (): void {
- test('abc\x9d123tzf\x9cdefg', [
+ test('abc\x9d123;tzf\x9cdefg', [
['print', 'abc'],
- ['osc', '123tzf'],
+ ['osc', '123;tzf, success: true'],
['print', 'defg']
], null);
});
@@ -1039,9 +1073,9 @@ describe('EscapeSequenceParser', function (): void {
], null);
});
it('7bit ST should be swallowed', function (): void {
- test('abc\x9d123tzf\x1b\\defg', [
+ test('abc\x9d123;tzf\x1b\\defg', [
['print', 'abc'],
- ['osc', '123tzf'],
+ ['osc', '123;tzf, success: true'],
['print', 'defg']
], null);
});
@@ -1057,9 +1091,35 @@ describe('EscapeSequenceParser', function (): void {
it('colon notation in DCS params', function (): void {
test('abc\x901;2::55;3+$abc;de\x9c', [
['print', 'abc'],
- ['dcs hook', '+$', [1, 2, [-1, 55], 3], 'a'],
+ ['dcs hook', [1, 2, [-1, 55], 3]],
['dcs put', 'bc;de'],
- ['dcs unhook']
+ ['dcs unhook', true]
+ ], null);
+ });
+ it('CAN should abort DCS', () => {
+ test('abc\x901;2::55;3+$abc;de\x18', [
+ ['print', 'abc'],
+ ['dcs hook', [1, 2, [-1, 55], 3]],
+ ['dcs put', 'bc;de'],
+ ['dcs unhook', false] // false for abort
+ ], null);
+ });
+ it('SUB should abort DCS', () => {
+ test('abc\x901;2::55;3+$abc;de\x1a', [
+ ['print', 'abc'],
+ ['dcs hook', [1, 2, [-1, 55], 3]],
+ ['dcs put', 'bc;de'],
+ ['dcs unhook', false] // false for abort
+ ], null);
+ });
+ it('CAN should abort OSC', () => {
+ test('\x1b]0;abc123€öäü\x18', [
+ ['osc', '0;abc123€öäü, success: false']
+ ], null);
+ });
+ it('SUB should abort OSC', () => {
+ test('\x1b]0;abc123€öäü\x1a', [
+ ['osc', '0;abc123€öäü, success: false']
], null);
});
});
@@ -1091,7 +1151,7 @@ describe('EscapeSequenceParser', function (): void {
parser.currentState = ParserState.DCS_PASSTHROUGH;
parse(parser, '\x901;2;3+$a€öäü');
chai.expect(parser.currentState).equal(ParserState.DCS_PASSTHROUGH);
- testTerminal.compare([['dcs hook', '+$', [1, 2, 3], 'a'], ['dcs put', '€öäü']]);
+ testTerminal.compare([['dcs hook', [1, 2, 3]], ['dcs put', '€öäü']]);
parser.reset();
testTerminal.clear();
});
@@ -1143,32 +1203,83 @@ describe('EscapeSequenceParser', function (): void {
chai.expect(print).equal('');
});
it('ESC handler', function (): void {
- parser2.setEscHandler('%G', function (): void {
+ parser2.setEscHandler({intermediates: '%', final: 'G'}, function (): void {
esc.push('%G');
});
- parser2.setEscHandler('E', function (): void {
+ parser2.setEscHandler({final: 'E'}, function (): void {
esc.push('E');
});
parse(parser2, INPUT);
chai.expect(esc).eql(['%G', 'E']);
- parser2.clearEscHandler('%G');
- parser2.clearEscHandler('%G'); // should not throw
+ parser2.clearEscHandler({intermediates: '%', final: 'G'});
+ parser2.clearEscHandler({intermediates: '%', final: 'G'}); // should not throw
clearAccu();
parse(parser2, INPUT);
chai.expect(esc).eql(['E']);
- parser2.clearEscHandler('E');
+ parser2.clearEscHandler({final: 'E'});
clearAccu();
parse(parser2, INPUT);
chai.expect(esc).eql([]);
});
+ describe('ESC custom handlers', () => {
+ it('prevent fallback', () => {
+ parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); });
+ parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
+ parse(parser2, INPUT);
+ chai.expect(esc).eql(['custom - %G']);
+ });
+ it('allow fallback', () => {
+ parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); });
+ parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return false; });
+ parse(parser2, INPUT);
+ chai.expect(esc).eql(['custom - %G', 'default - %G']);
+ });
+ it('Multiple custom handlers fallback once', () => {
+ parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); });
+ parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
+ parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return false; });
+ parse(parser2, INPUT);
+ chai.expect(esc).eql(['custom2 - %G', 'custom - %G']);
+ });
+ it('Multiple custom handlers no fallback', () => {
+ parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); });
+ parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
+ parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return true; });
+ parse(parser2, INPUT);
+ chai.expect(esc).eql(['custom2 - %G']);
+ });
+ it('Execution order should go from latest handler down to the original', () => {
+ const order: number[] = [];
+ parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { order.push(1); });
+ parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { order.push(2); return false; });
+ parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { order.push(3); return false; });
+ parse(parser2, '\x1b%G');
+ chai.expect(order).eql([3, 2, 1]);
+ });
+ it('Dispose should work', () => {
+ parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); });
+ const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
+ dispo.dispose();
+ parse(parser2, INPUT);
+ chai.expect(esc).eql(['default - %G']);
+ });
+ it('Should not corrupt the parser when dispose is called twice', () => {
+ parser2.setEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); });
+ const dispo = parser2.addEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
+ dispo.dispose();
+ dispo.dispose();
+ parse(parser2, INPUT);
+ chai.expect(esc).eql(['default - %G']);
+ });
+ });
it('CSI handler', function (): void {
- parser2.setCsiHandler('m', function (params: IParams, collect: string): void {
- csi.push(['m', params.toArray(), collect]);
+ parser2.setCsiHandler({final: 'm'}, function (params: IParams): void {
+ csi.push(['m', params.toArray(), '']);
});
parse(parser2, INPUT);
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]);
- parser2.clearCsiHandler('m');
- parser2.clearCsiHandler('m'); // should not throw
+ parser2.clearCsiHandler({final: 'm'});
+ parser2.clearCsiHandler({final: 'm'}); // should not throw
clearAccu();
parse(parser2, INPUT);
chai.expect(csi).eql([]);
@@ -1176,16 +1287,16 @@ describe('EscapeSequenceParser', function (): void {
describe('CSI custom handlers', () => {
it('Prevent fallback', () => {
const csiCustom: [string, ParamsArray, string][] = [];
- parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect]));
- parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; });
+ parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); });
+ parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parse(parser2, INPUT);
chai.expect(csi).eql([], 'Should not fallback to original handler');
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
});
it('Allow fallback', () => {
const csiCustom: [string, ParamsArray, string][] = [];
- parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect]));
- parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return false; });
+ parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); });
+ parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return false; });
parse(parser2, INPUT);
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler');
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
@@ -1193,9 +1304,9 @@ describe('EscapeSequenceParser', function (): void {
it('Multiple custom handlers fallback once', () => {
const csiCustom: [string, ParamsArray, string][] = [];
const csiCustom2: [string, ParamsArray, string][] = [];
- parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect]));
- parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; });
- parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params.toArray(), collect]); return false; });
+ parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); });
+ parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
+ parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return false; });
parse(parser2, INPUT);
chai.expect(csi).eql([], 'Should not fallback to original handler');
chai.expect(csiCustom).eql([['m', [1, 31], ''], ['m', [0], '']]);
@@ -1204,9 +1315,9 @@ describe('EscapeSequenceParser', function (): void {
it('Multiple custom handlers no fallback', () => {
const csiCustom: [string, ParamsArray, string][] = [];
const csiCustom2: [string, ParamsArray, string][] = [];
- parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect]));
- parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; });
- parser2.addCsiHandler('m', (params, collect) => { csiCustom2.push(['m', params.toArray(), collect]); return true; });
+ parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); });
+ parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
+ parser2.addCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return true; });
parse(parser2, INPUT);
chai.expect(csi).eql([], 'Should not fallback to original handler');
chai.expect(csiCustom).eql([], 'Should not fallback once');
@@ -1214,16 +1325,16 @@ describe('EscapeSequenceParser', function (): void {
});
it('Execution order should go from latest handler down to the original', () => {
const order: number[] = [];
- parser2.setCsiHandler('m', () => order.push(1));
- parser2.addCsiHandler('m', () => { order.push(2); return false; });
- parser2.addCsiHandler('m', () => { order.push(3); return false; });
+ parser2.setCsiHandler({final: 'm'}, () => { order.push(1); });
+ parser2.addCsiHandler({final: 'm'}, () => { order.push(2); return false; });
+ parser2.addCsiHandler({final: 'm'}, () => { order.push(3); return false; });
parse(parser2, '\x1b[0m');
chai.expect(order).eql([3, 2, 1]);
});
it('Dispose should work', () => {
const csiCustom: [string, ParamsArray, string][] = [];
- parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect]));
- const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; });
+ parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); });
+ const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
customHandler.dispose();
parse(parser2, INPUT);
chai.expect(csi).eql([['m', [1, 31], ''], ['m', [0], '']]);
@@ -1231,8 +1342,8 @@ describe('EscapeSequenceParser', function (): void {
});
it('Should not corrupt the parser when dispose is called twice', () => {
const csiCustom: [string, ParamsArray, string][] = [];
- parser2.setCsiHandler('m', (params, collect) => csi.push(['m', params.toArray(), collect]));
- const customHandler = parser2.addCsiHandler('m', (params, collect) => { csiCustom.push(['m', params.toArray(), collect]); return true; });
+ parser2.setCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); });
+ const customHandler = parser2.addCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
customHandler.dispose();
customHandler.dispose();
parse(parser2, INPUT);
@@ -1256,9 +1367,9 @@ describe('EscapeSequenceParser', function (): void {
chai.expect(exe).eql(['\n']);
});
it('OSC handler', function (): void {
- parser2.setOscHandler(1, function (data: string): void {
+ parser2.setOscHandler(1, new OscHandler(function (data: string): void {
osc.push([1, data]);
- });
+ }));
parse(parser2, INPUT);
chai.expect(osc).eql([[1, 'foo=bar']]);
parser2.clearOscHandler(1);
@@ -1270,16 +1381,16 @@ describe('EscapeSequenceParser', function (): void {
describe('OSC custom handlers', () => {
it('Prevent fallback', () => {
const oscCustom: [number, string][] = [];
- parser2.setOscHandler(1, data => osc.push([1, data]));
- parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
+ parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data])));
+ parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; }));
parse(parser2, INPUT);
chai.expect(osc).eql([], 'Should not fallback to original handler');
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
});
it('Allow fallback', () => {
const oscCustom: [number, string][] = [];
- parser2.setOscHandler(1, data => osc.push([1, data]));
- parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return false; });
+ parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data])));
+ parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return false; }));
parse(parser2, INPUT);
chai.expect(osc).eql([[1, 'foo=bar']], 'Should fallback to original handler');
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
@@ -1287,9 +1398,9 @@ describe('EscapeSequenceParser', function (): void {
it('Multiple custom handlers fallback once', () => {
const oscCustom: [number, string][] = [];
const oscCustom2: [number, string][] = [];
- parser2.setOscHandler(1, data => osc.push([1, data]));
- parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
- parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return false; });
+ parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data])));
+ parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; }));
+ parser2.addOscHandler(1, new OscHandler(data => { oscCustom2.push([1, data]); return false; }));
parse(parser2, INPUT);
chai.expect(osc).eql([], 'Should not fallback to original handler');
chai.expect(oscCustom).eql([[1, 'foo=bar']]);
@@ -1298,9 +1409,9 @@ describe('EscapeSequenceParser', function (): void {
it('Multiple custom handlers no fallback', () => {
const oscCustom: [number, string][] = [];
const oscCustom2: [number, string][] = [];
- parser2.setOscHandler(1, data => osc.push([1, data]));
- parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
- parser2.addOscHandler(1, data => { oscCustom2.push([1, data]); return true; });
+ parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data])));
+ parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; }));
+ parser2.addOscHandler(1, new OscHandler(data => { oscCustom2.push([1, data]); return true; }));
parse(parser2, INPUT);
chai.expect(osc).eql([], 'Should not fallback to original handler');
chai.expect(oscCustom).eql([], 'Should not fallback once');
@@ -1308,16 +1419,16 @@ describe('EscapeSequenceParser', function (): void {
});
it('Execution order should go from latest handler down to the original', () => {
const order: number[] = [];
- parser2.setOscHandler(1, () => order.push(1));
- parser2.addOscHandler(1, () => { order.push(2); return false; });
- parser2.addOscHandler(1, () => { order.push(3); return false; });
+ parser2.setOscHandler(1, new OscHandler(() => order.push(1)));
+ parser2.addOscHandler(1, new OscHandler(() => { order.push(2); return false; }));
+ parser2.addOscHandler(1, new OscHandler(() => { order.push(3); return false; }));
parse(parser2, '\x1b]1;foo=bar\x1b\\');
chai.expect(order).eql([3, 2, 1]);
});
it('Dispose should work', () => {
const oscCustom: [number, string][] = [];
- parser2.setOscHandler(1, data => osc.push([1, data]));
- const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
+ parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data])));
+ const customHandler = parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; }));
customHandler.dispose();
parse(parser2, INPUT);
chai.expect(osc).eql([[1, 'foo=bar']]);
@@ -1325,8 +1436,8 @@ describe('EscapeSequenceParser', function (): void {
});
it('Should not corrupt the parser when dispose is called twice', () => {
const oscCustom: [number, string][] = [];
- parser2.setOscHandler(1, data => osc.push([1, data]));
- const customHandler = parser2.addOscHandler(1, data => { oscCustom.push([1, data]); return true; });
+ parser2.setOscHandler(1, new OscHandler(data => osc.push([1, data])));
+ const customHandler = parser2.addOscHandler(1, new OscHandler(data => { oscCustom.push([1, data]); return true; }));
customHandler.dispose();
customHandler.dispose();
parse(parser2, INPUT);
@@ -1335,9 +1446,9 @@ describe('EscapeSequenceParser', function (): void {
});
});
it('DCS handler', function (): void {
- parser2.setDcsHandler('+p', {
- hook: function (collect: string, params: IParams, flag: number): void {
- dcs.push(['hook', collect, params.toArray(), flag]);
+ parser2.setDcsHandler({intermediates: '+', final: 'p'}, {
+ hook: function (params: IParams): void {
+ dcs.push(['hook', '', params.toArray(), 0]);
},
put: function (data: Uint32Array, start: number, end: number): void {
let s = '';
@@ -1353,17 +1464,75 @@ describe('EscapeSequenceParser', function (): void {
parse(parser2, '\x1bP1;2;3+pabc');
parse(parser2, ';de\x9c');
chai.expect(dcs).eql([
- ['hook', '+', [1, 2, 3], 'p'.charCodeAt(0)],
+ ['hook', '', [1, 2, 3], 0],
['put', 'abc'], ['put', ';de'],
['unhook']
]);
- parser2.clearDcsHandler('+p');
- parser2.clearDcsHandler('+p'); // should not throw
+ parser2.clearDcsHandler({intermediates: '+', final: 'p'});
+ parser2.clearDcsHandler({intermediates: '+', final: 'p'}); // should not throw
clearAccu();
parse(parser2, '\x1bP1;2;3+pabc');
parse(parser2, ';de\x9c');
chai.expect(dcs).eql([]);
});
+ describe('DCS custom handlers', () => {
+ const DCS_INPUT = '\x1bP1;2;3+pabc\x1b\\';
+ it('Prevent fallback', () => {
+ const dcsCustom: [string, (number | number[])[], string][] = [];
+ parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data])));
+ parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
+ parse(parser2, DCS_INPUT);
+ chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc']]);
+ });
+ it('Allow fallback', () => {
+ const dcsCustom: [string, (number | number[])[], string][] = [];
+ parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data])));
+ parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; }));
+ parse(parser2, DCS_INPUT);
+ chai.expect(dcsCustom).eql([['B', [1, 2, 3], 'abc'], ['A', [1, 2, 3], 'abc']]);
+ });
+ it('Multiple custom handlers fallback once', () => {
+ const dcsCustom: [string, (number | number[])[], string][] = [];
+ parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data])));
+ parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
+ parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; }));
+ parse(parser2, DCS_INPUT);
+ chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc'], ['B', [1, 2, 3], 'abc']]);
+ });
+ it('Multiple custom handlers no fallback', () => {
+ const dcsCustom: [string, (number | number[])[], string][] = [];
+ parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data])));
+ parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
+ parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; }));
+ parse(parser2, DCS_INPUT);
+ chai.expect(dcsCustom).eql([['C', [1, 2, 3], 'abc']]);
+ });
+ it('Execution order should go from latest handler down to the original', () => {
+ const order: number[] = [];
+ parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => order.push(1)));
+ parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(2); return false; }));
+ parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(3); return false; }));
+ parse(parser2, DCS_INPUT);
+ chai.expect(order).eql([3, 2, 1]);
+ });
+ it('Dispose should work', () => {
+ const dcsCustom: [string, (number | number[])[], string][] = [];
+ parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data])));
+ const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
+ dispo.dispose();
+ parse(parser2, DCS_INPUT);
+ chai.expect(dcsCustom).eql([['A', [1, 2, 3], 'abc']]);
+ });
+ it('Should not corrupt the parser when dispose is called twice', () => {
+ const dcsCustom: [string, (number | number[])[], string][] = [];
+ parser2.setDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => dcsCustom.push(['A', params.toArray(), data])));
+ const dispo = parser2.addDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
+ dispo.dispose();
+ dispo.dispose();
+ parse(parser2, DCS_INPUT);
+ chai.expect(dcsCustom).eql([['A', [1, 2, 3], 'abc']]);
+ });
+ });
it('ERROR handler', function (): void {
let errorState: IParsingState | null = null;
parser2.setErrorHandler(function (state: IParsingState): IParsingState {
@@ -1375,8 +1544,7 @@ describe('EscapeSequenceParser', function (): void {
position: 6,
code: '€'.charCodeAt(0),
currentState: ParserState.CSI_PARAM,
- osc: '',
- collect: '',
+ collect: 0,
params: Params.fromArray([1, 2, 0]), // extra zero here
abort: false
});
@@ -1387,5 +1555,110 @@ describe('EscapeSequenceParser', function (): void {
chai.expect(errorState).eql(null);
});
});
+ describe('function identifiers', () => {
+ describe('registration limits', () => {
+ it('prefix range 0x3c .. 0x3f, one byte', () => {
+ for (let i = 0x3c; i <= 0x3f; ++i) {
+ const c = String.fromCharCode(i);
+ chai.expect(parser.identToString(parser.identifier({prefix: c, final: 'z'}))).eql(c + 'z');
+ }
+ chai.assert.throws(() => { parser.identifier({prefix: '\x3b', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f');
+ chai.assert.throws(() => { parser.identifier({prefix: '\x40', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f');
+ chai.assert.throws(() => { parser.identifier({prefix: '??', final: 'z'}); }, 'only one byte as prefix supported');
+ });
+ it('intermediates range 0x20 .. 0x2f, up to two bytes', () => {
+ for (let i = 0x20; i <= 0x2f; ++i) {
+ const c = String.fromCharCode(i);
+ chai.expect(parser.identToString(parser.identifier({intermediates: c + c, final: 'z'}))).eql(c + c + 'z');
+ }
+ chai.assert.throws(() => { parser.identifier({intermediates: '\x1f', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f');
+ chai.assert.throws(() => { parser.identifier({intermediates: '\x30', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f');
+ chai.assert.throws(() => { parser.identifier({intermediates: '!!!', final: 'z'}); }, 'only two bytes as intermediates are supported');
+ });
+ it('final CSI/DCS range 0x40 .. 0x7e (default), one byte', () => {
+ for (let i = 0x40; i <= 0x7e; ++i) {
+ const c = String.fromCharCode(i);
+ chai.expect(parser.identToString(parser.identifier({final: c}))).eql(c);
+ }
+ chai.assert.throws(() => { parser.identifier({final: '\x3f'}); }, 'final must be in range 64 .. 126');
+ chai.assert.throws(() => { parser.identifier({final: '\x7f'}); }, 'final must be in range 64 .. 126');
+ chai.assert.throws(() => { parser.identifier({final: 'zz'}); }, 'final must be a single byte');
+ });
+ it('final ESC range 0x30 .. 0x7e, one byte', () => {
+ for (let i = 0x30; i <= 0x7e; ++i) {
+ const c = String.fromCharCode(i);
+ let handler: IDisposable | undefined;
+ chai.assert.doesNotThrow(() => { handler = parser.addEscHandler({final: c}, () => {}); }, 'final must be in range 48 .. 126');
+ if (handler) handler.dispose();
+ }
+ chai.assert.throws(() => { parser.addEscHandler({final: '\x2f'}, () => {}); }, 'final must be in range 48 .. 126');
+ chai.assert.throws(() => { parser.addEscHandler({final: '\x7f'}, () => {}); }, 'final must be in range 48 .. 126');
+ });
+ it('id calculation - should stacking prefix -> intermediate -> final', () => {
+ chai.expect(parser.identToString(parser.identifier({final: 'z'}))).eql('z');
+ chai.expect(parser.identToString(parser.identifier({prefix: '?', final: 'z'}))).eql('?z');
+ chai.expect(parser.identToString(parser.identifier({intermediates: '!', final: 'z'}))).eql('!z');
+ chai.expect(parser.identToString(parser.identifier({prefix: '?', intermediates: '!', final: 'z'}))).eql('?!z');
+ chai.expect(parser.identToString(parser.identifier({prefix: '?', intermediates: '!!', final: 'z'}))).eql('?!!z');
+ });
+ });
+ describe('identifier invocation', () => {
+ it('ESC', () => {
+ const callstack: string[] = [];
+ const h1 = parser.addEscHandler({final: 'z'}, () => { callstack.push('z'); });
+ const h2 = parser.addEscHandler({intermediates: '!', final: 'z'}, () => { callstack.push('!z'); });
+ const h3 = parser.addEscHandler({intermediates: '!!', final: 'z'}, () => { callstack.push('!!z'); });
+ parse(parser, '\x1bz\x1b!z\x1b!!z');
+ h1.dispose();
+ h2.dispose();
+ h3.dispose();
+ parse(parser, '\x1bz\x1b!z\x1b!!z');
+ chai.expect(callstack).eql(['z', '!z', '!!z']);
+ });
+ it('CSI', () => {
+ const callstack: any[] = [];
+ const h1 = parser.addCsiHandler({final: 'z'}, params => { callstack.push(['z', params.toArray()]); });
+ const h2 = parser.addCsiHandler({intermediates: '!', final: 'z'}, params => { callstack.push(['!z', params.toArray()]); });
+ const h3 = parser.addCsiHandler({intermediates: '!!', final: 'z'}, params => { callstack.push(['!!z', params.toArray()]); });
+ const h4 = parser.addCsiHandler({prefix: '?', final: 'z'}, params => { callstack.push(['?z', params.toArray()]); });
+ const h5 = parser.addCsiHandler({prefix: '?', intermediates: '!', final: 'z'}, params => { callstack.push(['?!z', params.toArray()]); });
+ const h6 = parser.addCsiHandler({prefix: '?', intermediates: '!!', final: 'z'}, params => { callstack.push(['?!!z', params.toArray()]); });
+ parse(parser, '\x1b[1;z\x1b[1;!z\x1b[1;!!z\x1b[?1;z\x1b[?1;!z\x1b[?1;!!z');
+ h1.dispose();
+ h2.dispose();
+ h3.dispose();
+ h4.dispose();
+ h5.dispose();
+ h6.dispose();
+ parse(parser, '\x1b[1;z\x1b[1;!z\x1b[1;!!z\x1b[?1;z\x1b[?1;!z\x1b[?1;!!z');
+ chai.expect(callstack).eql([['z', [1, 0]], ['!z', [1, 0]], ['!!z', [1, 0]], ['?z', [1, 0]], ['?!z', [1, 0]], ['?!!z', [1, 0]]]);
+ });
+ it('DCS', () => {
+ const callstack: any[] = [];
+ const h1 = parser.addDcsHandler({final: 'z'}, new DcsHandler((data, params) => { callstack.push(['z', params.toArray(), data]); }));
+ const h2 = parser.addDcsHandler({intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!z', params.toArray(), data]); }));
+ const h3 = parser.addDcsHandler({intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!!z', params.toArray(), data]); }));
+ const h4 = parser.addDcsHandler({prefix: '?', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?z', params.toArray(), data]); }));
+ const h5 = parser.addDcsHandler({prefix: '?', intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!z', params.toArray(), data]); }));
+ const h6 = parser.addDcsHandler({prefix: '?', intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!!z', params.toArray(), data]); }));
+ parse(parser, '\x1bP1;zAB\x1b\\\x1bP1;!zAB\x1b\\\x1bP1;!!zAB\x1b\\\x1bP?1;zAB\x1b\\\x1bP?1;!zAB\x1b\\\x1bP?1;!!zAB\x1b\\');
+ h1.dispose();
+ h2.dispose();
+ h3.dispose();
+ h4.dispose();
+ h5.dispose();
+ h6.dispose();
+ parse(parser, '\x1bP1;zAB\x1b\\\x1bP1;!zAB\x1b\\\x1bP1;!!zAB\x1b\\\x1bP?1;zAB\x1b\\\x1bP?1;!zAB\x1b\\\x1bP?1;!!zAB\x1b\\');
+ chai.expect(callstack).eql([
+ ['z', [1, 0], 'AB'],
+ ['!z', [1, 0], 'AB'],
+ ['!!z', [1, 0], 'AB'],
+ ['?z', [1, 0], 'AB'],
+ ['?!z', [1, 0], 'AB'],
+ ['?!!z', [1, 0], 'AB']
+ ]);
+ });
+ });
+ });
// TODO: error conditions and error recovery (not implemented yet in parser)
});
diff --git a/src/common/parser/EscapeSequenceParser.ts b/src/common/parser/EscapeSequenceParser.ts
index 1d2b7e1e..bc0b620e 100644
--- a/src/common/parser/EscapeSequenceParser.ts
+++ b/src/common/parser/EscapeSequenceParser.ts
@@ -3,20 +3,14 @@
* @license MIT
*/
-import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams } from 'common/parser/Types';
+import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType } from 'common/parser/Types';
import { ParserState, ParserAction } from 'common/parser/Constants';
import { Disposable } from 'common/Lifecycle';
-import { utf32ToString } from 'common/input/TextDecoder';
import { IDisposable } from 'common/Types';
import { fill } from 'common/TypedArrayUtils';
import { Params } from 'common/parser/Params';
-
-interface IHandlerCollection {
- [key: string]: T[];
-}
-
-type CsiHandler = (params: IParams, collect: string) => boolean | void;
-type OscHandler = (data: string) => boolean | void;
+import { OscParser } from 'common/parser/OscParser';
+import { DcsParser } from 'common/parser/DcsParser';
/**
* Table values are generated like this:
@@ -193,7 +187,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
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);
+ table.addMany([0x1b, 0x9c, 0x18, 0x1a], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);
// special handling of unicode chars
table.add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);
table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
@@ -203,14 +197,6 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
return table;
})();
-/**
- * Dummy DCS handler as default fallback.
- */
-class DcsDummy implements IDcsHandler {
- hook(collect: string, params: IParams, flag: number): void { }
- put(data: Uint32Array, start: number, end: number): void { }
- unhook(): void { }
-}
/**
* EscapeSequenceParser.
@@ -220,17 +206,25 @@ class DcsDummy implements IDcsHandler {
* 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
+ * the optional `transitions` constructor argument and
* reimplement the `parse` method.
*
* This parser is currently hardcoded to operate in ZDM (Zero Default Mode)
* as suggested by the original parser, thus empty parameters are set to 0.
- * This this is not in line with the latest ECMA specification
+ * This this is not in line with the latest ECMA-48 specification
* (ZDM was part of the early specs and got completely removed later on).
*
* Other than the original parser from vt100.net this parser supports
* sub parameters in digital parameters separated by colons. Empty sub parameters
- * are set to -1.
+ * are set to -1 (no ZDM for sub parameters).
+ *
+ * About prefix and intermediate bytes:
+ * This parser follows the assumptions of the vt100.net parser with these restrictions:
+ * - only one prefix byte is allowed as first parameter byte, byte range 0x3c .. 0x3f
+ * - max. two intermediates are respected, byte range 0x20 .. 0x2f
+ * Note that this is not in line with ECMA-48 which does not limit either of those.
+ * Furthermore ECMA-48 allows the prefix byte range at any param byte position. Currently
+ * there are no known sequences that follow the broader definition of the specification.
*
* TODO: implement error recovery hook via error handler return values
*/
@@ -240,27 +234,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
public precedingCodepoint: number;
// buffers over several parse calls
- protected _osc: string;
protected _params: Params;
- protected _collect: string;
+ protected _collect: number;
// handler lookup containers
- protected _printHandler: (data: Uint32Array, start: number, end: number) => void;
- protected _executeHandlers: any;
- protected _csiHandlers: IHandlerCollection;
- protected _escHandlers: any;
- protected _oscHandlers: IHandlerCollection;
- protected _dcsHandlers: any;
- protected _activeDcsHandler: IDcsHandler | null;
+ protected _printHandler: PrintHandlerType;
+ protected _executeHandlers: {[flag: number]: ExecuteHandlerType};
+ protected _csiHandlers: IHandlerCollection;
+ protected _escHandlers: IHandlerCollection;
+ protected _oscParser: IOscParser;
+ protected _dcsParser: IDcsParser;
protected _errorHandler: (state: IParsingState) => IParsingState;
// fallback handlers
- protected _printHandlerFb: (data: Uint32Array, start: number, end: number) => void;
- protected _executeHandlerFb: (code: number) => void;
- protected _csiHandlerFb: (collect: string, params: IParams, flag: number) => void;
- protected _escHandlerFb: (collect: string, flag: number) => void;
- protected _oscHandlerFb: (identifier: number, data: string) => void;
- protected _dcsHandlerFb: IDcsHandler;
+ protected _printHandlerFb: PrintFallbackHandlerType;
+ protected _executeHandlerFb: ExecuteFallbackHandlerType;
+ protected _csiHandlerFb: CsiFallbackHandlerType;
+ protected _escHandlerFb: EscFallbackHandlerType;
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) {
@@ -268,145 +258,197 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this.initialState = ParserState.GROUND;
this.currentState = this.initialState;
- this._osc = '';
this._params = new Params(); // defaults to 32 storable params/subparams
this._params.addParam(0); // ZDM
- this._collect = '';
+ this._collect = 0;
this.precedingCodepoint = 0;
// set default fallback handlers and handler lookup containers
this._printHandlerFb = (data, start, end): void => { };
this._executeHandlerFb = (code: number): void => { };
- this._csiHandlerFb = (collect: string, params: IParams, flag: number): void => { };
- this._escHandlerFb = (collect: string, flag: number): void => { };
- this._oscHandlerFb = (identifier: number, data: string): void => { };
- this._dcsHandlerFb = new DcsDummy();
+ this._csiHandlerFb = (ident: number, params: IParams): void => { };
+ this._escHandlerFb = (ident: number): void => { };
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._oscParser = new OscParser();
+ this._dcsParser = new DcsParser();
this._errorHandler = this._errorHandlerFb;
// swallow 7bit ST (ESC+\)
- this.setEscHandler('\\', () => {});
+ this.setEscHandler({final: '\\'}, () => {});
+ }
+
+ protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number {
+ let res = 0;
+ if (id.prefix) {
+ if (id.prefix.length > 1) {
+ throw new Error('only one byte as prefix supported');
+ }
+ res = id.prefix.charCodeAt(0);
+ if (res && 0x3c > res || res > 0x3f) {
+ throw new Error('prefix must be in range 0x3c .. 0x3f');
+ }
+ }
+ if (id.intermediates) {
+ if (id.intermediates.length > 2) {
+ throw new Error('only two bytes as intermediates are supported');
+ }
+ for (let i = 0; i < id.intermediates.length; ++i) {
+ const intermediate = id.intermediates.charCodeAt(i);
+ if (0x20 > intermediate || intermediate > 0x2f) {
+ throw new Error('intermediate must be in range 0x20 .. 0x2f');
+ }
+ res <<= 8;
+ res |= intermediate;
+ }
+ }
+ if (id.final.length !== 1) {
+ throw new Error('final must be a single byte');
+ }
+ const finalCode = id.final.charCodeAt(0);
+ if (finalRange[0] > finalCode || finalCode > finalRange[1]) {
+ throw new Error(`final must be in range ${finalRange[0]} .. ${finalRange[1]}`);
+ }
+ res <<= 8;
+ res |= finalCode;
+
+ return res;
+ }
+
+ public identToString(ident: number): string {
+ const res: string[] = [];
+ while (ident) {
+ res.push(String.fromCharCode(ident & 0xFF));
+ ident >>= 8;
+ }
+ return res.reverse().join('');
}
public dispose(): void {
- this._executeHandlers = null;
- this._escHandlers = null;
- this._dcsHandlers = null;
- this._activeDcsHandler = null;
+ this._csiHandlers = Object.create(null);
+ this._executeHandlers = Object.create(null);
+ this._escHandlers = Object.create(null);
+ this._oscParser.dispose();
+ this._dcsParser.dispose();
}
- setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void {
- this._printHandler = callback;
+ public setPrintHandler(handler: PrintHandlerType): void {
+ this._printHandler = handler;
}
- clearPrintHandler(): void {
+ public 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;
- }
-
- addCsiHandler(flag: string, callback: CsiHandler): IDisposable {
- const index = flag.charCodeAt(0);
- if (this._csiHandlers[index] === undefined) {
- this._csiHandlers[index] = [];
+ public addEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable {
+ const ident = this._identifier(id, [0x30, 0x7e]);
+ if (this._escHandlers[ident] === undefined) {
+ this._escHandlers[ident] = [];
}
- const handlerList = this._csiHandlers[index];
- handlerList.push(callback);
+ const handlerList = this._escHandlers[ident];
+ handlerList.push(handler);
return {
dispose: () => {
- const handlerIndex = handlerList.indexOf(callback);
+ const handlerIndex = handlerList.indexOf(handler);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
- setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void {
- this._csiHandlers[flag.charCodeAt(0)] = [callback];
+ public setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void {
+ this._escHandlers[this._identifier(id, [0x30, 0x7e])] = [handler];
}
- clearCsiHandler(flag: string): void {
- if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)];
+ public clearEscHandler(id: IFunctionIdentifier): void {
+ if (this._escHandlers[this._identifier(id, [0x30, 0x7e])]) delete this._escHandlers[this._identifier(id, [0x30, 0x7e])];
}
- setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void {
+ public setEscHandlerFallback(handler: EscFallbackHandlerType): void {
+ this._escHandlerFb = handler;
+ }
+
+ public setExecuteHandler(flag: string, handler: ExecuteHandlerType): void {
+ this._executeHandlers[flag.charCodeAt(0)] = handler;
+ }
+ public clearExecuteHandler(flag: string): void {
+ if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)];
+ }
+ public setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void {
+ this._executeHandlerFb = handler;
+ }
+
+ public addCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable {
+ const ident = this._identifier(id);
+ if (this._csiHandlers[ident] === undefined) {
+ this._csiHandlers[ident] = [];
+ }
+ const handlerList = this._csiHandlers[ident];
+ handlerList.push(handler);
+ return {
+ dispose: () => {
+ const handlerIndex = handlerList.indexOf(handler);
+ if (handlerIndex !== -1) {
+ handlerList.splice(handlerIndex, 1);
+ }
+ }
+ };
+ }
+ public setCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): void {
+ this._csiHandlers[this._identifier(id)] = [handler];
+ }
+ public clearCsiHandler(id: IFunctionIdentifier): void {
+ if (this._csiHandlers[this._identifier(id)]) delete this._csiHandlers[this._identifier(id)];
+ }
+ public setCsiHandlerFallback(callback: (ident: number, params: IParams) => void): void {
this._csiHandlerFb = callback;
}
- setEscHandler(collectAndFlag: string, callback: () => void): void {
- this._escHandlers[collectAndFlag] = callback;
+ public addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable {
+ return this._dcsParser.addHandler(this._identifier(id), handler);
}
- clearEscHandler(collectAndFlag: string): void {
- if (this._escHandlers[collectAndFlag]) delete this._escHandlers[collectAndFlag];
+ public setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void {
+ this._dcsParser.setHandler(this._identifier(id), handler);
}
- setEscHandlerFallback(callback: (collect: string, flag: number) => void): void {
- this._escHandlerFb = callback;
+ public clearDcsHandler(id: IFunctionIdentifier): void {
+ this._dcsParser.clearHandler(this._identifier(id));
+ }
+ public setDcsHandlerFallback(handler: DcsFallbackHandlerType): void {
+ this._dcsParser.setHandlerFallback(handler);
}
- addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
- if (this._oscHandlers[ident] === undefined) {
- this._oscHandlers[ident] = [];
- }
- const handlerList = this._oscHandlers[ident];
- handlerList.push(callback);
- return {
- dispose: () => {
- const handlerIndex = handlerList.indexOf(callback);
- if (handlerIndex !== -1) {
- handlerList.splice(handlerIndex, 1);
- }
- }
- };
+ public addOscHandler(ident: number, handler: IOscHandler): IDisposable {
+ return this._oscParser.addHandler(ident, handler);
}
- setOscHandler(ident: number, callback: (data: string) => void): void {
- this._oscHandlers[ident] = [callback];
+ public setOscHandler(ident: number, handler: IOscHandler): void {
+ this._oscParser.setHandler(ident, handler);
}
- clearOscHandler(ident: number): void {
- if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
+ public clearOscHandler(ident: number): void {
+ this._oscParser.clearHandler(ident);
}
- setOscHandlerFallback(callback: (identifier: number, data: string) => void): void {
- this._oscHandlerFb = callback;
+ public setOscHandlerFallback(handler: OscFallbackHandlerType): void {
+ this._oscParser.setHandlerFallback(handler);
}
- 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 {
+ public setErrorHandler(callback: (state: IParsingState) => IParsingState): void {
this._errorHandler = callback;
}
- clearErrorHandler(): void {
+ public clearErrorHandler(): void {
this._errorHandler = this._errorHandlerFb;
}
- reset(): void {
+ public reset(): void {
this.currentState = this.initialState;
- this._osc = '';
+ this._oscParser.reset();
+ this._dcsParser.reset();
this._params.reset();
this._params.addParam(0); // ZDM
- this._collect = '';
- this._activeDcsHandler = null;
+ this._collect = 0;
this.precedingCodepoint = 0;
}
+
+
/**
* Parse UTF32 codepoints in `data` up to `length`.
*
@@ -421,16 +463,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
* - OSC_STRING:OSC_PUT
* - DCS_PASSTHROUGH:DCS_PUT
*/
- parse(data: Uint32Array, length: number): void {
+ public parse(data: Uint32Array, length: number): void {
let code = 0;
let transition = 0;
let currentState = this.currentState;
- let osc = this._osc;
+ const osc = this._oscParser;
+ const dcs = this._dcsParser;
let collect = this._collect;
const params = this._params;
const table: Uint8Array = this.TRANSITIONS.table;
- let dcsHandler: IDcsHandler | null = this._activeDcsHandler;
- let callback: Function | null = null;
// process input string
for (let i = 0; i < length; ++i) {
@@ -466,8 +507,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
}
break;
case ParserAction.EXECUTE:
- callback = this._executeHandlers[code];
- if (callback) callback();
+ if (this._executeHandlers[code]) this._executeHandlers[code]();
else this._executeHandlerFb(code);
this.precedingCodepoint = 0;
break;
@@ -479,7 +519,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
position: i,
code,
currentState,
- osc,
collect,
params,
abort: false
@@ -489,142 +528,107 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
break;
case ParserAction.CSI_DISPATCH:
// Trigger CSI Handler
- const handlers = this._csiHandlers[code];
+ const handlers = this._csiHandlers[collect << 8 | code];
let j = handlers ? handlers.length - 1 : -1;
for (; j >= 0; j--) {
// undefined or true means success and to stop bubbling
- if (handlers[j](params, collect) !== false) {
+ if (handlers[j](params) !== false) {
break;
}
}
if (j < 0) {
- this._csiHandlerFb(collect, params, code);
+ this._csiHandlerFb(collect << 8 | code, params);
}
this.precedingCodepoint = 0;
break;
case ParserAction.PARAM:
// inner loop: digits (0x30 - 0x39) and ; (0x3b) and : (0x3a)
- let isSub = false;
do {
switch (code) {
case 0x3b:
params.addParam(0); // ZDM
- isSub = false;
break;
case 0x3a:
params.addSubParam(-1);
- isSub = true;
break;
default: // 0x30 - 0x39
- if (isSub) params.addSubParamDigit(code - 48);
- else params.addParamDigit(code - 48);
+ params.addDigit(code - 48);
}
} while (++i < length && (code = data[i]) > 0x2f && code < 0x3c);
i--;
break;
case ParserAction.COLLECT:
- collect += String.fromCharCode(code);
+ collect <<= 8;
+ collect |= code;
break;
case ParserAction.ESC_DISPATCH:
- callback = this._escHandlers[collect + String.fromCharCode(code)];
- if (callback) callback(collect, code);
- else this._escHandlerFb(collect, code);
+ const handlersEsc = this._escHandlers[collect << 8 | code];
+ let jj = handlersEsc ? handlersEsc.length - 1 : -1;
+ for (; jj >= 0; jj--) {
+ // undefined or true means success and to stop bubbling
+ if (handlersEsc[jj]() !== false) {
+ break;
+ }
+ }
+ if (jj < 0) {
+ this._escHandlerFb(collect << 8 | code);
+ }
this.precedingCodepoint = 0;
break;
case ParserAction.CLEAR:
- osc = '';
params.reset();
params.addParam(0); // ZDM
- collect = '';
+ collect = 0;
break;
case ParserAction.DCS_HOOK:
- dcsHandler = this._dcsHandlers[collect + String.fromCharCode(code)];
- if (!dcsHandler) dcsHandler = this._dcsHandlerFb;
- dcsHandler.hook(collect, params, code);
+ dcs.hook(collect << 8 | code, params);
break;
case ParserAction.DCS_PUT:
// inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f
- // unhook triggered by: 0x1b, 0x9c
+ // unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort)
for (let j = i + 1; ; ++j) {
if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
- if (dcsHandler) {
- dcsHandler.put(data, i, j);
- }
+ dcs.put(data, i, j);
i = j - 1;
break;
}
}
break;
case ParserAction.DCS_UNHOOK:
- if (dcsHandler) {
- dcsHandler.unhook();
- dcsHandler = null;
- }
+ dcs.unhook(code !== 0x18 && code !== 0x1a);
if (code === 0x1b) transition |= ParserState.ESCAPE;
- osc = '';
params.reset();
params.addParam(0); // ZDM
- collect = '';
+ collect = 0;
this.precedingCodepoint = 0;
break;
case ParserAction.OSC_START:
- osc = '';
+ osc.start();
break;
case ParserAction.OSC_PUT:
// inner loop: 0x20 (SP) included, 0x7F (DEL) included
for (let j = i + 1; ; j++) {
if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code <= 0x9f)) {
- osc += utf32ToString(data, i, j);
+ osc.put(data, i, j);
i = j - 1;
break;
}
}
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);
- // Trigger OSC Handler
- const handlers = this._oscHandlers[identifier];
- let j = handlers ? handlers.length - 1 : -1;
- for (; j >= 0; j--) {
- // undefined or true means success and to stop bubbling
- if (handlers[j](content) !== false) {
- break;
- }
- }
- if (j < 0) {
- this._oscHandlerFb(identifier, content);
- }
- }
- }
+ osc.end(code !== 0x18 && code !== 0x1a);
if (code === 0x1b) transition |= ParserState.ESCAPE;
- osc = '';
params.reset();
params.addParam(0); // ZDM
- collect = '';
+ collect = 0;
this.precedingCodepoint = 0;
break;
}
currentState = transition & TableAccess.TRANSITION_STATE_MASK;
}
- // save non pushable buffers
- this._osc = osc;
+ // save collected intermediates
this._collect = collect;
- this._params = params;
-
- // save active dcs handler reference
- this._activeDcsHandler = dcsHandler;
// save state
this.currentState = currentState;
diff --git a/src/common/parser/OscParser.test.ts b/src/common/parser/OscParser.test.ts
new file mode 100644
index 00000000..6969d6f3
--- /dev/null
+++ b/src/common/parser/OscParser.test.ts
@@ -0,0 +1,251 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+import { assert } from 'chai';
+import { OscParser, OscHandler } from 'common/parser/OscParser';
+import { StringToUtf32, utf32ToString } from 'common/input/TextDecoder';
+import { IOscHandler } from 'common/parser/Types';
+import { PAYLOAD_LIMIT } from 'common/parser/Constants';
+
+function toUtf32(s: string): Uint32Array {
+ const utf32 = new Uint32Array(s.length);
+ const decoder = new StringToUtf32();
+ const length = decoder.decode(s, utf32);
+ return utf32.subarray(0, length);
+}
+
+class TestHandler implements IOscHandler {
+ constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {}
+ start(): void {
+ this.output.push([this.msg, this.id, 'START']);
+ }
+ put(data: Uint32Array, start: number, end: number): void {
+ this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
+ }
+ end(success: boolean): void | boolean {
+ this.output.push([this.msg, this.id, 'END', success]);
+ if (this.returnFalse) {
+ return false;
+ }
+ }
+}
+
+describe('OscParser', () => {
+ let parser: OscParser;
+ let reports: any[] = [];
+ beforeEach(() => {
+ reports = [];
+ parser = new OscParser();
+ parser.setHandlerFallback((id, action, data) => {
+ reports.push([id, action, data]);
+ });
+ });
+ describe('identifier parsing', () => {
+ it('no report for illegal ids', () => {
+ const data = toUtf32('hello world!');
+ parser.put(data, 0, data.length);
+ parser.end(true);
+ assert.deepEqual(reports, []);
+ });
+ it('no payload', () => {
+ parser.start();
+ let data = toUtf32('12');
+ parser.put(data, 0, data.length);
+ data = toUtf32('34');
+ parser.put(data, 0, data.length);
+ parser.end(true);
+ assert.deepEqual(reports, [[1234, 'START', undefined], [1234, 'END', true]]);
+ });
+ it('with payload', () => {
+ parser.start();
+ let data = toUtf32('12');
+ parser.put(data, 0, data.length);
+ data = toUtf32('34');
+ parser.put(data, 0, data.length);
+ data = toUtf32(';h');
+ parser.put(data, 0, data.length);
+ data = toUtf32('ello');
+ parser.put(data, 0, data.length);
+ parser.end(true);
+ assert.deepEqual(reports, [
+ [1234, 'START', undefined],
+ [1234, 'PUT', 'h'],
+ [1234, 'PUT', 'ello'],
+ [1234, 'END', true]
+ ]);
+ });
+ });
+ describe('handler registration', () => {
+ it('setOscHandler', () => {
+ parser.setHandler(1234, new TestHandler(1234, reports, 'th'));
+ parser.start();
+ let data = toUtf32('1234;Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32('the mouse!');
+ parser.put(data, 0, data.length);
+ parser.end(true);
+ assert.deepEqual(reports, [
+ // messages from TestHandler
+ ['th', 1234, 'START'],
+ ['th', 1234, 'PUT', 'Here comes'],
+ ['th', 1234, 'PUT', 'the mouse!'],
+ ['th', 1234, 'END', true]
+ ]);
+ });
+ it('clearOscHandler', () => {
+ parser.setHandler(1234, new TestHandler(1234, reports, 'th'));
+ parser.clearHandler(1234);
+ parser.start();
+ let data = toUtf32('1234;Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32('the mouse!');
+ parser.put(data, 0, data.length);
+ parser.end(true);
+ assert.deepEqual(reports, [
+ // messages from fallback handler
+ [1234, 'START', undefined],
+ [1234, 'PUT', 'Here comes'],
+ [1234, 'PUT', 'the mouse!'],
+ [1234, 'END', true]
+ ]);
+ });
+ it('addOscHandler', () => {
+ parser.setHandler(1234, new TestHandler(1234, reports, 'th1'));
+ parser.addHandler(1234, new TestHandler(1234, reports, 'th2'));
+ parser.start();
+ let data = toUtf32('1234;Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32('the mouse!');
+ parser.put(data, 0, data.length);
+ parser.end(true);
+ assert.deepEqual(reports, [
+ ['th2', 1234, 'START'],
+ ['th1', 1234, 'START'],
+ ['th2', 1234, 'PUT', 'Here comes'],
+ ['th1', 1234, 'PUT', 'Here comes'],
+ ['th2', 1234, 'PUT', 'the mouse!'],
+ ['th1', 1234, 'PUT', 'the mouse!'],
+ ['th2', 1234, 'END', true],
+ ['th1', 1234, 'END', false] // false due being already handled by th2!
+ ]);
+ });
+ it('addOscHandler with return false', () => {
+ parser.setHandler(1234, new TestHandler(1234, reports, 'th1'));
+ parser.addHandler(1234, new TestHandler(1234, reports, 'th2', true));
+ parser.start();
+ let data = toUtf32('1234;Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32('the mouse!');
+ parser.put(data, 0, data.length);
+ parser.end(true);
+ assert.deepEqual(reports, [
+ ['th2', 1234, 'START'],
+ ['th1', 1234, 'START'],
+ ['th2', 1234, 'PUT', 'Here comes'],
+ ['th1', 1234, 'PUT', 'Here comes'],
+ ['th2', 1234, 'PUT', 'the mouse!'],
+ ['th1', 1234, 'PUT', 'the mouse!'],
+ ['th2', 1234, 'END', true],
+ ['th1', 1234, 'END', true] // true since th2 indicated to keep bubbling
+ ]);
+ });
+ it('dispose handlers', () => {
+ parser.setHandler(1234, new TestHandler(1234, reports, 'th1'));
+ const dispo = parser.addHandler(1234, new TestHandler(1234, reports, 'th2', true));
+ dispo.dispose();
+ parser.start();
+ let data = toUtf32('1234;Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32('the mouse!');
+ parser.put(data, 0, data.length);
+ parser.end(true);
+ assert.deepEqual(reports, [
+ ['th1', 1234, 'START'],
+ ['th1', 1234, 'PUT', 'Here comes'],
+ ['th1', 1234, 'PUT', 'the mouse!'],
+ ['th1', 1234, 'END', true]
+ ]);
+ });
+ });
+ describe('OscHandlerFactory', () => {
+ it('should be called once on end(true)', () => {
+ parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
+ parser.start();
+ let data = toUtf32('1234;Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32(' the mouse!');
+ parser.put(data, 0, data.length);
+ parser.end(true);
+ assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]);
+ });
+ it('should not be called on end(false)', () => {
+ parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
+ parser.start();
+ let data = toUtf32('1234;Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32(' the mouse!');
+ parser.put(data, 0, data.length);
+ parser.end(false);
+ assert.deepEqual(reports, []);
+ });
+ it('should be disposable', () => {
+ parser.setHandler(1234, new OscHandler(data => reports.push(['one', data])));
+ const dispo = parser.addHandler(1234, new OscHandler(data => reports.push(['two', data])));
+ parser.start();
+ let data = toUtf32('1234;Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32(' the mouse!');
+ parser.put(data, 0, data.length);
+ parser.end(true);
+ assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
+ dispo.dispose();
+ parser.start();
+ data = toUtf32('1234;some other');
+ parser.put(data, 0, data.length);
+ data = toUtf32(' data');
+ parser.put(data, 0, data.length);
+ parser.end(true);
+ assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
+ });
+ it('should respect return false', () => {
+ parser.setHandler(1234, new OscHandler(data => reports.push(['one', data])));
+ parser.addHandler(1234, new OscHandler(data => { reports.push(['two', data]); return false; }));
+ parser.start();
+ let data = toUtf32('1234;Here comes');
+ parser.put(data, 0, data.length);
+ data = toUtf32(' the mouse!');
+ parser.put(data, 0, data.length);
+ parser.end(true);
+ assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
+ });
+ it('should work up to payload limit', function(): void {
+ this.timeout(10000);
+ parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
+ parser.start();
+ let data = toUtf32('1234;');
+ parser.put(data, 0, data.length);
+ data = toUtf32('A'.repeat(1000));
+ for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
+ parser.put(data, 0, data.length);
+ }
+ parser.end(true);
+ assert.deepEqual(reports, [[1234, 'A'.repeat(PAYLOAD_LIMIT)]]);
+ });
+ it('should abort for payload limit +1', function(): void {
+ this.timeout(10000);
+ parser.setHandler(1234, new OscHandler(data => reports.push([1234, data])));
+ parser.start();
+ let data = toUtf32('1234;');
+ parser.put(data, 0, data.length);
+ data = toUtf32('A'.repeat(1000));
+ for (let i = 0; i < PAYLOAD_LIMIT; i += 1000) {
+ parser.put(data, 0, data.length);
+ }
+ data = toUtf32('A');
+ parser.put(data, 0, data.length);
+ parser.end(true);
+ assert.deepEqual(reports, []);
+ });
+ });
+});
diff --git a/src/common/parser/OscParser.ts b/src/common/parser/OscParser.ts
new file mode 100644
index 00000000..e8c5a801
--- /dev/null
+++ b/src/common/parser/OscParser.ts
@@ -0,0 +1,203 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser } from 'common/parser/Types';
+import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants';
+import { utf32ToString } from 'common/input/TextDecoder';
+import { IDisposable } from 'common/Types';
+
+
+export class OscParser implements IOscParser {
+ private _state = OscState.START;
+ private _id = -1;
+ private _handlers: IHandlerCollection = Object.create(null);
+ private _handlerFb: OscFallbackHandlerType = () => { };
+
+ public addHandler(ident: number, handler: IOscHandler): IDisposable {
+ if (this._handlers[ident] === undefined) {
+ this._handlers[ident] = [];
+ }
+ const handlerList = this._handlers[ident];
+ handlerList.push(handler);
+ return {
+ dispose: () => {
+ const handlerIndex = handlerList.indexOf(handler);
+ if (handlerIndex !== -1) {
+ handlerList.splice(handlerIndex, 1);
+ }
+ }
+ };
+ }
+ public setHandler(ident: number, handler: IOscHandler): void {
+ this._handlers[ident] = [handler];
+ }
+ public clearHandler(ident: number): void {
+ if (this._handlers[ident]) delete this._handlers[ident];
+ }
+ public setHandlerFallback(handler: OscFallbackHandlerType): void {
+ this._handlerFb = handler;
+ }
+
+ public dispose(): void {
+ this._handlers = Object.create(null);
+ this._handlerFb = () => {};
+ }
+
+ public reset(): void {
+ // cleanup handlers if payload was already sent
+ if (this._state === OscState.PAYLOAD) {
+ this.end(false);
+ }
+ this._id = -1;
+ this._state = OscState.START;
+ }
+
+ private _start(): void {
+ const handlers = this._handlers[this._id];
+ if (!handlers) {
+ this._handlerFb(this._id, 'START');
+ } else {
+ for (let j = handlers.length - 1; j >= 0; j--) {
+ handlers[j].start();
+ }
+ }
+ }
+
+ private _put(data: Uint32Array, start: number, end: number): void {
+ const handlers = this._handlers[this._id];
+ if (!handlers) {
+ this._handlerFb(this._id, 'PUT', utf32ToString(data, start, end));
+ } else {
+ for (let j = handlers.length - 1; j >= 0; j--) {
+ handlers[j].put(data, start, end);
+ }
+ }
+ }
+
+ private _end(success: boolean): void {
+ // other than the old code we always have to call .end
+ // to keep the bubbling we use `success` to indicate
+ // whether a handler should execute
+ const handlers = this._handlers[this._id];
+ if (!handlers) {
+ this._handlerFb(this._id, 'END', success);
+ } else {
+ let j = handlers.length - 1;
+ for (; j >= 0; j--) {
+ if (handlers[j].end(success) !== false) {
+ break;
+ }
+ }
+ j--;
+ // cleanup left over handlers
+ for (; j >= 0; j--) {
+ handlers[j].end(false);
+ }
+ }
+ }
+
+ public start(): void {
+ // always reset leftover handlers
+ this.reset();
+ this._id = -1;
+ this._state = OscState.ID;
+ }
+
+ /**
+ * Put data to current OSC command.
+ * Expects the identifier of the OSC command in the form
+ * OSC id ; payload ST/BEL
+ * Payload chunks are not further processed and get
+ * directly passed to the handlers.
+ */
+ public put(data: Uint32Array, start: number, end: number): void {
+ if (this._state === OscState.ABORT) {
+ return;
+ }
+ if (this._state === OscState.ID) {
+ while (start < end) {
+ const code = data[start++];
+ if (code === 0x3b) {
+ this._state = OscState.PAYLOAD;
+ this._start();
+ break;
+ }
+ if (code < 0x30 || 0x39 < code) {
+ this._state = OscState.ABORT;
+ return;
+ }
+ if (this._id === -1) {
+ this._id = 0;
+ }
+ this._id = this._id * 10 + code - 48;
+ }
+ }
+ if (this._state === OscState.PAYLOAD && end - start > 0) {
+ this._put(data, start, end);
+ }
+ }
+
+ /**
+ * Indicates end of an OSC command.
+ * Whether the OSC got aborted or finished normally
+ * is indicated by `success`.
+ */
+ public end(success: boolean): void {
+ if (this._state === OscState.START) {
+ return;
+ }
+ // do nothing if command was faulty
+ if (this._state !== OscState.ABORT) {
+ // if we are still in ID state and get an early end
+ // means that the command has no payload thus we still have
+ // to announce START and send END right after
+ if (this._state === OscState.ID) {
+ this._start();
+ }
+ this._end(success);
+ }
+ this._id = -1;
+ this._state = OscState.START;
+ }
+}
+
+/**
+ * Convenient class to allow attaching string based handler functions
+ * as OSC handlers.
+ */
+export class OscHandler implements IOscHandler {
+ private _data = '';
+ private _hitLimit: boolean = false;
+
+ constructor(private _handler: (data: string) => any) {}
+
+ public start(): void {
+ this._data = '';
+ this._hitLimit = false;
+ }
+
+ public put(data: Uint32Array, start: number, end: number): void {
+ if (this._hitLimit) {
+ return;
+ }
+ this._data += utf32ToString(data, start, end);
+ if (this._data.length > PAYLOAD_LIMIT) {
+ this._data = '';
+ this._hitLimit = true;
+ }
+ }
+
+ public end(success: boolean): any {
+ let ret;
+ if (this._hitLimit) {
+ ret = false;
+ } else if (success) {
+ ret = this._handler(this._data);
+ }
+ this._data = '';
+ this._hitLimit = false;
+ return ret;
+ }
+}
diff --git a/src/common/parser/Params.test.ts b/src/common/parser/Params.test.ts
index 01287b8f..2418fa45 100644
--- a/src/common/parser/Params.test.ts
+++ b/src/common/parser/Params.test.ts
@@ -16,27 +16,29 @@ class TestParams extends Params {
}
/** `Params` parser shim */
-function parse(params: Params, s: string): void {
+function parse(params: Params, s: string | string[]): void {
params.reset();
params.addParam(0);
- let isSub = false;
- for (let i = 0; i < s.length; ++i) {
- let code = s.charCodeAt(i);
- do {
- switch (code) {
- case 0x3b:
- params.addParam(0);
- isSub = false;
- break;
- case 0x3a:
- params.addSubParam(-1);
- isSub = true;
- break;
- default: // 0x30 - 0x39
- if (isSub) params.addSubParamDigit(code - 48);
- else params.addParamDigit(code - 48);
- }
- } while (++i < s.length && (code = s.charCodeAt(i)) > 0x2f && code < 0x3c);
+ if (typeof s === 'string') {
+ s = [s];
+ }
+ for (const chunk of s) {
+ for (let i = 0; i < chunk.length; ++i) {
+ let code = chunk.charCodeAt(i);
+ do {
+ switch (code) {
+ case 0x3b:
+ params.addParam(0);
+ break;
+ case 0x3a:
+ params.addSubParam(-1);
+ break;
+ default: // 0x30 - 0x39
+ params.addDigit(code - 48);
+ }
+ } while (++i < s.length && (code = chunk.charCodeAt(i)) > 0x2f && code < 0x3c);
+ i--;
+ }
}
}
@@ -217,4 +219,19 @@ describe('Params', () => {
assert.deepEqual(params.toArray(), [0, [0x7FFFFFFF]]);
});
});
+ describe('issue 2389', () => {
+ it('should cancel subdigits if beyond params limit', () => {
+ const params = new Params();
+ parse(params, ';;;;;;;;;10;;;;;;;;;;20;;;;;;;;;;30;31;32;33;34;35::::::::');
+ assert.deepEqual(params.toArray(), [
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 10,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 20,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 30, 31, 32]);
+ });
+ it('should carry forward isSub state', () => {
+ const params = new Params();
+ parse(params, ['1:22:33', '44']);
+ assert.deepEqual(params.toArray(), [1, [22, 3344]]);
+ });
+ });
});
diff --git a/src/common/parser/Params.ts b/src/common/parser/Params.ts
index 7ef8341a..47ee129d 100644
--- a/src/common/parser/Params.ts
+++ b/src/common/parser/Params.ts
@@ -41,6 +41,7 @@ export class Params implements IParams {
private _subParamsIdx: Uint16Array;
private _rejectDigits: boolean;
private _rejectSubDigits: boolean;
+ private _digitIsSub: boolean;
/**
* Create a `Params` type from JS array representation.
@@ -79,6 +80,7 @@ export class Params implements IParams {
this._subParamsIdx = new Uint16Array(maxLength);
this._rejectDigits = false;
this._rejectSubDigits = false;
+ this._digitIsSub = false;
}
/**
@@ -91,6 +93,9 @@ export class Params implements IParams {
newParams._subParams.set(this._subParams);
newParams._subParamsLength = this._subParamsLength;
newParams._subParamsIdx.set(this._subParamsIdx);
+ newParams._rejectDigits = this._rejectDigits;
+ newParams._rejectSubDigits = this._rejectSubDigits;
+ newParams._digitIsSub = this._digitIsSub;
return newParams;
}
@@ -121,6 +126,7 @@ export class Params implements IParams {
this._subParamsLength = 0;
this._rejectDigits = false;
this._rejectSubDigits = false;
+ this._digitIsSub = false;
}
/**
@@ -131,6 +137,7 @@ export class Params implements IParams {
* store up to 30.
*/
public addParam(value: number): void {
+ this._digitIsSub = false;
if (this.length >= this.maxLength) {
this._rejectDigits = true;
return;
@@ -150,10 +157,11 @@ export class Params implements IParams {
* sub parameter will be ignored.
*/
public addSubParam(value: number): void {
+ this._digitIsSub = true;
if (!this.length) {
return;
}
- if (this._subParamsLength >= this.maxSubParamsLength) {
+ if (this._rejectDigits || this._subParamsLength >= this.maxSubParamsLength) {
this._rejectSubDigits = true;
return;
}
@@ -204,30 +212,18 @@ export class Params implements IParams {
/**
* Add a single digit value to current parameter.
* This is used by the parser to account digits on a char by char basis.
- * Do not use this method directly, consider using `addParam` instead.
*/
- public addParamDigit(value: number): void {
- if (this._rejectDigits) {
+ public addDigit(value: number): void {
+ let length;
+ if (this._rejectDigits
+ || !(length = this._digitIsSub ? this._subParamsLength : this.length)
+ || (this._digitIsSub && this._rejectSubDigits)
+ ) {
return;
}
- const v = this.params[this.length - 1] * 10 + value;
- this.params[this.length - 1] = v > MAX_VALUE ? MAX_VALUE : v;
- }
- /**
- * Add a single digit value to current sub parameter.
- * This is used by the parser to account digits on a char by char basis.
- * Do not use this method directly, consider using `addSubParam` instead.
- */
- public addSubParamDigit(value: number): void {
- if (!this._subParamsLength || this._rejectDigits || this._rejectSubDigits) {
- return;
- }
- if (this._subParams[this._subParamsLength - 1] === -1) {
- this._subParams[this._subParamsLength - 1] = value;
- } else {
- const v = this._subParams[this._subParamsLength - 1] * 10 + value;
- this._subParams[this._subParamsLength - 1] = v > MAX_VALUE ? MAX_VALUE : v;
- }
+ const store = this._digitIsSub ? this._subParams : this.params;
+ const cur = store[length - 1];
+ store[length - 1] = ~cur ? Math.min(cur * 10 + value, MAX_VALUE) : value;
}
}
diff --git a/src/common/parser/Types.d.ts b/src/common/parser/Types.d.ts
index d432800d..e2fac89f 100644
--- a/src/common/parser/Types.d.ts
+++ b/src/common/parser/Types.d.ts
@@ -53,10 +53,8 @@ export interface IParsingState {
code: number;
// current parser state
currentState: ParserState;
- // osc string buffer
- osc: string;
// collect buffer with intermediate characters
- collect: string;
+ collect: number;
// params buffer
params: IParams;
// should abort (default: false)
@@ -64,31 +62,83 @@ export interface IParsingState {
}
/**
-* DCS handler signature for EscapeSequenceParser.
-* EscapeSequenceParser handles DCS commands via separate
-* subparsers that get hook/unhooked and can handle
-* arbitrary amount of 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.
-* Note: A DCS parser is only instantiated once, therefore
-* you cannot rely on the ctor to reinitialize state.
-*
-* EscapeSequenceParser will call `put` several times if the
-* parsed data got split, therefore you might have to collect
-* `data` until `unhook` is called.
-* Note: `data` is borrowed, if you cannot process the data
-* in chunks you have to copy it, doing otherwise will lead to
-* data losses or corruption.
-*
-* `unhook` marks the end of the current DCS sequence.
-*/
+ * Command handler interfaces.
+ */
+
+/**
+ * CSI handler types.
+ * Note: `params` is borrowed.
+ */
+export type CsiHandlerType = (params: IParams) => boolean | void;
+export type CsiFallbackHandlerType = (ident: number, params: IParams) => void;
+
+/**
+ * DCS handler types.
+ */
export interface IDcsHandler {
- hook(collect: string, params: IParams, flag: number): void;
+ /**
+ * Called when a DCS command starts.
+ * Prepare needed data structures here.
+ * Note: `params` is borrowed.
+ */
+ hook(params: IParams): void;
+ /**
+ * Incoming payload chunk.
+ * Note: `params` is borrowed.
+ */
put(data: Uint32Array, start: number, end: number): void;
- unhook(): void;
+ /**
+ * End of DCS command. `success` indicates whether the
+ * command finished normally or got aborted, thus final
+ * execution of the command should depend on `success`.
+ * To save memory also cleanup data structures here.
+ */
+ unhook(success: boolean): void | boolean;
}
+export type DcsFallbackHandlerType = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void;
+
+/**
+ * ESC handler types.
+ */
+export type EscHandlerType = () => boolean | void;
+export type EscFallbackHandlerType = (identifier: number) => void;
+
+/**
+ * EXECUTE handler types.
+ */
+export type ExecuteHandlerType = () => boolean | void;
+export type ExecuteFallbackHandlerType = (ident: number) => void;
+
+/**
+ * OSC handler types.
+ */
+export interface IOscHandler {
+ /**
+ * Announces start of this OSC command.
+ * Prepare needed data structures here.
+ */
+ start(): void;
+ /**
+ * Incoming data chunk.
+ * Note: Data is borrowed.
+ */
+ put(data: Uint32Array, start: number, end: number): void;
+ /**
+ * End of OSC command. `success` indicates whether the
+ * command finished normally or got aborted, thus final
+ * execution of the command should depend on `success`.
+ * To save memory also cleanup data structures here.
+ */
+ end(success: boolean): void | boolean;
+}
+export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
+
+/**
+ * PRINT handler types.
+ */
+export type PrintHandlerType = (data: Uint32Array, start: number, end: number) => void;
+export type PrintFallbackHandlerType = PrintHandlerType;
+
/**
* EscapeSequenceParser interface.
@@ -112,31 +162,83 @@ export interface IEscapeSequenceParser extends IDisposable {
*/
parse(data: Uint32Array, length: number): void;
- setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void;
+ /**
+ * Get string from numercial function identifier `ident`.
+ * Useful in fallback handlers which expose the low level
+ * numcerical function identifier for debugging purposes.
+ * Note: A full back translation to `IFunctionIdentifier`
+ * is not implemented.
+ */
+ identToString(ident: number): string;
+
+ setPrintHandler(handler: PrintHandlerType): void;
clearPrintHandler(): void;
- setExecuteHandler(flag: string, callback: () => void): void;
+ setEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): void;
+ clearEscHandler(id: IFunctionIdentifier): void;
+ setEscHandlerFallback(handler: EscFallbackHandlerType): void;
+ addEscHandler(id: IFunctionIdentifier, handler: EscHandlerType): IDisposable;
+
+ setExecuteHandler(flag: string, handler: ExecuteHandlerType): void;
clearExecuteHandler(flag: string): void;
- setExecuteHandlerFallback(callback: (code: number) => void): void;
+ setExecuteHandlerFallback(handler: ExecuteFallbackHandlerType): void;
- setCsiHandler(flag: string, callback: (params: IParams, collect: string) => void): void;
- clearCsiHandler(flag: string): void;
- setCsiHandlerFallback(callback: (collect: string, params: IParams, flag: number) => void): void;
- addCsiHandler(flag: string, callback: (params: IParams, collect: string) => boolean): IDisposable;
- addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
+ setCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): void;
+ clearCsiHandler(id: IFunctionIdentifier): void;
+ setCsiHandlerFallback(callback: CsiFallbackHandlerType): void;
+ addCsiHandler(id: IFunctionIdentifier, handler: CsiHandlerType): IDisposable;
- setEscHandler(collectAndFlag: string, callback: () => void): void;
- clearEscHandler(collectAndFlag: string): void;
- setEscHandlerFallback(callback: (collect: string, flag: number) => void): void;
+ setDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): void;
+ clearDcsHandler(id: IFunctionIdentifier): void;
+ setDcsHandlerFallback(handler: DcsFallbackHandlerType): void;
+ addDcsHandler(id: IFunctionIdentifier, handler: IDcsHandler): IDisposable;
- setOscHandler(ident: number, callback: (data: string) => void): void;
+ setOscHandler(ident: number, handler: IOscHandler): void;
clearOscHandler(ident: number): void;
- setOscHandlerFallback(callback: (identifier: number, data: string) => void): void;
+ setOscHandlerFallback(handler: OscFallbackHandlerType): void;
+ addOscHandler(ident: number, handler: IOscHandler): IDisposable;
- setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void;
- clearDcsHandler(collectAndFlag: string): void;
- setDcsHandlerFallback(handler: IDcsHandler): void;
-
- setErrorHandler(callback: (state: IParsingState) => IParsingState): void;
+ setErrorHandler(handler: (state: IParsingState) => IParsingState): void;
clearErrorHandler(): void;
}
+
+/**
+ * Subparser interfaces.
+ * The subparsers are instantiated in `EscapeSequenceParser` and
+ * called during `EscapeSequenceParser.parse`.
+ */
+export interface ISubParser extends IDisposable {
+ reset(): void;
+ addHandler(ident: number, handler: T): IDisposable;
+ setHandler(ident: number, handler: T): void;
+ clearHandler(ident: number): void;
+ setHandlerFallback(handler: U): void;
+ put(data: Uint32Array, start: number, end: number): void;
+}
+
+export interface IOscParser extends ISubParser {
+ start(): void;
+ end(success: boolean): void;
+}
+
+export interface IDcsParser extends ISubParser {
+ hook(ident: number, params: IParams): void;
+ unhook(success: boolean): void;
+}
+
+/**
+ * Interface to denote a specific ESC, CSI or DCS handler slot.
+ * The values are used to create an integer respresentation during handler
+ * regristation before passed to the subparsers as `ident`.
+ * The integer translation is made to allow a faster handler access
+ * in `EscapeSequenceParser.parse`.
+ */
+export interface IFunctionIdentifier {
+ prefix?: string;
+ intermediates?: string;
+ final: string;
+}
+
+export interface IHandlerCollection {
+ [key: string]: T[];
+}
diff --git a/src/common/services/CoreMouseService.test.ts b/src/common/services/CoreMouseService.test.ts
new file mode 100644
index 00000000..2bf011b0
--- /dev/null
+++ b/src/common/services/CoreMouseService.test.ts
@@ -0,0 +1,203 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+import { CoreMouseService } from 'common/services/CoreMouseService';
+import { MockCoreService, MockBufferService } from 'common/TestUtils.test';
+import { assert } from 'chai';
+import { ICoreMouseEvent, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
+
+// needed mock services
+const bufferService = new MockBufferService(300, 100);
+const coreService = new MockCoreService();
+
+function toBytes(s: string | undefined): number[] {
+ if (!s) {
+ return [];
+ }
+ const res: number[] = [];
+ for (let i = 0; i < s.length; ++i) {
+ res.push(s.charCodeAt(i));
+ }
+ return res;
+}
+
+describe('CoreMouseService', () => {
+ it('init', () => {
+ const cms = new CoreMouseService(bufferService, coreService);
+ assert.equal(cms.activeEncoding, 'DEFAULT');
+ assert.equal(cms.activeProtocol, 'NONE');
+ });
+ it('default protocols - NONE, X10, VT200, DRAG, ANY', () => {
+ const cms = new CoreMouseService(bufferService, coreService);
+ assert.deepEqual(Object.keys((cms as any)._protocols), ['NONE', 'X10', 'VT200', 'DRAG', 'ANY']);
+ });
+ it('default encodings - DEFAULT, SGR', () => {
+ const cms = new CoreMouseService(bufferService, coreService);
+ assert.deepEqual(Object.keys((cms as any)._encodings), ['DEFAULT', 'SGR']);
+ });
+ it('protocol/encoding setter, reset', () => {
+ const cms = new CoreMouseService(bufferService, coreService);
+ cms.activeEncoding = 'SGR';
+ cms.activeProtocol = 'ANY';
+ assert.equal(cms.activeEncoding, 'SGR');
+ assert.equal(cms.activeProtocol, 'ANY');
+ cms.reset();
+ assert.equal(cms.activeEncoding, 'DEFAULT');
+ assert.equal(cms.activeProtocol, 'NONE');
+ assert.throws(() => { cms.activeEncoding = 'xyz'; }, 'unknown encoding "xyz"');
+ assert.throws(() => { cms.activeProtocol = 'xyz'; }, 'unknown protocol "xyz"');
+ });
+ it('addEncoding', () => {
+ const cms = new CoreMouseService(bufferService, coreService);
+ cms.addEncoding('XYZ', (e: ICoreMouseEvent) => '');
+ cms.activeEncoding = 'XYZ';
+ assert.equal(cms.activeEncoding, 'XYZ');
+ });
+ it('addProtocol', () => {
+ const cms = new CoreMouseService(bufferService, coreService);
+ cms.addProtocol('XYZ', { events: CoreMouseEventType.NONE, restrict: (e: ICoreMouseEvent) => false });
+ cms.activeProtocol = 'XYZ';
+ assert.equal(cms.activeProtocol, 'XYZ');
+ });
+ it('onProtocolChange', () => {
+ const cms = new CoreMouseService(bufferService, coreService);
+ const wantedEvents: CoreMouseEventType[] = [];
+ cms.onProtocolChange(events => wantedEvents.push(events));
+ cms.activeProtocol = 'NONE';
+ assert.deepEqual(wantedEvents, [CoreMouseEventType.NONE]);
+ cms.activeProtocol = 'ANY';
+ assert.deepEqual(wantedEvents, [
+ CoreMouseEventType.NONE,
+ CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE
+ ]);
+ });
+ describe('triggerMouseEvent', () => {
+ let cms: CoreMouseService;
+ let reports: string[];
+ beforeEach(() => {
+ cms = new CoreMouseService(bufferService, coreService);
+ reports = [];
+ coreService.triggerDataEvent = (data: string, userInput?: boolean) => reports.push(data);
+ coreService.triggerBinaryEvent = (data: string) => reports.push(data);
+ });
+ it('NONE', () => {
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
+ });
+ it('X10', () => {
+ cms.activeProtocol = 'X10';
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
+ });
+ it('VT200', () => {
+ cms.activeProtocol = 'VT200';
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), false);
+ });
+ it('DRAG', () => {
+ cms.activeProtocol = 'DRAG';
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
+ });
+ it('ANY', () => {
+ cms.activeProtocol = 'ANY';
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE }), true);
+ // should not report in any case
+ // invalid button + action combinations
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.MOVE }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.DOWN }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.UP }), false);
+ // invalid coords
+ assert.equal(cms.triggerMouseEvent({ col: -1, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 500, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: -1, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 500, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), false);
+ });
+ describe('coords', () => {
+ it('DEFAULT encoding', () => {
+ cms.activeProtocol = 'ANY';
+ for (let i = 0; i < bufferService.cols; ++i) {
+ assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
+ if (i > 222) {
+ // supress mouse reports if we are out of addressible range (max. 222)
+ assert.deepEqual(toBytes(reports.pop()), []);
+ } else {
+ assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]);
+ }
+ }
+ });
+ it('SGR encoding', () => {
+ cms.activeProtocol = 'ANY';
+ cms.activeEncoding = 'SGR';
+ for (let i = 0; i < bufferService.cols; ++i) {
+ assert.equal(cms.triggerMouseEvent({ col: i, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
+ assert.deepEqual(reports.pop(), `\x1b[<0;${i + 1};1M`);
+ }
+ });
+ });
+ it('eventCodes with modifiers (DEFAULT encoding)', () => {
+ // TODO: implement AUX button tests
+ cms.activeProtocol = 'ANY';
+ cms.activeEncoding = 'DEFAULT';
+ // all buttons + down + no modifer
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.DOWN, ctrl: false, alt: false, shift: false }), true);
+ assert.deepEqual(reports, ['\x1b[M !!', '\x1b[M!!!', '\x1b[M"!!', '\x1b[Ma!!']);
+ while (reports.pop()) { }
+
+ // all buttons + up + no modifier
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.WHEEL, action: CoreMouseAction.UP, ctrl: false, alt: false, shift: false }), true);
+ assert.deepEqual(reports, ['\x1b[M#!!', '\x1b[M#!!', '\x1b[M#!!', '\x1b[M`!!']);
+ while (reports.pop()) { }
+
+ // all buttons + move + no modifier
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.MIDDLE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.RIGHT, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: false }), true);
+ assert.deepEqual(reports, ['\x1b[M@!!', '\x1b[MA!!', '\x1b[MB!!', '\x1b[MC!!']);
+ while (reports.pop()) { }
+
+ // button none + move + modifiers
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: false, shift: false }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: false }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: false, shift: true }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: false }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: false, alt: true, shift: true }), true);
+ assert.equal(cms.triggerMouseEvent({ col: 0, row: 0, button: CoreMouseButton.NONE, action: CoreMouseAction.MOVE, ctrl: true, alt: true, shift: true }), true);
+ assert.deepEqual(reports, ['\x1b[MS!!', '\x1b[MK!!', '\x1b[MG!!', '\x1b[M[!!', '\x1b[MO!!', '\x1b[M_!!']);
+ while (reports.pop()) { }
+ });
+ });
+});
diff --git a/src/common/services/CoreMouseService.ts b/src/common/services/CoreMouseService.ts
new file mode 100644
index 00000000..0bd25dd0
--- /dev/null
+++ b/src/common/services/CoreMouseService.ts
@@ -0,0 +1,305 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+import { IBufferService, ICoreService, ICoreMouseService } from 'common/services/Services';
+import { EventEmitter, IEvent } from 'common/EventEmitter';
+import { ICoreMouseProtocol, ICoreMouseEvent, CoreMouseEncoding, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
+
+/**
+ * Supported default protocols.
+ */
+const DEFAULT_PROTOCOLS: {[key: string]: ICoreMouseProtocol} = {
+ /**
+ * NONE
+ * Events: none
+ * Modifiers: none
+ */
+ NONE: {
+ events: CoreMouseEventType.NONE,
+ restrict: () => false
+ },
+ /**
+ * X10
+ * Events: mousedown
+ * Modifiers: none
+ */
+ X10: {
+ events: CoreMouseEventType.DOWN,
+ restrict: (e: ICoreMouseEvent) => {
+ // no wheel, no move, no up
+ if (e.button === CoreMouseButton.WHEEL || e.action !== CoreMouseAction.DOWN) {
+ return false;
+ }
+ // no modifiers
+ e.ctrl = false;
+ e.alt = false;
+ e.shift = false;
+ return true;
+ }
+ },
+ /**
+ * VT200
+ * Events: mousedown / mouseup / wheel
+ * Modifiers: all
+ */
+ VT200: {
+ events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL,
+ restrict: (e: ICoreMouseEvent) => {
+ // no move
+ if (e.action === CoreMouseAction.MOVE) {
+ return false;
+ }
+ return true;
+ }
+ },
+ /**
+ * DRAG
+ * Events: mousedown / mouseup / wheel / mousedrag
+ * Modifiers: all
+ */
+ DRAG: {
+ events: CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL | CoreMouseEventType.DRAG,
+ restrict: (e: ICoreMouseEvent) => {
+ // no move without button
+ if (e.action === CoreMouseAction.MOVE && e.button === CoreMouseButton.NONE) {
+ return false;
+ }
+ return true;
+ }
+ },
+ /**
+ * ANY
+ * Events: all mouse related events
+ * Modifiers: all
+ */
+ ANY: {
+ events:
+ CoreMouseEventType.DOWN | CoreMouseEventType.UP | CoreMouseEventType.WHEEL
+ | CoreMouseEventType.DRAG | CoreMouseEventType.MOVE,
+ restrict: (e: ICoreMouseEvent) => true
+ }
+};
+
+const enum Modifiers {
+ SHIFT = 4,
+ ALT = 8,
+ CTRL = 16
+}
+
+// helper for default encoders to generate the event code.
+function eventCode(e: ICoreMouseEvent, isSGR: boolean): number {
+ let code = (e.ctrl ? Modifiers.CTRL : 0) | (e.shift ? Modifiers.SHIFT : 0) | (e.alt ? Modifiers.ALT : 0);
+ if (e.button === CoreMouseButton.WHEEL) {
+ code |= 64;
+ code |= e.action;
+ } else {
+ code |= e.button & 3;
+ if (e.button & 4) {
+ code |= 64;
+ }
+ if (e.button & 8) {
+ code |= 128;
+ }
+ if (e.action === CoreMouseAction.MOVE) {
+ code |= CoreMouseAction.MOVE;
+ } else if (e.action === CoreMouseAction.UP && !isSGR) {
+ // special case - only SGR can report button on release
+ // all others have to go with NONE
+ code |= CoreMouseButton.NONE;
+ }
+ }
+ return code;
+}
+
+const S = String.fromCharCode;
+
+/**
+ * Supported default encodings.
+ */
+const DEFAULT_ENCODINGS: {[key: string]: CoreMouseEncoding} = {
+ /**
+ * DEFAULT - CSI M Pb Px Py
+ * Single byte encoding for coords and event code.
+ * Can encode values up to 223 (1-based).
+ */
+ DEFAULT: (e: ICoreMouseEvent) => {
+ const params = [eventCode(e, false) + 32, e.col + 32, e.row + 32];
+ // supress mouse report if we exceed addressible range
+ // Note this is handled differently by emulators
+ // - xterm: sends 0;0 coords instead
+ // - vte, konsole: no report
+ if (params[0] > 255 || params[1] > 255 || params[2] > 255) {
+ return '';
+ }
+ return `\x1b[M${S(params[0])}${S(params[1])}${S(params[2])}`;
+ },
+ /**
+ * SGR - CSI < Pb ; Px ; Py M|m
+ * No encoding limitation.
+ * Can report button on release and works with a well formed sequence.
+ */
+ SGR: (e: ICoreMouseEvent) => {
+ const final = (e.action === CoreMouseAction.UP && e.button !== CoreMouseButton.WHEEL) ? 'm' : 'M';
+ return `\x1b[<${eventCode(e, true)};${e.col};${e.row}${final}`;
+ }
+};
+
+/**
+ * CoreMouseService
+ *
+ * Provides mouse tracking reports with different protocols and encodings.
+ * - protocols: NONE (default), X10, VT200, DRAG, ANY
+ * - encodings: DEFAULT, SGR (UTF8, URXVT removed in #2507)
+ *
+ * Custom protocols/encodings can be added by `addProtocol` / `addEncoding`.
+ * To activate a protocol/encoding, set `activeProtocol` / `activeEncoding`.
+ * Switching a protocol will send a notification event `onProtocolChange`
+ * with a list of needed events to track.
+ *
+ * The service handles the mouse tracking state and decides whether to send
+ * a tracking report to the backend based on protocol and encoding limitations.
+ * To send a mouse event call `triggerMouseEvent`.
+ */
+export class CoreMouseService implements ICoreMouseService {
+ private _protocols: {[name: string]: ICoreMouseProtocol} = {};
+ private _encodings: {[name: string]: CoreMouseEncoding} = {};
+ private _activeProtocol: string = '';
+ private _activeEncoding: string = '';
+ private _onProtocolChange = new EventEmitter();
+ private _lastEvent: ICoreMouseEvent | null = null;
+
+ constructor(
+ @IBufferService private readonly _bufferService: IBufferService,
+ @ICoreService private readonly _coreService: ICoreService
+ ) {
+ // register default protocols and encodings
+ Object.keys(DEFAULT_PROTOCOLS).forEach(name => this.addProtocol(name, DEFAULT_PROTOCOLS[name]));
+ Object.keys(DEFAULT_ENCODINGS).forEach(name => this.addEncoding(name, DEFAULT_ENCODINGS[name]));
+ // call reset to set defaults
+ this.reset();
+ }
+
+ public addProtocol(name: string, protocol: ICoreMouseProtocol): void {
+ this._protocols[name] = protocol;
+ }
+
+ public addEncoding(name: string, encoding: CoreMouseEncoding): void {
+ this._encodings[name] = encoding;
+ }
+
+ public get activeProtocol(): string {
+ return this._activeProtocol;
+ }
+
+ public set activeProtocol(name: string) {
+ if (!this._protocols[name]) {
+ throw new Error(`unknown protocol "${name}"`);
+ }
+ this._activeProtocol = name;
+ this._onProtocolChange.fire(this._protocols[name].events);
+ }
+
+ public get activeEncoding(): string {
+ return this._activeEncoding;
+ }
+
+ public set activeEncoding(name: string) {
+ if (!this._encodings[name]) {
+ throw new Error(`unknown encoding "${name}"`);
+ }
+ this._activeEncoding = name;
+ }
+
+ public reset(): void {
+ this.activeProtocol = 'NONE';
+ this.activeEncoding = 'DEFAULT';
+ this._lastEvent = null;
+ }
+
+ /**
+ * Event to announce changes in mouse tracking.
+ */
+ public get onProtocolChange(): IEvent {
+ return this._onProtocolChange.event;
+ }
+
+ /**
+ * Triggers a mouse event to be sent.
+ *
+ * Returns true if the event passed all protocol restrictions and a report
+ * was sent, otherwise false. The return value may be used to decide whether
+ * the default event action in the bowser component should be omitted.
+ *
+ * Note: The method will change values of the given event object
+ * to fullfill protocol and encoding restrictions.
+ */
+ public triggerMouseEvent(e: ICoreMouseEvent): boolean {
+ // range check for col/row
+ if (e.col < 0 || e.col >= this._bufferService.cols
+ || e.row < 0 || e.row >= this._bufferService.rows) {
+ return false;
+ }
+
+ // filter nonsense combinations of button + action
+ if (e.button === CoreMouseButton.WHEEL && e.action === CoreMouseAction.MOVE) {
+ return false;
+ }
+ if (e.button === CoreMouseButton.NONE && e.action !== CoreMouseAction.MOVE) {
+ return false;
+ }
+ if (e.button !== CoreMouseButton.WHEEL && (e.action === CoreMouseAction.LEFT || e.action === CoreMouseAction.RIGHT)) {
+ return false;
+ }
+
+ // report 1-based coords
+ e.col++;
+ e.row++;
+
+ // debounce move at grid level
+ if (e.action === CoreMouseAction.MOVE && this._lastEvent && this._compareEvents(this._lastEvent, e)) {
+ return false;
+ }
+
+ // apply protocol restrictions
+ if (!this._protocols[this._activeProtocol].restrict(e)) {
+ return false;
+ }
+
+ // encode report and send
+ const report = this._encodings[this._activeEncoding](e);
+ if (report) {
+ // always send DEFAULT as binary data
+ if (this._activeEncoding === 'DEFAULT') {
+ this._coreService.triggerBinaryEvent(report);
+ } else {
+ this._coreService.triggerDataEvent(report, true);
+ }
+ }
+
+ this._lastEvent = e;
+
+ return true;
+ }
+
+ public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} {
+ return {
+ DOWN: !!(events & CoreMouseEventType.DOWN),
+ UP: !!(events & CoreMouseEventType.UP),
+ DRAG: !!(events & CoreMouseEventType.DRAG),
+ MOVE: !!(events & CoreMouseEventType.MOVE),
+ WHEEL: !!(events & CoreMouseEventType.WHEEL)
+ };
+ }
+
+ private _compareEvents(e1: ICoreMouseEvent, e2: ICoreMouseEvent): boolean {
+ if (e1.col !== e2.col) return false;
+ if (e1.row !== e2.row) return false;
+ if (e1.button !== e2.button) return false;
+ if (e1.action !== e2.action) return false;
+ if (e1.ctrl !== e2.ctrl) return false;
+ if (e1.alt !== e2.alt) return false;
+ if (e1.shift !== e2.shift) return false;
+ return true;
+ }
+}
diff --git a/src/common/services/CoreService.test.ts b/src/common/services/CoreService.test.ts
new file mode 100644
index 00000000..00be4953
--- /dev/null
+++ b/src/common/services/CoreService.test.ts
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { ICoreService } from 'common/services/Services';
+import { CoreService } from 'common/services/CoreService';
+import { MockBufferService, MockLogService, MockOptionsService } from 'common/TestUtils.test';
+import { assert } from 'chai';
+
+describe('CoreService', () => {
+ let coreService: ICoreService;
+
+ beforeEach(() => {
+ coreService = new CoreService(
+ () => {},
+ new MockBufferService(80, 30),
+ new MockLogService(),
+ new MockOptionsService());
+ });
+
+ describe('reset', () => {
+ it('should not affect isCursorInitialized', () => {
+ coreService.isCursorInitialized = true;
+ coreService.reset();
+ assert.equal(coreService.isCursorInitialized, true);
+ coreService.isCursorInitialized = false;
+ coreService.reset();
+ assert.equal(coreService.isCursorInitialized, false);
+ });
+ });
+});
diff --git a/src/common/services/CoreService.ts b/src/common/services/CoreService.ts
index d17b0c93..35b61e84 100644
--- a/src/common/services/CoreService.ts
+++ b/src/common/services/CoreService.ts
@@ -15,12 +15,16 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({
export class CoreService implements ICoreService {
serviceBrand: any;
+ public isCursorInitialized: boolean = false;
+ public isCursorHidden: boolean = false;
public decPrivateModes: IDecPrivateModes;
private _onData = new EventEmitter();
public get onData(): IEvent { return this._onData.event; }
private _onUserInput = new EventEmitter();
public get onUserInput(): IEvent { return this._onUserInput.event; }
+ private _onBinary = new EventEmitter();
+ public get onBinary(): IEvent { return this._onBinary.event; }
constructor(
// TODO: Move this into a service
@@ -54,7 +58,15 @@ export class CoreService implements ICoreService {
}
// Fire onData API
- this._logService.debug('sending data', data);
+ this._logService.debug(`sending data "${data}"`, () => data.split('').map(e => e.charCodeAt(0)));
this._onData.fire(data);
}
+
+ public triggerBinaryEvent(data: string): void {
+ if (this._optionsService.options.disableStdin) {
+ return;
+ }
+ this._logService.debug(`sending binary "${data}"`, () => data.split('').map(e => e.charCodeAt(0)));
+ this._onBinary.fire(data);
+ }
}
diff --git a/src/common/services/LogService.ts b/src/common/services/LogService.ts
index 6740ad4a..4f48d8fe 100644
--- a/src/common/services/LogService.ts
+++ b/src/common/services/LogService.ts
@@ -5,12 +5,14 @@
import { ILogService, IOptionsService } from 'common/services/Services';
+type LogType = (message?: any, ...optionalParams: any[]) => void;
+
interface IConsole {
- log(message?: any, ...optionalParams: any[]): void;
- error(message?: any, ...optionalParams: any[]): void;
- info(message?: any, ...optionalParams: any[]): void;
- trace(message?: any, ...optionalParams: any[]): void;
- warn(message?: any, ...optionalParams: any[]): void;
+ log: LogType;
+ error: LogType;
+ info: LogType;
+ trace: LogType;
+ warn: LogType;
}
// console is available on both node.js and browser contexts but the common
@@ -56,27 +58,40 @@ export class LogService implements ILogService {
this._logLevel = optionsKeyToLogLevel[this._optionsService.options.logLevel];
}
+ private _evalLazyOptionalParams(optionalParams: any[]): void {
+ for (let i = 0; i < optionalParams.length; i++) {
+ if (typeof optionalParams[i] === 'function') {
+ optionalParams[i] = optionalParams[i]();
+ }
+ }
+ }
+
+ private _log(type: LogType, message: string, optionalParams: any[]): void {
+ this._evalLazyOptionalParams(optionalParams);
+ type.call(console, LOG_PREFIX + message, ...optionalParams);
+ }
+
debug(message: string, ...optionalParams: any[]): void {
if (this._logLevel <= LogLevel.DEBUG) {
- console.log.call(console, LOG_PREFIX + message, ...optionalParams);
+ this._log(console.log, message, optionalParams);
}
}
info(message: string, ...optionalParams: any[]): void {
if (this._logLevel <= LogLevel.INFO) {
- console.info.call(console, LOG_PREFIX + message, ...optionalParams);
+ this._log(console.info, message, optionalParams);
}
}
warn(message: string, ...optionalParams: any[]): void {
if (this._logLevel <= LogLevel.WARN) {
- console.warn.call(console, LOG_PREFIX + message, ...optionalParams);
+ this._log(console.warn, message, optionalParams);
}
}
error(message: string, ...optionalParams: any[]): void {
if (this._logLevel <= LogLevel.ERROR) {
- console.error.call(console, LOG_PREFIX + message, ...optionalParams);
+ this._log(console.error, message, optionalParams);
}
}
}
diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts
index 9a5d2151..b0b57694 100644
--- a/src/common/services/OptionsService.ts
+++ b/src/common/services/OptionsService.ts
@@ -12,7 +12,7 @@ import { clone } from 'common/Clone';
// This sound is released under the Creative Commons Attribution 3.0 Unported
// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been
// made, apart from the conversion to base64.
-export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
+export const DEFAULT_BELL_SOUND = 'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjMyLjEwNAAAAAAAAAAAAAAA//tQxAADB8AhSmxhIIEVCSiJrDCQBTcu3UrAIwUdkRgQbFAZC1CQEwTJ9mjRvBA4UOLD8nKVOWfh+UlK3z/177OXrfOdKl7pyn3Xf//WreyTRUoAWgBgkOAGbZHBgG1OF6zM82DWbZaUmMBptgQhGjsyYqc9ae9XFz280948NMBWInljyzsNRFLPWdnZGWrddDsjK1unuSrVN9jJsK8KuQtQCtMBjCEtImISdNKJOopIpBFpNSMbIHCSRpRR5iakjTiyzLhchUUBwCgyKiweBv/7UsQbg8isVNoMPMjAAAA0gAAABEVFGmgqK////9bP/6XCykxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq';
// TODO: Freeze?
export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({
@@ -23,6 +23,8 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({
bellSound: DEFAULT_BELL_SOUND,
bellStyle: 'none',
drawBoldTextInBrightColors: true,
+ fastScrollModifier: 'alt',
+ fastScrollSensitivity: 5,
fontFamily: 'courier-new, courier, monospace',
fontSize: 15,
fontWeight: 'normal',
@@ -31,9 +33,11 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({
letterSpacing: 0,
logLevel: 'info',
scrollback: 1000,
+ scrollSensitivity: 1,
screenReaderMode: false,
macOptionIsMeta: false,
macOptionClickForcesSelection: false,
+ minimumContrastRatio: 1,
disableStdin: false,
allowTransparency: false,
tabStopWidth: 8,
@@ -47,7 +51,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({
screenKeys: false,
cancelEvents: false,
useFlowControl: false,
- wordSeparator: ' ()[]{}\'"'
+ wordSeparator: ' ()[]{}\',:;"'
});
/**
@@ -113,12 +117,20 @@ export class OptionsService implements IOptionsService {
throw new Error(`${key} cannot be less than 1, value: ${value}`);
}
break;
+ case 'minimumContrastRatio':
+ value = Math.max(1, Math.min(21, Math.round(value * 10) / 10));
case 'scrollback':
value = Math.min(value, 4294967295);
if (value < 0) {
throw new Error(`${key} cannot be less than 0, value: ${value}`);
}
break;
+ case 'fastScrollSensitivity':
+ case 'scrollSensitivity':
+ if (value <= 0) {
+ throw new Error(`${key} cannot be less than or equal to 0, value: ${value}`);
+ }
+ break;
}
return value;
}
diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts
index 1af55e9a..b90db299 100644
--- a/src/common/services/Services.ts
+++ b/src/common/services/Services.ts
@@ -5,7 +5,7 @@
import { IEvent } from 'common/EventEmitter';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
-import { IDecPrivateModes } from 'common/Types';
+import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType } from 'common/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
export const IBufferService = createDecorator('BufferService');
@@ -23,14 +23,52 @@ export interface IBufferService {
reset(): void;
}
+export const ICoreMouseService = createDecorator('CoreMouseService');
+export interface ICoreMouseService {
+ activeProtocol: string;
+ activeEncoding: string;
+ addProtocol(name: string, protocol: ICoreMouseProtocol): void;
+ addEncoding(name: string, encoding: CoreMouseEncoding): void;
+ reset(): void;
+
+ /**
+ * Triggers a mouse event to be sent.
+ *
+ * Returns true if the event passed all protocol restrictions and a report
+ * was sent, otherwise false. The return value may be used to decide whether
+ * the default event action in the bowser component should be omitted.
+ *
+ * Note: The method will change values of the given event object
+ * to fullfill protocol and encoding restrictions.
+ */
+ triggerMouseEvent(event: ICoreMouseEvent): boolean;
+
+ /**
+ * Event to announce changes in mouse tracking.
+ */
+ onProtocolChange: IEvent;
+
+ /**
+ * Human readable version of mouse events.
+ */
+ explainEvents(events: CoreMouseEventType): {[event: string]: boolean};
+}
+
export const ICoreService = createDecorator('CoreService');
export interface ICoreService {
serviceBrand: any;
+ /**
+ * Initially the cursor will not be visible until the first time the terminal
+ * is focused.
+ */
+ isCursorInitialized: boolean;
+ isCursorHidden: boolean;
readonly decPrivateModes: IDecPrivateModes;
readonly onData: IEvent;
readonly onUserInput: IEvent;
+ readonly onBinary: IEvent;
reset(): void;
@@ -41,8 +79,14 @@ export interface ICoreService {
* resulting from parsing incoming data). When true this will also:
* - Scroll to the bottom of the buffer.s
* - Fire the `onUserInput` event (so selection can be cleared).
- */
+ */
triggerDataEvent(data: string, wasUserInput?: boolean): void;
+
+ /**
+ * Triggers the onBinary event in the public API.
+ * @param data The data that is being emitted.
+ */
+ triggerBinaryEvent(data: string): void;
}
export const IDirtyRowService = createDecorator('DirtyRowService');
@@ -149,6 +193,8 @@ export interface IPartialTerminalOptions {
cursorStyle?: 'block' | 'underline' | 'bar';
disableStdin?: boolean;
drawBoldTextInBrightColors?: boolean;
+ fastScrollModifier?: 'alt' | 'ctrl' | 'shift';
+ fastScrollSensitivity?: number;
fontSize?: number;
fontFamily?: string;
fontWeight?: FontWeight;
@@ -163,6 +209,7 @@ export interface IPartialTerminalOptions {
rows?: number;
screenReaderMode?: boolean;
scrollback?: number;
+ scrollSensitivity?: number;
tabStopWidth?: number;
theme?: ITheme;
windowsMode?: boolean;
@@ -178,6 +225,8 @@ export interface ITerminalOptions {
cursorStyle: 'block' | 'underline' | 'bar';
disableStdin: boolean;
drawBoldTextInBrightColors: boolean;
+ fastScrollModifier: 'alt' | 'ctrl' | 'shift' | undefined;
+ fastScrollSensitivity: number;
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
@@ -187,11 +236,13 @@ export interface ITerminalOptions {
logLevel: LogLevel;
macOptionIsMeta: boolean;
macOptionClickForcesSelection: boolean;
+ minimumContrastRatio: number;
rendererType: RendererType;
rightClickSelectsWord: boolean;
rows: number;
screenReaderMode: boolean;
scrollback: number;
+ scrollSensitivity: number;
tabStopWidth: number;
theme: ITheme;
windowsMode: boolean;
diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts
index 0cfd0560..22776d0a 100644
--- a/src/public/Terminal.ts
+++ b/src/public/Terminal.ts
@@ -3,7 +3,7 @@
* @license MIT
*/
-import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm';
+import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier } from 'xterm';
import { ITerminal } from '../Types';
import { IBufferLine } from 'common/Types';
import { IBuffer } from 'common/buffer/Types';
@@ -18,6 +18,7 @@ import { IParams } from 'common/parser/Types';
export class Terminal implements ITerminalApi {
private _core: ITerminal;
private _addonManager: AddonManager;
+ private _parser: IParser;
constructor(options?: ITerminalOptions) {
this._core = new TerminalCore(options);
@@ -28,14 +29,21 @@ export class Terminal implements ITerminalApi {
public get onLineFeed(): IEvent { return this._core.onLineFeed; }
public get onSelectionChange(): IEvent { return this._core.onSelectionChange; }
public get onData(): IEvent { return this._core.onData; }
+ public get onBinary(): IEvent { return this._core.onBinary; }
public get onTitleChange(): IEvent { return this._core.onTitleChange; }
public get onScroll(): IEvent { return this._core.onScroll; }
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
- public get element(): HTMLElement { return this._core.element; }
- public get textarea(): HTMLTextAreaElement { return this._core.textarea; }
+ public get element(): HTMLElement | undefined { return this._core.element; }
+ public get parser(): IParser {
+ if (!this._parser) {
+ this._parser = new ParserApi(this._core);
+ }
+ return this._parser;
+ }
+ public get textarea(): HTMLTextAreaElement | undefined { return this._core.textarea; }
public get rows(): number { return this._core.rows; }
public get cols(): number { return this._core.cols; }
public get buffer(): IBufferApi { return new BufferApiView(this._core.buffer); }
@@ -50,21 +58,12 @@ export class Terminal implements ITerminalApi {
this._verifyIntegers(columns, rows);
this._core.resize(columns, rows);
}
- public writeln(data: string): void {
- this._core.writeln(data);
- }
public open(parent: HTMLElement): void {
this._core.open(parent);
}
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
}
- public addCsiHandler(flag: string, callback: (params: (number | number[])[], collect: string) => boolean): IDisposable {
- return this._core.addCsiHandler(flag, (params: IParams, collect: string) => callback(params.toArray(), collect));
- }
- public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
- return this._core.addOscHandler(ident, callback);
- }
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number {
return this._core.registerLinkMatcher(regex, handler, options);
}
@@ -129,11 +128,18 @@ export class Terminal implements ITerminalApi {
public clear(): void {
this._core.clear();
}
- public write(data: string): void {
- this._core.write(data);
+ public write(data: string | Uint8Array, callback?: () => void): void {
+ this._core.write(data, callback);
}
- public writeUtf8(data: Uint8Array): void {
- this._core.writeUtf8(data);
+ public writeUtf8(data: Uint8Array, callback?: () => void): void {
+ this._core.write(data, callback);
+ }
+ public writeln(data: string | Uint8Array, callback?: () => void): void {
+ this._core.write(data);
+ this._core.write('\r\n', callback);
+ }
+ public paste(data: string): void {
+ this._core.paste(data);
}
public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string;
public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean;
@@ -166,9 +172,6 @@ export class Terminal implements ITerminalApi {
public reset(): void {
this._core.reset();
}
- public static applyAddon(addon: any): void {
- addon.apply(Terminal);
- }
public loadAddon(addon: ITerminalAddon): void {
return this._addonManager.loadAddon(this, addon);
}
@@ -178,8 +181,8 @@ export class Terminal implements ITerminalApi {
private _verifyIntegers(...values: number[]): void {
values.forEach(value => {
- if (value % 1 !== 0) {
- throw new Error('This API does not accept floating point numbers');
+ if (value === Infinity || isNaN(value) || value % 1 !== 0) {
+ throw new Error('This API only accepts integers');
}
});
}
@@ -276,3 +279,20 @@ class BufferCellApiView implements IBufferCellApi {
return (this.cell.bg & COLOR_MASK) === (other.cell.bg & COLOR_MASK);
}
}
+
+class ParserApi implements IParser {
+ constructor(private _core: ITerminal) {}
+
+ public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
+ return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray()));
+ }
+ public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
+ return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));
+ }
+ public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
+ return this._core.addEscHandler(id, handler);
+ }
+ public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
+ return this._core.addOscHandler(ident, callback);
+ }
+}
diff --git a/test/api/CharWidth.api.ts b/test/api/CharWidth.api.ts
index 035208cc..39f0bfe7 100644
--- a/test/api/CharWidth.api.ts
+++ b/test/api/CharWidth.api.ts
@@ -4,8 +4,8 @@
*/
import * as puppeteer from 'puppeteer';
-import { assert } from 'chai';
import { ITerminalOptions } from 'xterm';
+import { pollFor } from './TestUtils';
const APP = 'http://127.0.0.1:3000/test';
@@ -15,13 +15,10 @@ const width = 800;
const height = 600;
describe('CharWidth Integration Tests', function(): void {
- this.timeout(20000);
-
before(async function(): Promise {
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
- slowMo: 80,
- args: [`--window-size=${width},${height}`]
+ args: [`--window-size=${width},${height}`, `--no-sandbox`]
});
page = (await browser.pages())[0];
await page.setViewport({ width, height });
@@ -41,37 +38,37 @@ describe('CharWidth Integration Tests', function(): void {
it('ASCII chars', async function(): Promise {
const input = 'This is just ASCII text.#';
await page.evaluate(`window.term.write('${input}')`);
- assert.equal(25, await sumWidths(0, 1, '#'));
+ await pollFor(page, () => sumWidths(0, 1, '#'), 25);
});
it('combining chars', async function(): Promise {
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301#';
await page.evaluate(`window.term.write('${input}')`);
- assert.equal(10, await sumWidths(0, 1, '#'));
+ await pollFor(page, () => sumWidths(0, 1, '#'), 10);
});
it('surrogate chars', async function(): Promise {
const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞#';
await page.evaluate(`window.term.write('${input}')`);
- assert.equal(28, await sumWidths(0, 1, '#'));
+ await pollFor(page, () => sumWidths(0, 1, '#'), 28);
});
it('surrogate combining chars', async function(): Promise {
const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301#';
await page.evaluate(`window.term.write('${input}')`);
- assert.equal(12, await sumWidths(0, 1, '#'));
+ await pollFor(page, () => sumWidths(0, 1, '#'), 12);
});
it('fullwidth chars', async function(): Promise {
const input = '1234567890#';
await page.evaluate(`window.term.write('${input}')`);
- assert.equal(21, await sumWidths(0, 1, '#'));
+ await pollFor(page, () => sumWidths(0, 1, '#'), 21);
});
it('fullwidth chars offset 1', async function(): Promise {
const input = 'a1234567890#';
await page.evaluate(`window.term.write('${input}')`);
- assert.equal(22, await sumWidths(0, 1, '#'));
+ await pollFor(page, () => sumWidths(0, 1, '#'), 22);
});
// TODO: multiline tests once #1685 is resolved
diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts
index dfd16ddb..968a9e0b 100644
--- a/test/api/InputHandler.api.ts
+++ b/test/api/InputHandler.api.ts
@@ -6,6 +6,7 @@
import * as puppeteer from 'puppeteer';
import { assert } from 'chai';
import { ITerminalOptions } from 'xterm';
+import { pollFor } from './TestUtils';
const APP = 'http://127.0.0.1:3000/test';
@@ -15,13 +16,10 @@ const width = 800;
const height = 600;
describe('InputHandler Integration Tests', function(): void {
- this.timeout(20000);
-
before(async function(): Promise {
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
- slowMo: 80,
- args: [`--window-size=${width},${height}`]
+ args: [`--window-size=${width},${height}`, `--no-sandbox`]
});
page = (await browser.pages())[0];
await page.setViewport({ width, height });
@@ -45,7 +43,7 @@ describe('InputHandler Integration Tests', function(): void {
// Explicit
window.term.write('bar\\x1b[3D\\x1b[4@')
`);
- assert.deepEqual(await getLinesAsArray(2), [' foo', ' bar']);
+ await pollFor(page, () => getLinesAsArray(2), [' foo', ' bar']);
});
it('CUU: Cursor Up Ps Times (default = 1) - CSI Ps A', async function(): Promise {
@@ -55,7 +53,7 @@ describe('InputHandler Integration Tests', function(): void {
// Explicit
window.term.write('\x1b[2Ab')
`);
- assert.deepEqual(await getLinesAsArray(4), ['', ' b', '', 'a']);
+ await pollFor(page, () => getLinesAsArray(4), ['', ' b', '', 'a']);
});
it('CUD: Cursor Down Ps Times (default = 1) - CSI Ps B', async function(): Promise {
@@ -65,7 +63,7 @@ describe('InputHandler Integration Tests', function(): void {
// Explicit
window.term.write('\x1b[2Bb')
`);
- assert.deepEqual(await getLinesAsArray(4), ['', 'a', '', ' b']);
+ await pollFor(page, () => getLinesAsArray(4), ['', 'a', '', ' b']);
});
it('CUF: Cursor Forward Ps Times (default = 1) - CSI Ps C', async function(): Promise {
@@ -75,7 +73,7 @@ describe('InputHandler Integration Tests', function(): void {
// Explicit
window.term.write('\x1b[2Cb')
`);
- assert.deepEqual(await getLinesAsArray(1), [' a b']);
+ await pollFor(page, () => getLinesAsArray(1), [' a b']);
});
it('CUB: Cursor Backward Ps Times (default = 1) - CSI Ps D', async function(): Promise {
@@ -85,7 +83,7 @@ describe('InputHandler Integration Tests', function(): void {
// Explicit
window.term.write('\x1b[2Db')
`);
- assert.deepEqual(await getLinesAsArray(1), ['fba']);
+ await pollFor(page, () => getLinesAsArray(1), ['fba']);
});
it('CNL: Cursor Next Line Ps Times (default = 1) - CSI Ps E', async function(): Promise {
@@ -95,7 +93,7 @@ describe('InputHandler Integration Tests', function(): void {
// Explicit
window.term.write('\x1b[2Eb')
`);
- assert.deepEqual(await getLinesAsArray(4), ['', 'a', '', 'b']);
+ await pollFor(page, () => getLinesAsArray(4), ['', 'a', '', 'b']);
});
it('CPL: Cursor Preceding Line Ps Times (default = 1) - CSI Ps F', async function(): Promise {
@@ -105,7 +103,7 @@ describe('InputHandler Integration Tests', function(): void {
// Explicit
window.term.write('\x1b[2Fb')
`);
- assert.deepEqual(await getLinesAsArray(5), ['', 'b', '', 'a', '']);
+ await pollFor(page, () => getLinesAsArray(5), ['', 'b', '', 'a', '']);
});
it('CHA: Cursor Character Absolute [column] (default = [row,1]) - CSI Ps G', async function(): Promise {
@@ -115,7 +113,7 @@ describe('InputHandler Integration Tests', function(): void {
// Explicit
window.term.write('\x1b[10Gb')
`);
- assert.deepEqual(await getLinesAsArray(1), ['aoo b']);
+ await pollFor(page, () => getLinesAsArray(1), ['aoo b']);
});
it('CUP: Cursor Position [row;column] (default = [1,1]) - CSI Ps ; Ps H', async function(): Promise {
@@ -125,7 +123,7 @@ describe('InputHandler Integration Tests', function(): void {
// Explicit
window.term.write('\x1b[3;3Hb')
`);
- assert.deepEqual(await getLinesAsArray(3), ['aoo', '', ' b']);
+ await pollFor(page, () => getLinesAsArray(3), ['aoo', '', ' b']);
});
it('CHT: Cursor Forward Tabulation Ps tab stops (default = 1) - CSI Ps I', async function(): Promise {
@@ -135,7 +133,7 @@ describe('InputHandler Integration Tests', function(): void {
// Explicit
window.term.write('\\n\\r\x1b[2Ib')
`);
- assert.deepEqual(await getLinesAsArray(2), [' a', ' b']);
+ await pollFor(page, () => getLinesAsArray(2), [' a', ' b']);
});
it('ED: Erase in Display, VT100 - CSI Ps J', async function(): Promise {
@@ -145,26 +143,26 @@ describe('InputHandler Integration Tests', function(): void {
window.term.resize(5, 5);
window.term.write('${fixture}\x1b[J')
`);
- assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']);
+ await pollFor(page, () => getLinesAsArray(3), ['abc', 'd', '']);
await page.evaluate(`
// 0: Erase Below
window.term.reset()
window.term.write('${fixture}\x1b[0J')
`);
- assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']);
+ await pollFor(page, () => getLinesAsArray(3), ['abc', 'd', '']);
await page.evaluate(`
// 1: Erase Above
window.term.reset()
window.term.write('${fixture}\x1b[1J')
`);
- assert.deepEqual(await getLinesAsArray(3), ['', ' f', 'ghi']);
+ await pollFor(page, () => getLinesAsArray(3), ['', ' f', 'ghi']);
await page.evaluate(`
// 2: Erase Saved Lines (scrollback)
window.term.reset()
window.term.write('1\\n2\\n3\\n4\\n5${fixture}\x1b[3J')
`);
- assert.equal(await page.evaluate(`window.term.buffer.length`), 5);
- assert.deepEqual(await getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']);
+ await pollFor(page, () => page.evaluate(`window.term.buffer.length`), 5);
+ await pollFor(page, () => getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']);
});
it('DECSED: Erase in Display, VT220 - CSI ? Ps J', async function(): Promise {
@@ -174,26 +172,56 @@ describe('InputHandler Integration Tests', function(): void {
window.term.resize(5, 5);
window.term.write('${fixture}\x1b[?J')
`);
- assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']);
+ await pollFor(page, () => getLinesAsArray(3), ['abc', 'd', '']);
await page.evaluate(`
// 0: Erase Below
window.term.reset()
window.term.write('${fixture}\x1b[?0J')
`);
- assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']);
+ await pollFor(page, () => getLinesAsArray(3), ['abc', 'd', '']);
await page.evaluate(`
// 1: Erase Above
window.term.reset()
window.term.write('${fixture}\x1b[?1J')
`);
- assert.deepEqual(await getLinesAsArray(3), ['', ' f', 'ghi']);
+ await pollFor(page, () => getLinesAsArray(3), ['', ' f', 'ghi']);
await page.evaluate(`
// 2: Erase Saved Lines (scrollback)
window.term.reset()
window.term.write('1\\n2\\n3\\n4\\n5${fixture}\x1b[?3J')
`);
- assert.equal(await page.evaluate(`window.term.buffer.length`), 5);
- assert.deepEqual(await getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']);
+ await pollFor(page, () => page.evaluate(`window.term.buffer.length`), 5);
+ await pollFor(page, () => getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']);
+ });
+
+ it('IL: Insert Ps Line(s) (default = 1) - CSI Ps L', async function(): Promise {
+ await page.evaluate(`
+ // Default
+ window.term.write('foo\x1b[La')
+ // Explicit
+ window.term.write('\x1b[2Lb')
+ `);
+ await pollFor(page, () => getLinesAsArray(4), ['b', '', 'a', 'foo']);
+ });
+
+ it('DL: Delete Ps Line(s) (default = 1) - CSI Ps M', async function(): Promise {
+ await page.evaluate(`
+ // Default
+ window.term.write('a\\nb\x1b[1F\x1b[M')
+ // Explicit
+ window.term.write('\x1b[1Ed\\ne\\nf\x1b[2F\x1b[2M')
+ `);
+ await pollFor(page, () => getLinesAsArray(5), [' b', ' f', '', '', '']);
+ });
+
+ it('DCH: Delete Ps Character(s) (default = 1) - CSI Ps P', async function(): Promise {
+ await page.evaluate(`
+ // Default
+ window.term.write('abc\x1b[1;1H\x1b[P')
+ // Explicit
+ window.term.write('\\n\\rdef\x1b[2;1H\x1b[2P')
+ `);
+ await pollFor(page, () => getLinesAsArray(2), ['bc', 'f']);
});
describe('DSR: Device Status Report', () => {
@@ -202,31 +230,31 @@ describe('InputHandler Integration Tests', function(): void {
window.term.onData(e => window.result = e);
window.term.write('\\x1b[5n');
`);
- assert.equal(await page.evaluate(`window.result`), '\x1b[0n');
+ await pollFor(page, () => page.evaluate(`window.result`), '\x1b[0n');
});
it('Report Cursor Position (CPR) - CSI 6 n', async function(): Promise {
await page.evaluate(`window.term.write('\\n\\nfoo')`);
- assert.deepEqual(await page.evaluate(`
+ await pollFor(page, () => page.evaluate(`
[window.term.buffer.cursorY, window.term.buffer.cursorX]
`), [2, 3]);
await page.evaluate(`
window.term.onData(e => window.result = e);
window.term.write('\\x1b[6n');
`);
- assert.equal(await page.evaluate(`window.result`), '\x1b[3;4R');
+ await pollFor(page, () => page.evaluate(`window.result`), '\x1b[3;4R');
});
it('Report Cursor Position (DECXCPR) - CSI ? 6 n', async function(): Promise {
await page.evaluate(`window.term.write('\\n\\nfoo')`);
- assert.deepEqual(await page.evaluate(`
+ await pollFor(page, () => page.evaluate(`
[window.term.buffer.cursorY, window.term.buffer.cursorX]
`), [2, 3]);
await page.evaluate(`
window.term.onData(e => window.result = e);
window.term.write('\\x1b[?6n');
`);
- assert.equal(await page.evaluate(`window.result`), '\x1b[?3;4R');
+ await pollFor(page, () => page.evaluate(`window.result`), '\x1b[?3;4R');
});
});
@@ -247,22 +275,23 @@ describe('InputHandler Integration Tests', function(): void {
await page.mouse.up();
// Clear selection
await page.mouse.click((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 2);
- assert.equal(await page.evaluate(`window.term.getSelection().length`), 0);
+ await pollFor(page, () => page.evaluate(`window.term.getSelection().length`), 0);
// Enable mouse events
await page.evaluate(`window.term.write('\x1b[?1003h')`);
// Click and drag and ensure there is no selection
await page.mouse.click((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 2);
await page.mouse.down();
await page.mouse.move((coords.left + coords.right) / 2, (coords.top + coords.bottom) / 4);
- assert.equal(await page.evaluate(`window.term.getSelection().length`), 0, 'mouse events are on so there should be no selection');
+ // mouse events are on so there should be no selection
+ await pollFor(page, () => page.evaluate(`window.term.getSelection().length`), 0);
await page.mouse.up();
});
it('Pm = 2004, Set bracketed paste mode', async function(): Promise {
- assert.equal(await simulatePaste('foo'), 'foo');
+ await pollFor(page, () => simulatePaste('foo'), 'foo');
await page.evaluate(`window.term.write('\x1b[?2004h')`);
- assert.equal(await simulatePaste('bar'), '\x1b[200~bar\x1b[201~');
+ await pollFor(page, () => simulatePaste('bar'), '\x1b[200~bar\x1b[201~');
await page.evaluate(`window.term.write('\x1b[?2004l')`);
- assert.equal(await simulatePaste('baz'), 'baz');
+ await pollFor(page, () => simulatePaste('baz'), 'baz');
});
});
});
@@ -279,32 +308,32 @@ describe('InputHandler Integration Tests', function(): void {
window.term.writeln('');
window.term.write('#\x1b[5b');
`);
- assert.deepEqual(await getLinesAsArray(4), ['##', '##', '##', '######']);
- assert.deepEqual(await getCursor(), {col: 6, row: 3});
+ await pollFor(page, () => getLinesAsArray(4), ['##', '##', '##', '######']);
+ await pollFor(page, () => getCursor(), {col: 6, row: 3});
// should not repeat on fullwidth chars
await page.evaluate(`
window.term.reset();
window.term.write('¥\x1b[10b');
`);
- assert.deepEqual(await getLinesAsArray(1), ['¥']);
+ await pollFor(page, () => getLinesAsArray(1), ['¥']);
// should repeat only base char of combining
await page.evaluate(`
window.term.reset();
window.term.write('e\u0301\x1b[5b');
`);
- assert.deepEqual(await getLinesAsArray(1), ['e\u0301eeeee']);
+ await pollFor(page, () => getLinesAsArray(1), ['e\u0301eeeee']);
// should wrap correctly
await page.evaluate(`
window.term.reset();
window.term.write('#\x1b[15b');
`);
- assert.deepEqual(await getLinesAsArray(2), ['##########', '######']);
+ await pollFor(page, () => getLinesAsArray(2), ['##########', '######']);
await page.evaluate(`
window.term.reset();
window.term.write('\x1b[?7l'); // disable wrap around
window.term.write('#\x1b[15b');
`);
- assert.deepEqual(await getLinesAsArray(2), ['##########', '']);
+ await pollFor(page, () => getLinesAsArray(2), ['##########', '']);
// any successful sequence should reset REP
await page.evaluate(`
window.term.reset();
@@ -314,7 +343,7 @@ describe('InputHandler Integration Tests', function(): void {
window.term.writeln('');
window.term.write('abcdefg\x1b[3D\x1b[10b#\x1b[3b');
`);
- assert.deepEqual(await getLinesAsArray(3), ['#', ' #', 'abcd####']);
+ await pollFor(page, () => getLinesAsArray(3), ['#', ' #', 'abcd####']);
});
});
@@ -330,27 +359,10 @@ describe('InputHandler Integration Tests', function(): void {
window.term.resize(10, 4);
window.term.write('\\x1b[?47l\\x1b8');
`);
- assert.deepEqual(await getCursor(), {col: 1, row: 3});
+ await pollFor(page, () => getCursor(), {col: 1, row: 3});
});
});
});
-
- describe('addCsiHandler', () => {
- it('should call custom CSI handler with js array params', async () => {
- await page.evaluate(`
- window.term.reset();
- const _customCsiHandlerParams = [];
- const _customCsiHandler = window.term.addCsiHandler('m', (params, collect) => {
- _customCsiHandlerParams.push(params);
- return false;
- }, '');
- `);
- await page.evaluate(`
- window.term.write('\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams');
- `);
- assert.deepEqual(await page.evaluate(`(() => _customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]);
- });
- });
});
async function openTerminal(options: ITerminalOptions = {}): Promise {
diff --git a/test/api/MouseTracking.api.ts b/test/api/MouseTracking.api.ts
index 266edff7..7cae660f 100644
--- a/test/api/MouseTracking.api.ts
+++ b/test/api/MouseTracking.api.ts
@@ -4,15 +4,17 @@
*/
import * as puppeteer from 'puppeteer';
-import { assert } from 'chai';
import { ITerminalOptions } from 'xterm';
+import { pollFor, writeSync } from './TestUtils';
const APP = 'http://127.0.0.1:3000/test';
let browser: puppeteer.Browser;
let page: puppeteer.Page;
-const width = 1024;
-const height = 768;
+// adjusted to work inside devcontainer
+// see https://github.com/xtermjs/xterm.js/issues/2379
+const width = 1280;
+const height = 960;
// adjust terminal row/col size so we can test
// >80 up to 223 and >255
@@ -74,10 +76,6 @@ async function mouseMove(col: number, row: number): Promise {
const [xPixels, yPixels] = await cellPos(col, row);
return await page.mouse.move(xPixels, yPixels);
}
-async function mouseClick(col: number, row: number): Promise {
- const [xPixels, yPixels] = await cellPos(col, row);
- return await page.mouse.click(xPixels, yPixels);
-}
async function mouseDown(button: 'left' | 'right' | 'middle' | undefined): Promise {
return await page.mouse.down({button});
}
@@ -86,15 +84,25 @@ async function mouseUp(button: 'left' | 'right' | 'middle' | undefined): Promise
}
async function wheelUp(): Promise {
const self = (page.mouse as any);
- return await page.evaluate(`
- window.term.element.dispatchEvent(new WheelEvent('wheel', {clientX: ${self._x}, clientY: ${self._y}, deltaX: 0, deltaY: -10, modifiers: ${self._keyboard._modifiers}}));
- `);
+ return await self._client.send('Input.dispatchMouseEvent', {
+ type: 'mouseWheel',
+ x: self._x,
+ y: self._y,
+ deltaX: 0,
+ deltaY: -10,
+ modifiers: self._keyboard._modifiers
+ });
}
async function wheelDown(): Promise {
const self = (page.mouse as any);
- return await page.evaluate(`
- window.term.element.dispatchEvent(new WheelEvent('wheel', {clientX: ${self._x}, clientY: ${self._y}, deltaX: 0, deltaY: 10, modifiers: ${self._keyboard._modifiers}}));
- `);
+ return await self._client.send('Input.dispatchMouseEvent', {
+ type: 'mouseWheel',
+ x: self._x,
+ y: self._y,
+ deltaX: 0,
+ deltaY: 10,
+ modifiers: self._keyboard._modifiers
+ });
}
// button definitions
@@ -134,7 +142,7 @@ function evalButtonCode(code: number): any {
button |= 8;
}
if (code & 64) {
- button |= 4
+ button |= 4;
}
let actionS = 'press';
let buttonS = reverseButtons[button];
@@ -159,7 +167,6 @@ function parseReport(encoding: string, msg: number[]): {state: any; row: number;
let col: number;
// unpack msg
const report = String.fromCharCode.apply(null, msg);
- // console.log([report]);
// skip non mouse reports
if (!report || report[0] !== '\x1b') {
return report;
@@ -171,14 +178,6 @@ function parseReport(encoding: string, msg: number[]): {state: any; row: number;
col: report.charCodeAt(4) - 32,
row: report.charCodeAt(5) - 32
};
- case 'UTF8':
- // TODO: once the binary patch is in place,
- // use UTF8 byte check here
- return {
- state: evalButtonCode(report.charCodeAt(3) - 32),
- col: report.charCodeAt(4) - 32,
- row: report.charCodeAt(5) - 32
- };
case 'SGR':
sReport = report.slice(3, -1);
[buttonCode, col, row] = sReport.split(';').map(el => parseInt(el));
@@ -187,10 +186,6 @@ function parseReport(encoding: string, msg: number[]): {state: any; row: number;
state.action = 'release';
}
return {state, row, col};
- case 'URXVT':
- sReport = report.slice(2, -1);
- [buttonCode, col, row] = sReport.split(';').map(el => parseInt(el));
- return {state: evalButtonCode(buttonCode - 32), row: --row, col: --col}; // FIXME: remove -1 here when fixed!
default:
return {
state: evalButtonCode(report.charCodeAt(3) - 32),
@@ -203,22 +198,17 @@ function parseReport(encoding: string, msg: number[]): {state: any; row: number;
/**
* Mouse tracking tests.
*/
-describe('Mouse Tracking Tests', function(): void {
- this.timeout(30000);
-
- before(async function(): Promise {
+describe('Mouse Tracking Tests', () => {
+ before(async function(): Promise {
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
- slowMo: 80,
- args: [`--window-size=${width},${height}`]
+ args: [`--window-size=${width},${height}`, `--no-sandbox`]
});
page = (await browser.pages())[0];
await page.setViewport({ width, height });
});
- after(() => {
- browser.close();
- });
+ after(async () => browser.close());
beforeEach(async () => {
await page.goto(APP);
@@ -230,6 +220,7 @@ describe('Mouse Tracking Tests', function(): void {
await page.evaluate(`
window.calls = [];
window.term.onData(e => calls.push( Array.from(e).map(el => el.charCodeAt(0)) ));
+ window.term.onBinary(e => calls.push( Array.from(e).map(el => el.charCodeAt(0)) ));
window.term.setOption('fontSize', ${fontSize});
window.term.resize(${cols}, ${rows});
`);
@@ -247,30 +238,35 @@ describe('Mouse Tracking Tests', function(): void {
const encoding = 'DEFAULT';
await resetMouseModes();
await mouseMove(0, 0);
- await page.evaluate(`window.term.write('\x1b[?9h');`);
+ await writeSync(page, '\x1b[?9h');
// test at 0,0
await mouseDown('left');
- assert.deepEqual(await getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
// mouseup should not report
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), []);
// mousemove should not report
await mouseMove(50, 10);
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), []);
await mouseDown('left');
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
// test at max rows/cols
- // bug: we are capped at col 95 currently
- // fix: allow values up to 223, any bigger should drop to 0
- await mouseMove(cols - 1, rows - 1);
+ // capped at 223 (1-based)
+ await mouseMove(223 - 1, rows - 1);
await mouseDown('left');
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [{col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
+
+ // higher than 223 should not report at all
+ await mouseMove(257, rows - 1);
+ await mouseDown('left');
+ await mouseUp('left');
+ await pollFor(page, () => getReports(encoding), []);
// button press/move/release tests
// left button
@@ -279,7 +275,7 @@ describe('Mouse Tracking Tests', function(): void {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
// middle button
// bug: default action not cancelled (adds data to getReports from clipboard under X11)
// await mouseMove(43, 24);
@@ -287,7 +283,7 @@ describe('Mouse Tracking Tests', function(): void {
// await mouseDown('middle');
// await mouseMove(44, 24);
// await mouseUp('middle');
- // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
+ // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
// right button
// bug: default action not cancelled (popup shown)
await mouseMove(43, 24);
@@ -295,15 +291,15 @@ describe('Mouse Tracking Tests', function(): void {
await mouseDown('right');
await mouseMove(44, 24);
await mouseUp('right');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]);
// wheel
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await wheelUp();
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), []);
await wheelDown();
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), []);
// modifiers
// CTRL
@@ -315,7 +311,7 @@ describe('Mouse Tracking Tests', function(): void {
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Control');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
// ALT
@@ -327,7 +323,7 @@ describe('Mouse Tracking Tests', function(): void {
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Alt');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
// SHIFT
// note: caught by selection manager
@@ -342,175 +338,55 @@ describe('Mouse Tracking Tests', function(): void {
await wheelDown();
await page.keyboard.up('Shift');
if (noShift) {
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), []);
} else {
- assert.deepEqual(await getReports(encoding), [
+ await pollFor(page, () => getReports(encoding), [
{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}
]);
}
- /*
+
// all modifiers
- // bug: this is totally broken with wrong coords and messed up modifiers
+ // bug: Shift not working - reporting totally wrong coords and modifiers - selection manager again?
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await page.keyboard.down('Control');
await page.keyboard.down('Alt');
- await page.keyboard.down('Shift');
+ // await page.keyboard.down('Shift');
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Control');
await page.keyboard.up('Alt');
- await page.keyboard.up('Shift');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
- */
- });
- it('UTF8 encoding', async () => {
- const encoding = 'UTF8';
- await resetMouseModes();
- await mouseMove(0, 0);
- await page.evaluate(`window.term.write('\x1b[?9h\x1b[?1005h');`);
-
- // test at 0,0
- await mouseDown('left');
- assert.deepEqual(await getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
-
- // mouseup should not report
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), []);
-
- // mousemove should not report
- await mouseMove(50, 10);
- assert.deepEqual(await getReports(encoding), []);
- await mouseDown('left');
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
-
- // test at max rows/cols
- await mouseMove(cols - 1, rows - 1);
- await mouseDown('left');
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [{col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
-
- // button press/move/release tests
- // left button
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
- // middle button
- // bug: default action not cancelled (adds data to getReports from clipboard under X11)
- // await mouseMove(43, 24);
- // await getReports(encoding); // clear reports
- // await mouseDown('middle');
- // await mouseMove(44, 24);
- // await mouseUp('middle');
- // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
- // right button
- // bug: default action not cancelled (popup shown)
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await mouseDown('right');
- await mouseMove(44, 24);
- await mouseUp('right');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]);
-
- // wheel
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await wheelUp();
- assert.deepEqual(await getReports(encoding), []);
- await wheelDown();
- assert.deepEqual(await getReports(encoding), []);
-
- // modifiers
- // CTRL
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await page.keyboard.down('Control');
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- await page.keyboard.up('Control');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
-
-
- // ALT
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await page.keyboard.down('Alt');
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- await page.keyboard.up('Alt');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
-
- // SHIFT
- // note: caught by selection manager
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await page.keyboard.down('Shift'); // defaults to ShiftLeft
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- await page.keyboard.up('Shift');
- if (noShift) {
- assert.deepEqual(await getReports(encoding), []);
- } else {
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}
- ]);
- }
- /*
- // all modifiers
- // bug: this is totally broken with wrong coords and messed up modifiers
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await page.keyboard.down('Control');
- await page.keyboard.down('Alt');
- await page.keyboard.down('Shift');
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- await page.keyboard.up('Control');
- await page.keyboard.up('Alt');
- await page.keyboard.up('Shift');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
- */
+ // await page.keyboard.up('Shift');
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
});
it('SGR encoding', async () => {
const encoding = 'SGR';
await resetMouseModes();
await mouseMove(0, 0);
- await page.evaluate(`window.term.write('\x1b[?9h\x1b[?1006h');`);
+ await writeSync(page, '\x1b[?9h\x1b[?1006h');
// test at 0,0
await mouseDown('left');
- assert.deepEqual(await getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
// mouseup should not report
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), []);
// mousemove should not report
await mouseMove(50, 10);
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), []);
await mouseDown('left');
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
// test at max rows/cols
await mouseMove(cols - 1, rows - 1);
await mouseDown('left');
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [{col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
// button press/move/release tests
// left button
@@ -519,7 +395,7 @@ describe('Mouse Tracking Tests', function(): void {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
// middle button
// bug: default action not cancelled (adds data to getReports from clipboard under X11)
// await mouseMove(43, 24);
@@ -527,7 +403,7 @@ describe('Mouse Tracking Tests', function(): void {
// await mouseDown('middle');
// await mouseMove(44, 24);
// await mouseUp('middle');
- // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
+ // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
// right button
// bug: default action not cancelled (popup shown)
await mouseMove(43, 24);
@@ -535,15 +411,15 @@ describe('Mouse Tracking Tests', function(): void {
await mouseDown('right');
await mouseMove(44, 24);
await mouseUp('right');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]);
// wheel
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await wheelUp();
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), []);
await wheelDown();
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), []);
// modifiers
// CTRL
@@ -555,7 +431,7 @@ describe('Mouse Tracking Tests', function(): void {
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Control');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
// ALT
@@ -567,7 +443,7 @@ describe('Mouse Tracking Tests', function(): void {
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Alt');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
// SHIFT
// note: caught by selection manager
@@ -580,149 +456,28 @@ describe('Mouse Tracking Tests', function(): void {
await wheelDown();
await page.keyboard.up('Shift');
if (noShift) {
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), []);
} else {
- assert.deepEqual(await getReports(encoding), [
+ await pollFor(page, () => getReports(encoding), [
{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}
]);
}
- /*
+
// all modifiers
- // bug: this is totally broken with wrong coords and messed up modifiers
+ // bug: Shift not working - reporting totally wrong coords and modifiers - selection manager again?
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await page.keyboard.down('Control');
await page.keyboard.down('Alt');
- await page.keyboard.down('Shift');
+ // await page.keyboard.down('Shift');
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Control');
await page.keyboard.up('Alt');
- await page.keyboard.up('Shift');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
- */
- });
- it('URXVT encoding', async () => {
- // bug: always reports +1 for row/col (temp. fixed in parseReport to pass tests)
- const encoding = 'URXVT';
- await resetMouseModes();
- await mouseMove(0, 0);
- await page.evaluate(`window.term.write('\x1b[?9h\x1b[?1015h');`);
-
- // test at 0,0
- await mouseDown('left');
- assert.deepEqual(await getReports(encoding), [{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
-
- // mouseup should not report
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), []);
-
- // mousemove should not report
- await mouseMove(50, 10);
- assert.deepEqual(await getReports(encoding), []);
- await mouseDown('left');
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
-
- // test at max rows/cols
- await mouseMove(cols - 1, rows - 1);
- await mouseDown('left');
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [{col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
-
- // button press/move/release tests
- // left button
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
- // middle button
- // bug: default action not cancelled (adds data to getReports from clipboard under X11)
- // await mouseMove(43, 24);
- // await getReports(encoding); // clear reports
- // await mouseDown('middle');
- // await mouseMove(44, 24);
- // await mouseUp('middle');
- // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
- // right button
- // bug: default action not cancelled (popup shown)
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await mouseDown('right');
- await mouseMove(44, 24);
- await mouseUp('right');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}}]);
-
- // wheel
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await wheelUp();
- assert.deepEqual(await getReports(encoding), []);
- await wheelDown();
- assert.deepEqual(await getReports(encoding), []);
-
- // modifiers
- // CTRL
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await page.keyboard.down('Control');
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- await page.keyboard.up('Control');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
-
-
- // ALT
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await page.keyboard.down('Alt');
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- await page.keyboard.up('Alt');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
-
- // SHIFT
- // note: caught by selection manager
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await page.keyboard.down('Shift'); // defaults to ShiftLeft
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- await page.keyboard.up('Shift');
- if (noShift) {
- assert.deepEqual(await getReports(encoding), []);
- } else {
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}
- ]);
- }
- /*
- // all modifiers
- // bug: this is totally broken with wrong coords and messed up modifiers
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await page.keyboard.down('Control');
- await page.keyboard.down('Alt');
- await page.keyboard.down('Shift');
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- await page.keyboard.up('Control');
- await page.keyboard.up('Alt');
- await page.keyboard.up('Shift');
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
- */
+ // await page.keyboard.up('Shift');
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}]);
});
});
describe('DECSET 1000 (VT200 mouse)', () => {
@@ -737,53 +492,50 @@ describe('Mouse Tracking Tests', function(): void {
const encoding = 'DEFAULT';
await resetMouseModes();
await mouseMove(0, 0);
- await page.evaluate(`window.term.write('\x1b[?1000h');`);
+ await writeSync(page, '\x1b[?1000h');
// test at 0,0
- // bug: release is fired immediately - expected: only press event
await mouseDown('left');
- assert.deepEqual(await getReports(encoding), [
- {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
+ await pollFor(page, () => getReports(encoding), [
+ {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}
]);
// mouseup should report, encoding cannot report released button
- // bug: release already fired thus no event here - expected: release event
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), [
+ {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
+ ]);
// mousemove should not report
await mouseMove(50, 10);
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), []);
await mouseDown('left');
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
+ await pollFor(page, () => getReports(encoding), [
{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
{col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
]);
// test at max rows/cols
- // bug: we are capped at col 95 currently
- // fix: allow values up to 223, any bigger should drop to 0
- await mouseMove(cols - 1, rows - 1);
+ // capped at 223 (1-based)
+ await mouseMove(223 - 1, rows - 1);
await mouseDown('left');
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
- {col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 95, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
+ await pollFor(page, () => getReports(encoding), [
+ {col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
+ {col: 223, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
]);
// button press/move/release tests
// left button
- // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
+ await pollFor(page, () => getReports(encoding), [
{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
+ {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
]);
// middle button
// bug: default action not cancelled (adds data to getReports from clipboard under X11)
@@ -792,52 +544,44 @@ describe('Mouse Tracking Tests', function(): void {
// await mouseDown('middle');
// await mouseMove(44, 24);
// await mouseUp('middle');
- // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
+ // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
// right button
// bug: default action not cancelled (popup shown)
- // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await mouseDown('right');
await mouseMove(44, 24);
await mouseUp('right');
- assert.deepEqual(await getReports(encoding), [
+ await pollFor(page, () => getReports(encoding), [
{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
+ {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
]);
// wheel
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await wheelUp();
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
await wheelDown();
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
// modifiers
// CTRL
- // bug: totally broken - reports no modifier, reports wrong button and action, reports wrong coords for release
- // after fix: removed faulty reports below and uncomment lines
await mouseMove(43, 24);
await getReports(encoding); // clear reports
- // await page.keyboard.down('Control');
+ await page.keyboard.down('Control');
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
- // await page.keyboard.up('Control');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
- // expected
- // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}},
- // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}},
- // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}}
+ await page.keyboard.up('Control');
+ await pollFor(page, () => getReports(encoding), [
+ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}},
+ {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}},
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}}
]);
// ALT
- // bug: no modifier reported, release with wrong coords
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await page.keyboard.down('Alt');
@@ -846,14 +590,10 @@ describe('Mouse Tracking Tests', function(): void {
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Alt');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
- // expected
- // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}},
- // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}},
- // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}}
+ await pollFor(page, () => getReports(encoding), [
+ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}},
+ {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}},
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}}
]);
// SHIFT
@@ -868,230 +608,61 @@ describe('Mouse Tracking Tests', function(): void {
await wheelDown();
await page.keyboard.up('Shift');
if (noShift) {
- assert.deepEqual(await getReports(encoding), [
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
+ await pollFor(page, () => getReports(encoding), [
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}}
]);
} else {
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
+ await pollFor(page, () => getReports(encoding), [
+ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}},
+ {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}},
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}}
]);
}
- /*
// all modifiers
- // bug: this is totally broken with wrong coords and messed up modifiers
+ // bug: Shift not working - selection manager?
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await page.keyboard.down('Control');
await page.keyboard.down('Alt');
- await page.keyboard.down('Shift');
+ // await page.keyboard.down('Shift');
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Control');
await page.keyboard.up('Alt');
- await page.keyboard.up('Shift');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: true, meta: true}}},
- {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: true, meta: true}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: true, meta: true}}}
+ // await page.keyboard.up('Shift');
+ await pollFor(page, () => getReports(encoding), [
+ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}},
+ {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}},
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}}
]);
- */
- });
- it('UTF8 encoding', async () => {
- const encoding = 'UTF8';
- await resetMouseModes();
- await mouseMove(0, 0);
- await page.evaluate(`window.term.write('\x1b[?1000h\x1b[?1005h');`);
-
- // test at 0,0
- // bug: release is fired immediately - expected: only press event
- await mouseDown('left');
- assert.deepEqual(await getReports(encoding), [
- {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
- ]);
-
- // mouseup should report, encoding cannot report released button
- // bug: release already fired thus no event here - expected: release event
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), []);
-
- // mousemove should not report
- await mouseMove(50, 10);
- assert.deepEqual(await getReports(encoding), []);
- await mouseDown('left');
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
- {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
- ]);
-
- // test at max rows/cols
- await mouseMove(cols - 1, rows - 1);
- await mouseDown('left');
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
- {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
- ]);
-
- // button press/move/release tests
- // left button
- // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
- ]);
- // middle button
- // bug: default action not cancelled (adds data to getReports from clipboard under X11)
- // await mouseMove(43, 24);
- // await getReports(encoding); // clear reports
- // await mouseDown('middle');
- // await mouseMove(44, 24);
- // await mouseUp('middle');
- // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
- // right button
- // bug: default action not cancelled (popup shown)
- // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await mouseDown('right');
- await mouseMove(44, 24);
- await mouseUp('right');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
- ]);
-
- // wheel
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await wheelUp();
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
- await wheelDown();
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
-
- // modifiers
- // CTRL
- // bug: totally broken - reports no modifier, reports wrong button and action, reports wrong coords for release
- // after fix: removed faulty reports below and uncomment lines
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- // await page.keyboard.down('Control');
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- // await page.keyboard.up('Control');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
- // expected
- // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}},
- // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}},
- // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}}
- ]);
-
- // ALT
- // bug: no modifier reported, release with wrong coords
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await page.keyboard.down('Alt');
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- await page.keyboard.up('Alt');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
- // expected
- // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}},
- // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}},
- // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}}
- ]);
-
- // SHIFT
- // note: press/release caught by selection manager
- // bug: modifier not reported for passed events
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await page.keyboard.down('Shift'); // defaults to ShiftLeft
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- await page.keyboard.up('Shift');
- if (noShift) {
- assert.deepEqual(await getReports(encoding), [
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
- ]);
- } else {
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
- ]);
- }
-
- /*
- // all modifiers
- // bug: this is totally broken with wrong coords and messed up modifiers
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await page.keyboard.down('Control');
- await page.keyboard.down('Alt');
- await page.keyboard.down('Shift');
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- await page.keyboard.up('Control');
- await page.keyboard.up('Alt');
- await page.keyboard.up('Shift');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: true, meta: true}}},
- {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: true, meta: true}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: true, meta: true}}}
- ]);
- */
});
it('SGR encoding', async () => {
const encoding = 'SGR';
await resetMouseModes();
await mouseMove(0, 0);
- await page.evaluate(`window.term.write('\x1b[?1000h\x1b[?1006h');`);
+ await writeSync(page, '\x1b[?1000h\x1b[?1006h');
// test at 0,0
- // bug: release is fired immediately - expected: only press event
await mouseDown('left');
- assert.deepEqual(await getReports(encoding), [
- {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 1, row: 1, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}
+ await pollFor(page, () => getReports(encoding), [
+ {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}
]);
// mouseup should report
- // bug: release already fired thus no event here - expected: release event
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), [
+ {col: 1, row: 1, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}
+ ]);
// mousemove should not report
await mouseMove(50, 10);
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), []);
await mouseDown('left');
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
+ await pollFor(page, () => getReports(encoding), [
{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
{col: 51, row: 11, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}
]);
@@ -1100,22 +671,21 @@ describe('Mouse Tracking Tests', function(): void {
await mouseMove(cols - 1, rows - 1);
await mouseDown('left');
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
+ await pollFor(page, () => getReports(encoding), [
{col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
{col: cols, row: rows, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}
]);
// button press/move/release tests
// left button
- // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
+ await pollFor(page, () => getReports(encoding), [
{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}
+ {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}
]);
// middle button
// bug: default action not cancelled (adds data to getReports from clipboard under X11)
@@ -1124,53 +694,44 @@ describe('Mouse Tracking Tests', function(): void {
// await mouseDown('middle');
// await mouseMove(44, 24);
// await mouseUp('middle');
- // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
+ // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
// right button
// bug: default action not cancelled (popup shown)
- // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45
- // bug: release reports wrong button
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await mouseDown('right');
await mouseMove(44, 24);
await mouseUp('right');
- assert.deepEqual(await getReports(encoding), [
+ await pollFor(page, () => getReports(encoding), [
{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}}
+ {col: 45, row: 25, state: {action: 'release', button: 'right', modifier: {control: false, shift: false, meta: false}}}
]);
// wheel
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await wheelUp();
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
await wheelDown();
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
// modifiers
// CTRL
- // bug: totally broken - reports no modifier, reports wrong button and action, reports wrong coords for release
- // after fix: removed faulty reports below and uncomment lines
await mouseMove(43, 24);
await getReports(encoding); // clear reports
- // await page.keyboard.down('Control');
+ await page.keyboard.down('Control');
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
- // await page.keyboard.up('Control');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
- // expected
- // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}},
- // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}},
- // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}}
+ await page.keyboard.up('Control');
+ await pollFor(page, () => getReports(encoding), [
+ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}},
+ {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: false, meta: false}}},
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}}
]);
// ALT
- // bug: no modifier reported, release with wrong coords
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await page.keyboard.down('Alt');
@@ -1179,19 +740,14 @@ describe('Mouse Tracking Tests', function(): void {
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Alt');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
- // expected
- // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}},
- // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}},
- // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}}
+ await pollFor(page, () => getReports(encoding), [
+ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}},
+ {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: true}}},
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}}
]);
// SHIFT
// note: press/release caught by selection manager
- // bug: modifier not reported for passed events
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await page.keyboard.down('Shift'); // defaults to ShiftLeft
@@ -1201,209 +757,40 @@ describe('Mouse Tracking Tests', function(): void {
await wheelDown();
await page.keyboard.up('Shift');
if (noShift) {
- assert.deepEqual(await getReports(encoding), [
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
+ await pollFor(page, () => getReports(encoding), [
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}}
]);
} else {
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
+ await pollFor(page, () => getReports(encoding), [
+ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}},
+ {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: false, shift: true, meta: false}}},
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}}
]);
}
- /*
// all modifiers
- // bug: this is totally broken with wrong coords and messed up modifiers
+ // bug: Shift not working - selection manager?
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await page.keyboard.down('Control');
await page.keyboard.down('Alt');
- await page.keyboard.down('Shift');
+ // await page.keyboard.down('Shift');
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Control');
await page.keyboard.up('Alt');
- await page.keyboard.up('Shift');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: true, meta: true}}},
- {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: true, meta: true}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: true, meta: true}}}
+ // await page.keyboard.up('Shift');
+ await pollFor(page, () => getReports(encoding), [
+ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}},
+ {col: 45, row: 25, state: {action: 'release', button: 'left', modifier: {control: true, shift: false, meta: true}}},
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}}
]);
- */
- });
- it('URXVT encoding', async () => {
- const encoding = 'URXVT';
- await resetMouseModes();
- await mouseMove(0, 0);
- await page.evaluate(`window.term.write('\x1b[?1000h\x1b[?1015h');`);
-
- // test at 0,0
- // bug: release is fired immediately - expected: only press event
- await mouseDown('left');
- assert.deepEqual(await getReports(encoding), [
- {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
- ]);
-
- // mouseup should report, encoding cannot report released button
- // bug: release already fired thus no event here - expected: release event
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), []);
-
- // mousemove should not report
- await mouseMove(50, 10);
- assert.deepEqual(await getReports(encoding), []);
- await mouseDown('left');
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
- {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
- ]);
-
- // test at max rows/cols
- await mouseMove(cols - 1, rows - 1);
- await mouseDown('left');
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
- {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
- ]);
-
- // button press/move/release tests
- // left button
- // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
- ]);
- // middle button
- // bug: default action not cancelled (adds data to getReports from clipboard under X11)
- // await mouseMove(43, 24);
- // await getReports(encoding); // clear reports
- // await mouseDown('middle');
- // await mouseMove(44, 24);
- // await mouseUp('middle');
- // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
- // right button
- // bug: default action not cancelled (popup shown)
- // bug: release is fired immediately thus with wrong coords - expected: col in release event should be 45
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await mouseDown('right');
- await mouseMove(44, 24);
- await mouseUp('right');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
- ]);
-
- // wheel
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await wheelUp();
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
- await wheelDown();
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
-
- // modifiers
- // CTRL
- // bug: totally broken - reports no modifier, reports wrong button and action, reports wrong coords for release
- // after fix: removed faulty reports below and uncomment lines
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- // await page.keyboard.down('Control');
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- // await page.keyboard.up('Control');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
- // expected
- // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}},
- // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}},
- // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}}
- ]);
-
- // ALT
- // bug: no modifier reported, release with wrong coords
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await page.keyboard.down('Alt');
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- await page.keyboard.up('Alt');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
- // expected
- // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}},
- // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}},
- // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}}
- ]);
-
- // SHIFT
- // note: press/release caught by selection manager
- // bug: modifier not reported for passed events
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await page.keyboard.down('Shift'); // defaults to ShiftLeft
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- await page.keyboard.up('Shift');
- if (noShift) {
- assert.deepEqual(await getReports(encoding), [
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
- ]);
- } else {
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 44, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
- ]);
- }
-
- /*
- // all modifiers
- // bug: this is totally broken with wrong coords and messed up modifiers
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await page.keyboard.down('Control');
- await page.keyboard.down('Alt');
- await page.keyboard.down('Shift');
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- await wheelDown();
- await page.keyboard.up('Control');
- await page.keyboard.up('Alt');
- await page.keyboard.up('Shift');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: true, meta: true}}},
- {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: true, meta: true}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: true, meta: true}}}
- ]);
- */
});
});
describe('DECSET 1002 (xterm with drag)', () => {
/**
- * VT200 protocol:
* - press and release events
* - wheel up/down
* - move only on press (drag)
@@ -1414,39 +801,38 @@ describe('Mouse Tracking Tests', function(): void {
const encoding = 'DEFAULT';
await resetMouseModes();
await mouseMove(0, 0);
- await page.evaluate(`window.term.write('\x1b[?1002h');`);
+ await writeSync(page, '\x1b[?1002h');
// test at 0,0
await mouseDown('left');
- assert.deepEqual(await getReports(encoding), [
+ await pollFor(page, () => getReports(encoding), [
{col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}
]);
// mouseup should report, encoding cannot report released button
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
+ await pollFor(page, () => getReports(encoding), [
{col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
]);
// mousemove should not report
await mouseMove(50, 10);
- assert.deepEqual(await getReports(encoding), []);
+ await pollFor(page, () => getReports(encoding), []);
await mouseDown('left');
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
+ await pollFor(page, () => getReports(encoding), [
{col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
{col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
]);
// test at max rows/cols
- // bug: we are capped at col 95 currently
- // fix: allow values up to 223, any bigger should drop to 0
- await mouseMove(cols - 1, rows - 1);
+ // capped at 223 (1-based)
+ await mouseMove(223 - 1, rows - 1);
await mouseDown('left');
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
- {col: 95, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 95, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
+ await pollFor(page, () => getReports(encoding), [
+ {col: 223, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
+ {col: 223, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
]);
// button press/move/release tests
@@ -1456,7 +842,7 @@ describe('Mouse Tracking Tests', function(): void {
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
+ await pollFor(page, () => getReports(encoding), [
{col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
{col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}},
{col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
@@ -1468,7 +854,7 @@ describe('Mouse Tracking Tests', function(): void {
// await mouseDown('middle');
// await mouseMove(44, 24);
// await mouseUp('middle');
- // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
+ // await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
// right button
// bug: default action not cancelled (popup shown)
await mouseMove(43, 24);
@@ -1476,7 +862,7 @@ describe('Mouse Tracking Tests', function(): void {
await mouseDown('right');
await mouseMove(44, 24);
await mouseUp('right');
- assert.deepEqual(await getReports(encoding), [
+ await pollFor(page, () => getReports(encoding), [
{col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}},
{col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}},
{col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
@@ -1486,36 +872,28 @@ describe('Mouse Tracking Tests', function(): void {
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await wheelUp();
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'up', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
await wheelDown();
- assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
+ await pollFor(page, () => getReports(encoding), [{col: 44, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}]);
// modifiers
// CTRL
- // bug: totally broken - reports no modifier, reports wrong button and action
- // after fix: removed faulty reports below and uncomment lines
await mouseMove(43, 24);
await getReports(encoding); // clear reports
- // await page.keyboard.down('Control');
+ await page.keyboard.down('Control');
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
- // await page.keyboard.up('Control');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
- // expected
- // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}},
- // {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}},
- // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}},
- // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}}
+ await page.keyboard.up('Control');
+ await pollFor(page, () => getReports(encoding), [
+ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}},
+ {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}},
+ {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}},
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: false}}}
]);
// ALT
- // bug: no modifier reported
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await page.keyboard.down('Alt');
@@ -1524,21 +902,15 @@ describe('Mouse Tracking Tests', function(): void {
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Alt');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
- // expected
- // {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}},
- // {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}},
- // {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}},
- // {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}}
+ await pollFor(page, () => getReports(encoding), [
+ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: true}}},
+ {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: true}}},
+ {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: true}}},
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: true}}}
]);
// SHIFT
// note: press/release/drag caught by selection manager
- // bug: modifier not reported for passed events
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await page.keyboard.down('Shift'); // defaults to ShiftLeft
@@ -1548,241 +920,64 @@ describe('Mouse Tracking Tests', function(): void {
await wheelDown();
await page.keyboard.up('Shift');
if (noShift) {
- assert.deepEqual(await getReports(encoding), [
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
+ await pollFor(page, () => getReports(encoding), [
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}}
]);
} else {
- // bug: completely messed up - wrong modifier, only partially reported
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: false, meta: false}}}
+ await pollFor(page, () => getReports(encoding), [
+ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: true, meta: false}}},
+ {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: true, meta: false}}},
+ {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: true, meta: false}}},
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: false, shift: true, meta: false}}}
]);
}
- /*
// all modifiers
- // bug: this is totally broken with wrong coords and messed up modifiers
+ // bug: Shift not working
await mouseMove(43, 24);
await getReports(encoding); // clear reports
await page.keyboard.down('Control');
await page.keyboard.down('Alt');
- await page.keyboard.down('Shift');
+ // await page.keyboard.down('Shift');
await mouseDown('left');
await mouseMove(44, 24);
await mouseUp('left');
await wheelDown();
await page.keyboard.up('Control');
await page.keyboard.up('Alt');
- await page.keyboard.up('Shift');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: true, meta: true}}},
- {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: true, meta: true}}},
- {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: true, meta: true}}},
- {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: true, meta: true}}}
+ // await page.keyboard.up('Shift');
+ await pollFor(page, () => getReports(encoding), [
+ {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: true, shift: false, meta: true}}},
+ {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: true, shift: false, meta: true}}},
+ {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: true, shift: false, meta: true}}},
+ {col: 45, row: 25, state: {action: 'down', button: 'wheel', modifier: {control: true, shift: false, meta: true}}}
]);
- */
- });
- it('UTF8 encoding', async () => {
- const encoding = 'UTF8';
- await resetMouseModes();
- await mouseMove(0, 0);
- await page.evaluate(`window.term.write('\x1b[?1002h\x1b[?1005h');`);
-
- // test at 0,0
- // bug: release is fired immediately
- await mouseDown('left');
- assert.deepEqual(await getReports(encoding), [
- {col: 1, row: 1, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}}
- ]);
-
- // mouseup should report, encoding cannot report released button
- // bug: release already fired thus no event here - expected: release event
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
- {col: 1, row: 1, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
- ]);
-
- // mousemove should not report
- await mouseMove(50, 10);
- assert.deepEqual(await getReports(encoding), []);
- await mouseDown('left');
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
- {col: 51, row: 11, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 51, row: 11, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
- ]);
-
- // test at max rows/cols
- await mouseMove(cols - 1, rows - 1);
- await mouseDown('left');
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
- {col: cols, row: rows, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: cols, row: rows, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
- ]);
-
- // button press/move/release tests
- // left button
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await mouseDown('left');
- await mouseMove(44, 24);
- await mouseUp('left');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'move', button: 'left', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'release', button: '', modifier: {control: false, shift: false, meta: false}}}
- ]);
- // middle button
- // bug: default action not cancelled (adds data to getReports from clipboard under X11)
- // await mouseMove(43, 24);
- // await getReports(encoding); // clear reports
- // await mouseDown('middle');
- // await mouseMove(44, 24);
- // await mouseUp('middle');
- // assert.deepEqual(await getReports(encoding), [{col: 44, row: 25, state: {action: 'press', button: 'middle', modifier: {control: false, shift: false, meta: false}}}]);
- // right button
- // bug: default action not cancelled (popup shown)
- await mouseMove(43, 24);
- await getReports(encoding); // clear reports
- await mouseDown('right');
- await mouseMove(44, 24);
- await mouseUp('right');
- assert.deepEqual(await getReports(encoding), [
- {col: 44, row: 25, state: {action: 'press', button: 'right', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'move', button: 'right', modifier: {control: false, shift: false, meta: false}}},
- {col: 45, row: 25, state: {action: 'release', button: '