diff --git a/docker-compose.yml b/docker-compose.yml index 8e6a2f46b..6eefed896 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: volumes: - ./:/usr/src/app ports: - - ${XTERMJS_PORT:3000}:3000 + - ${XTERMJS_PORT:-3000}:3000 command: ["npm", "start"] watch: diff --git a/src/Buffer.ts b/src/Buffer.ts index d017aa7e8..5eea5d9a4 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -211,7 +211,7 @@ export class Buffer implements IBuffer { this.scrollBottom = newRows - 1; - if (this._hasScrollback) { + if (this._isReflowEnabled) { this._reflow(newCols, newRows); // Trim the end of the line off if cols shrunk @@ -226,6 +226,10 @@ export class Buffer implements IBuffer { this._rows = newRows; } + private get _isReflowEnabled(): boolean { + return this._hasScrollback && !(this._terminal as any).isWinptyCompatEnabled; + } + private _reflow(newCols: number, newRows: number): void { if (this._cols === newCols) { return; diff --git a/src/Terminal.ts b/src/Terminal.ts index 33fa3ca29..4a85758f0 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -64,10 +64,12 @@ const document = (typeof window !== 'undefined') ? window.document : null; const WRITE_BUFFER_PAUSE_THRESHOLD = 5; /** - * The number of writes to perform in a single batch before allowing the - * renderer to catch up with a 0ms setTimeout. + * 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_BATCH_SIZE = 300; +const WRITE_TIMEOUT_MS = 12; const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars const MINIMUM_ROWS = 1; @@ -1342,19 +1344,20 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } } - protected _innerWrite(): void { + protected _innerWrite(bufferOffset: number = 0): void { // Ensure the terminal isn't disposed if (this._isDisposed) { this.writeBuffer = []; } - const writeBatch = this.writeBuffer.splice(0, WRITE_BATCH_SIZE); - while (writeBatch.length > 0) { - const data = writeBatch.shift(); + 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 - // the writeBuffer is empty to allow more data to come in. - if (this._xoffSentToCatchUp && writeBatch.length === 0 && this.writeBuffer.length === 0) { + // we reached the end of the writeBuffer to allow more data to come in. + if (this._xoffSentToCatchUp && this.writeBuffer.length === bufferOffset) { this.handler(C0.DC1); this._xoffSentToCatchUp = false; } @@ -1372,12 +1375,17 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.updateRange(this.buffer.y); this.refresh(this._refreshStart, this._refreshEnd); + + if (Date.now() - startTime >= WRITE_TIMEOUT_MS) { + break; + } } - if (this.writeBuffer.length > 0) { + if (this.writeBuffer.length > bufferOffset) { // Allow renderer to catch up before processing the next batch - setTimeout(() => this._innerWrite(), 0); + setTimeout(() => this._innerWrite(bufferOffset), 0); } else { this._writeInProgress = false; + this.writeBuffer = []; } } diff --git a/src/addons/winptyCompat/winptyCompat.ts b/src/addons/winptyCompat/winptyCompat.ts index aec580edb..d162f4e92 100644 --- a/src/addons/winptyCompat/winptyCompat.ts +++ b/src/addons/winptyCompat/winptyCompat.ts @@ -19,6 +19,8 @@ export function winptyCompatInit(terminal: Terminal): void { return; } + (addonTerminal._core as any).isWinptyCompatEnabled = true; + // Winpty does not support wraparound mode which means that lines will never // be marked as wrapped. This causes issues for things like copying a line // retaining the wrapped new line characters or if consumers are listening diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 3e0b86430..a609d79ce 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -241,7 +241,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillText( charData[CHAR_DATA_CHAR_INDEX], x * this._scaledCellWidth + this._scaledCharLeft, - (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); } /** @@ -316,7 +316,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillText( chars, x * this._scaledCellWidth + this._scaledCharLeft, - (y + 0.5) * this._scaledCellHeight + this._scaledCharTop); + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); this._ctx.restore(); } diff --git a/src/ui/MouseZoneManager.ts b/src/ui/MouseZoneManager.ts index a232f5b94..790227239 100644 --- a/src/ui/MouseZoneManager.ts +++ b/src/ui/MouseZoneManager.ts @@ -23,6 +23,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { private _areZonesActive: boolean = false; private _mouseMoveListener: (e: MouseEvent) => any; + private _mouseLeaveListener: (e: MouseEvent) => any; private _clickListener: (e: MouseEvent) => any; private _tooltipTimeout: number = null; @@ -38,6 +39,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { // These events are expensive, only listen to it when mouse zones are active this._mouseMoveListener = e => this._onMouseMove(e); + this._mouseLeaveListener = e => this._onMouseLeave(e); this._clickListener = e => this._onClick(e); } @@ -89,6 +91,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { if (!this._areZonesActive) { this._areZonesActive = true; this._terminal.element.addEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.addEventListener('mouseleave', this._mouseLeaveListener); this._terminal.element.addEventListener('click', this._clickListener); } } @@ -97,6 +100,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { if (this._areZonesActive) { this._areZonesActive = false; this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener); + this._terminal.element.removeEventListener('mouseleave', this._mouseLeaveListener); this._terminal.element.removeEventListener('click', this._clickListener); } } @@ -169,6 +173,18 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager { } } + private _onMouseLeave(e: MouseEvent): void { + // Fire the hover end callback and cancel any existing timer if the mouse + // leaves the terminal element + if (this._currentZone) { + this._currentZone.leaveCallback(); + this._currentZone = null; + if (this._tooltipTimeout) { + clearTimeout(this._tooltipTimeout); + } + } + } + private _onClick(e: MouseEvent): void { // Find the active zone and click it if found const zone = this._findZoneEventAt(e);