mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge pull request #2422 from jerch/write_enhancement
write callback support
This commit is contained in:
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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<K extends keyof WebSocketEventMap>(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable {
|
||||
socket.addEventListener(type, handler);
|
||||
return {
|
||||
dispose: () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -111,12 +111,7 @@ async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
|
||||
}
|
||||
|
||||
async function writeSync(data: string): Promise<void> {
|
||||
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 {
|
||||
|
||||
@@ -145,12 +145,7 @@ async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
|
||||
}
|
||||
|
||||
async function writeSync(data: string): Promise<void> {
|
||||
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<number[]> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+4
-5
@@ -4,10 +4,9 @@ var os = require('os');
|
||||
var pty = require('node-pty');
|
||||
|
||||
/**
|
||||
* Whether to use UTF8 binary transport.
|
||||
* (Must also be switched in client.ts)
|
||||
* Whether to use binary transport.
|
||||
*/
|
||||
const USE_BINARY_UTF8 = false;
|
||||
const USE_BINARY = true;
|
||||
|
||||
|
||||
function startServer() {
|
||||
@@ -46,7 +45,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);
|
||||
@@ -108,7 +107,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 {
|
||||
|
||||
+7
-20
@@ -324,7 +324,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;
|
||||
@@ -334,30 +334,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 {
|
||||
|
||||
+37
-42
@@ -29,11 +29,6 @@ describe('Terminal', () => {
|
||||
(<any>term).renderer = new MockRenderer();
|
||||
term.viewport = new MockViewport();
|
||||
(<any>term)._compositionHelper = new MockCompositionHelper();
|
||||
// Force synchronous writes
|
||||
term.write = (data) => {
|
||||
term.writeBuffer.push(data);
|
||||
(<any>term)._innerWrite();
|
||||
};
|
||||
(<any>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('¥');
|
||||
|
||||
+13
-207
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
|
||||
Vendored
+2
-6
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+61
-13
@@ -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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
it('writeln with callback', async function(): Promise<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
@@ -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~']);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Vendored
+18
-10
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user