diff --git a/README.md b/README.md
index 2853696f..498278b8 100644
--- a/README.md
+++ b/README.md
@@ -32,8 +32,8 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
-
-
+
+
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 79b350e1..5a5c5d75 100644
--- a/addons/xterm-addon-attach/package.json
+++ b/addons/xterm-addon-attach/package.json
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-attach",
- "version": "0.1.0",
+ "version": "0.2.1",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -16,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..c5b2d858 100644
--- a/addons/xterm-addon-attach/src/AttachAddon.api.ts
+++ b/addons/xterm-addon-attach/src/AttachAddon.api.ts
@@ -54,7 +54,7 @@ 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 }))`);
+ 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');
server.close();
});
diff --git a/addons/xterm-addon-attach/src/AttachAddon.ts b/addons/xterm-addon-attach/src/AttachAddon.ts
index 9dc45ecb..117b2b58 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,17 +21,15 @@ 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)));
@@ -57,7 +53,7 @@ export class AttachAddon implements ITerminalAddon {
}
}
-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 77471c26..fa1bf852 100644
--- a/addons/xterm-addon-fit/package.json
+++ b/addons/xterm-addon-fit/package.json
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-fit",
- "version": "0.1.0",
+ "version": "0.2.1",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -16,6 +16,6 @@
"prepublishOnly": "npm run package"
},
"peerDependencies": {
- "xterm": "^3.14.0"
+ "xterm": "^4.0.0"
}
}
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/package.json b/addons/xterm-addon-search/package.json
index 6871c566..c2e14032 100644
--- a/addons/xterm-addon-search/package.json
+++ b/addons/xterm-addon-search/package.json
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-search",
- "version": "0.1.0",
+ "version": "0.2.1",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -16,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..14e12a8c 100644
--- a/addons/xterm-addon-search/src/SearchAddon.api.ts
+++ b/addons/xterm-addon-search/src/SearchAddon.api.ts
@@ -111,12 +111,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-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 2e390690..6167de74 100644
--- a/addons/xterm-addon-web-links/package.json
+++ b/addons/xterm-addon-web-links/package.json
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-web-links",
- "version": "0.1.0",
+ "version": "0.2.1",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -16,6 +16,6 @@
"prepublishOnly": "npm run package"
},
"peerDependencies": {
- "xterm": "^3.14.0"
+ "xterm": "^4.0.0"
}
}
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 0db5b204..9a01aea0 100644
--- a/addons/xterm-addon-webgl/package.json
+++ b/addons/xterm-addon-webgl/package.json
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-webgl",
- "version": "0.1.0",
+ "version": "0.2.1",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -16,6 +16,6 @@
"prepublishOnly": "npm run package"
},
"peerDependencies": {
- "xterm": "^3.14.0"
+ "xterm": "^4.0.0"
}
}
diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts
index b26790c4..66be22d9 100644
--- a/addons/xterm-addon-webgl/src/WebglRenderer.api.ts
+++ b/addons/xterm-addon-webgl/src/WebglRenderer.api.ts
@@ -145,12 +145,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 {
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index c784a611..26f0ca73 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -121,8 +121,7 @@ jobs:
displayName: 'Install Yarn'
- script: |
yarn
- BUILD_DIR=dist npm run build
displayName: 'Install dependencies and build'
- script: |
NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js
- displayName: 'Publish to npm'
+ displayName: 'Package and publish to npm'
diff --git a/bin/publish.js b/bin/publish.js
index 0610d116..d95ecc57 100644
--- a/bin/publish.js
+++ b/bin/publish.js
@@ -30,9 +30,11 @@ 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);
}
});
diff --git a/demo/client.ts b/demo/client.ts
index 040292e4..92bac731 100644
--- a/demo/client.ts
+++ b/demo/client.ts
@@ -167,14 +167,7 @@ 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}));
-
term._initialized = true;
}
diff --git a/demo/server.js b/demo/server.js
index 8955431b..9a45d06f 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();
@@ -32,9 +28,8 @@ function startServer() {
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) {
const env = Object.assign({}, process.env);
@@ -47,7 +42,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);
@@ -109,7 +104,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/tsconfig.json b/demo/tsconfig.json
index 2bf76f67..7a53302d 100644
--- a/demo/tsconfig.json
+++ b/demo/tsconfig.json
@@ -9,7 +9,8 @@
"xterm-addon-attach": ["../addons/xterm-addon-attach"],
"xterm-addon-fit": ["../addons/xterm-addon-fit"],
"xterm-addon-search": ["../addons/xterm-addon-search"],
- "xterm-addon-web-links": ["../addons/xterm-addon-web-links"]
+ "xterm-addon-web-links": ["../addons/xterm-addon-web-links"],
+ "xterm-addon-webgl": ["../addons/xterm-addon-webgl"]
}
},
"include": [
diff --git a/package.json b/package.json
index 68467a89..f52f7b79 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.0.0",
"main": "lib/xterm.js",
"style": "css/xterm.css",
"types": "typings/xterm.d.ts",
diff --git a/src/InputHandler.ts b/src/InputHandler.ts
index eb367868..06fa51d7 100644
--- a/src/InputHandler.ts
+++ b/src/InputHandler.ts
@@ -325,7 +325,7 @@ export class InputHandler extends Disposable implements IInputHandler {
super.dispose();
}
- public parse(data: string): void {
+ public parse(data: string | Uint8Array): void {
let buffer = this._bufferService.buffer;
const cursorStartX = buffer.x;
const cursorStartY = buffer.y;
@@ -335,30 +335,17 @@ export class InputHandler extends Disposable implements IInputHandler {
if (this._parseBuffer.length < data.length) {
this._parseBuffer = new Uint32Array(data.length);
}
- 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));
+ this._parser.parse(this._parseBuffer,
+ (typeof data === 'string')
+ ? this._stringDecoder.decode(data, this._parseBuffer)
+ : this._utf8Decoder.decode(data, this._parseBuffer)
+ );
buffer = this._bufferService.buffer;
if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) {
this._onCursorMove.fire();
}
+ this._terminal.refresh(this._dirtyRowService.start, this._dirtyRowService.end);
}
public print(data: Uint32Array, start: number, end: number): void {
diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts
index 59864f0a..667bf076 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();
});
@@ -195,7 +190,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);
@@ -244,7 +239,7 @@ describe('Terminal', () => {
assert.equal(e, '\x1b[200~foo\x1b[201~');
done();
});
- term.write('\x1b[?2004h');
+ term.writeSync('\x1b[?2004h');
term.paste('foo');
});
});
@@ -254,7 +249,7 @@ describe('Terminal', () => {
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;
});
@@ -289,7 +284,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;
});
@@ -312,7 +307,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', () => {
@@ -326,7 +321,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;
});
@@ -347,7 +342,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;
});
@@ -392,7 +387,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(() => {
@@ -737,7 +732,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);
@@ -751,7 +746,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('');
@@ -764,7 +759,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);
@@ -782,7 +777,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);
@@ -794,8 +789,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);
@@ -809,7 +804,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);
@@ -817,7 +812,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);
@@ -829,7 +824,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');
@@ -843,7 +838,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');
@@ -861,18 +856,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) {
@@ -893,7 +888,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)) {
@@ -918,7 +913,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)) {
@@ -942,7 +937,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) {
@@ -963,7 +958,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)) {
@@ -987,7 +982,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) {
@@ -1010,11 +1005,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');
@@ -1022,11 +1017,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('');
@@ -1035,16 +1030,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('¥');
diff --git a/src/Terminal.ts b/src/Terminal.ts
index d793abfc..63400bcb 100644
--- a/src/Terminal.ts
+++ b/src/Terminal.ts
@@ -62,25 +62,11 @@ import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions,
import { DirtyRowService } from 'common/services/DirtyRowService';
import { InstantiationService } from 'common/services/InstantiationService';
import { CoreMouseService } from 'common/services/CoreMouseService';
+import { WriteBuffer } from 'common/input/WriteBuffer';
// 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;
@@ -153,21 +139,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;
@@ -258,6 +231,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._setupOptionsListeners();
this._setup();
+
+ this._writeBuffer = new WriteBuffer(data => this._inputHandler.parse(data));
}
public dispose(): void {
@@ -306,13 +281,6 @@ 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
@@ -604,8 +572,8 @@ 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._colorManager.setTheme(this._theme);
@@ -1144,168 +1112,6 @@ 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);
}
@@ -1743,10 +1549,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
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();
@@ -1761,10 +1563,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
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
@@ -1795,6 +1593,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/TestUtils.test.ts b/src/TestUtils.test.ts
index ad19f721..a70f751c 100644
--- a/src/TestUtils.test.ts
+++ b/src/TestUtils.test.ts
@@ -19,10 +19,6 @@ 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); }
}
diff --git a/src/Types.d.ts b/src/Types.d.ts
index 13c65a4d..cb45d3b1 100644
--- a/src/Types.d.ts
+++ b/src/Types.d.ts
@@ -73,8 +73,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;
@@ -151,7 +150,6 @@ export interface IInputHandler {
export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
screenElement: HTMLElement;
browser: IBrowser;
- writeBuffer: string[];
cursorHidden: boolean;
cursorState: number;
buffer: IBuffer;
@@ -192,7 +190,6 @@ 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(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable;
@@ -218,8 +215,7 @@ 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;
diff --git a/src/browser/Linkifier.ts b/src/browser/Linkifier.ts
index a5454ac5..53a32aa7 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) {
diff --git a/src/common/input/Keyboard.ts b/src/common/input/Keyboard.ts
index 3cc0eb76..2f54add6 100644
--- a/src/common/input/Keyboard.ts
+++ b/src/common/input/Keyboard.ts
@@ -316,23 +316,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/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/services/CoreService.ts b/src/common/services/CoreService.ts
index d17b0c93..0e0ba609 100644
--- a/src/common/services/CoreService.ts
+++ b/src/common/services/CoreService.ts
@@ -54,7 +54,7 @@ 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);
}
}
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/public/Terminal.ts b/src/public/Terminal.ts
index 6d6cedca..b8a70ff7 100644
--- a/src/public/Terminal.ts
+++ b/src/public/Terminal.ts
@@ -55,9 +55,6 @@ 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);
}
@@ -128,11 +125,15 @@ 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);
diff --git a/test/api/InputHandler.api.ts b/test/api/InputHandler.api.ts
index 9ca0c943..05b16010 100644
--- a/test/api/InputHandler.api.ts
+++ b/test/api/InputHandler.api.ts
@@ -196,6 +196,36 @@ describe('InputHandler Integration Tests', function(): void {
assert.deepEqual(await 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')
+ `);
+ assert.deepEqual(await 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')
+ `);
+ assert.deepEqual(await 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')
+ `);
+ assert.deepEqual(await getLinesAsArray(2), ['bc', 'f']);
+ });
+
describe('DSR: Device Status Report', () => {
it('Status Report - CSI 5 n', async function(): Promise {
await page.evaluate(`
diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts
index b2157b1e..034b04c4 100644
--- a/test/api/Terminal.api.ts
+++ b/test/api/Terminal.api.ts
@@ -51,6 +51,44 @@ describe('API Integration Tests', function(): void {
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文');
});
+ it('write with callback', async function(): Promise {
+ await openTerminal();
+ await page.evaluate(`
+ window.term.write('foo', () => { window.__x = 'a'; });
+ window.term.write('bar', () => { window.__x += 'b'; });
+ window.term.write('文', () => { window.__x += 'c'; });
+ `);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文');
+ assert.equal(await page.evaluate(`window.__x`), 'abc');
+ });
+
+ it('write - bytes (UTF8)', async function(): Promise {
+ await openTerminal();
+ await page.evaluate(`
+ // foo
+ window.term.write(new Uint8Array([102, 111, 111]));
+ // bar
+ window.term.write(new Uint8Array([98, 97, 114]));
+ // 文
+ window.term.write(new Uint8Array([230, 150, 135]));
+ `);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文');
+ });
+
+ it('write - bytes (UTF8) with callback', async function(): Promise {
+ await openTerminal();
+ await page.evaluate(`
+ // foo
+ window.term.write(new Uint8Array([102, 111, 111]), () => { window.__x = 'A'; });
+ // bar
+ window.term.write(new Uint8Array([98, 97, 114]), () => { window.__x += 'B'; });
+ // 文
+ window.term.write(new Uint8Array([230, 150, 135]), () => { window.__x += 'C'; });
+ `);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文');
+ assert.equal(await page.evaluate(`window.__x`), 'ABC');
+ });
+
it('writeln', async function(): Promise {
await openTerminal();
await page.evaluate(`
@@ -63,17 +101,29 @@ describe('API Integration Tests', function(): void {
assert.equal(await page.evaluate(`window.term.buffer.getLine(2).translateToString(true)`), '文');
});
- it('writeUtf8', async function(): Promise {
+ it('writeln with callback', async function(): Promise {
await openTerminal();
await page.evaluate(`
- // foo
- window.term.writeUtf8(new Uint8Array([102, 111, 111]));
- // bar
- window.term.writeUtf8(new Uint8Array([98, 97, 114]));
- // 文
- window.term.writeUtf8(new Uint8Array([230, 150, 135]));
+ window.term.writeln('foo', () => { window.__x = '1'; });
+ window.term.writeln('bar', () => { window.__x += '2'; });
+ window.term.writeln('文', () => { window.__x += '3'; });
`);
- assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(1).translateToString(true)`), 'bar');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(2).translateToString(true)`), '文');
+ assert.equal(await page.evaluate(`window.__x`), '123');
+ });
+
+ it('writeln - bytes (UTF8)', async function(): Promise {
+ await openTerminal();
+ await page.evaluate(`
+ window.term.writeln(new Uint8Array([102, 111, 111]));
+ window.term.writeln(new Uint8Array([98, 97, 114]));
+ window.term.writeln(new Uint8Array([230, 150, 135]));
+ `);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(1).translateToString(true)`), 'bar');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(2).translateToString(true)`), '文');
});
it('paste', async function(): Promise {
@@ -83,11 +133,9 @@ describe('API Integration Tests', function(): void {
window.term.onData(e => calls.push(e));
window.term.paste('foo');
window.term.paste('\\r\\nfoo\\nbar\\r');
- window.term.write('\\x1b[?2004h');
- // TODO: Use promise/callback for write when we support that
- // Force sync write
- window.term._core._innerWrite();
- window.term.paste('foo');
+ window.term.write('\\x1b[?2004h', () => {
+ window.term.paste('foo');
+ });
`);
assert.deepEqual(await page.evaluate(`window.calls`), ['foo', '\rfoo\rbar\r', '\x1b[200~foo\x1b[201~']);
});
diff --git a/test/benchmark/Terminal.benchmark.ts b/test/benchmark/Terminal.benchmark.ts
index a0b8fd29..71a36cc7 100644
--- a/test/benchmark/Terminal.benchmark.ts
+++ b/test/benchmark/Terminal.benchmark.ts
@@ -9,17 +9,6 @@ import { spawn } from 'node-pty';
import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
import { Terminal } from 'Terminal';
-class TestTerminal extends Terminal {
- writeSync(data: string): void {
- this.writeBuffer.push(data);
- this._innerWrite();
- }
- writeSyncUtf8(data: Uint8Array): void {
- this.writeBufferUtf8.push(data);
- this._innerWriteUtf8();
- }
-}
-
perfContext('Terminal: ls -lR /usr/lib', () => {
let content = '';
let contentUtf8: Uint8Array;
@@ -56,9 +45,9 @@ perfContext('Terminal: ls -lR /usr/lib', () => {
});
perfContext('write', () => {
- let terminal: TestTerminal;
+ let terminal: Terminal;
before(() => {
- terminal = new TestTerminal({cols: 80, rows: 25, scrollback: 1000});
+ terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000});
});
new ThroughputRuntimeCase('', () => {
terminal.writeSync(content);
@@ -67,12 +56,12 @@ perfContext('Terminal: ls -lR /usr/lib', () => {
});
perfContext('writeUtf8', () => {
- let terminal: TestTerminal;
+ let terminal: Terminal;
before(() => {
- terminal = new TestTerminal({cols: 80, rows: 25, scrollback: 1000});
+ terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000});
});
new ThroughputRuntimeCase('', () => {
- terminal.writeSyncUtf8(contentUtf8);
+ terminal.writeSync(content);
return {payloadSize: contentUtf8.length};
}, {fork: false}).showAverageThroughput();
});
diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts
index cae54dc1..44ef66bd 100644
--- a/typings/xterm.d.ts
+++ b/typings/xterm.d.ts
@@ -650,24 +650,32 @@ declare module 'xterm' {
clear(): void;
/**
- * Writes text to the terminal.
- * @param data The text to write to the terminal.
+ * Write data to the terminal.
+ * @param data The data to write to the terminal. This can either be raw
+ * bytes given as Uint8Array from the pty or a string. Raw bytes will always
+ * be treated as UTF-8 encoded, string data as UTF-16.
+ * @param callback Optional callback that fires when the data was processed
+ * by the parser.
*/
- write(data: string): void;
+ write(data: string | Uint8Array, callback?: () => void): void;
/**
- * Writes text to the terminal, followed by a break line character (\n).
- * @param data The text to write to the terminal.
+ * Writes data to the terminal, followed by a break line character (\n).
+ * @param data The data to write to the terminal. This can either be raw
+ * bytes given as Uint8Array from the pty or a string. Raw bytes will always
+ * be treated as UTF-8 encoded, string data as UTF-16.
+ * @param callback Optional callback that fires when the data was processed
+ * by the parser.
*/
- writeln(data: string): void;
+ writeln(data: string | Uint8Array, callback?: () => void): void;
/**
- * Writes UTF8 data to the terminal. This has a slight performance advantage
- * over the string based write method due to lesser data conversions needed
- * on the way from the pty to xterm.js.
+ * Write UTF8 data to the terminal.
* @param data The data to write to the terminal.
+ * @param callback Optional callback when data was processed.
+ * @deprecated use `write` instead
*/
- writeUtf8(data: Uint8Array): void;
+ writeUtf8(data: Uint8Array, callback?: () => void): void;
/**
* Writes text to the terminal, performing the necessary transformations for pasted text.
@@ -804,7 +812,7 @@ declare module 'xterm' {
*/
export interface ITerminalAddon extends IDisposable {
/**
- * (EXPERIMENTAL) This is called when the addon is activated.
+ * This is called when the addon is activated.
*/
activate(terminal: Terminal): void;
}
diff --git a/webpack.config.js b/webpack.config.js
index 3f55dda3..a9a60241 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -35,7 +35,6 @@ module.exports = {
output: {
filename: 'xterm.js',
path: path.resolve('./lib'),
- library: 'Terminal',
libraryTarget: 'umd'
},
mode: 'production'