diff --git a/src/Buffer.ts b/src/Buffer.ts index d8311f06..101244f1 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -34,8 +34,8 @@ export class Buffer implements IBuffer { /** * Create a new Buffer. * @param _terminal The terminal the Buffer will belong to. - * @param _hasScrollback Whether the buffer should respecr the scrollback of - * the terminal.. + * @param _hasScrollback Whether the buffer should respect the scrollback of + * the terminal. */ constructor( private _terminal: ITerminal, diff --git a/src/Parser.ts b/src/Parser.ts index 983b3536..b047f91f 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -189,6 +189,9 @@ export class Parser { let code; let low; + const cursorStartX = this._terminal.buffer.x; + const cursorStartY = this._terminal.buffer.y; + if (this._terminal.debug) { this._terminal.log('data: ' + data); } @@ -580,6 +583,13 @@ export class Parser { break; } } + + // Fire the cursormove event if it's moved. This is done inside the parser + // as a render cannot happen in the middle of a parsing round. + if (this._terminal.buffer.x !== cursorStartX || this._terminal.buffer.y !== cursorStartY) { + this._terminal.emit('cursormove'); + } + return this._state; } diff --git a/src/Terminal.ts b/src/Terminal.ts index 0c6eb65d..17fd0d97 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -754,6 +754,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); this.renderer = new Renderer(this); + this.on('cursormove', () => { + console.log('cursormove fired'); + this.renderer.onCursorMove(); + }); this.on('resize', () => this.renderer.onResize(this.cols, this.rows)); this.charMeasure.on('charsizechanged', () => { this.renderer.onCharSizeChanged(this.charMeasure.width, this.charMeasure.height); diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts index 047269c3..378ac1a1 100644 --- a/src/renderer/BaseRenderLayer.ts +++ b/src/renderer/BaseRenderLayer.ts @@ -27,9 +27,9 @@ export abstract class BaseRenderLayer implements IRenderLayer { } } - public onOptionsChanged(options: ITerminal): void { // TODO: Should this do anything? - } + public onOptionsChanged(options: ITerminal): void {} + public onCursorMove(options: ITerminal): void {} public resize(terminal: ITerminal, canvasWidth: number, canvasHeight: number, charSizeChanged: boolean): void { this.scaledCharWidth = terminal.charMeasure.width * window.devicePixelRatio; diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts index 11e56347..3ccf2ec2 100644 --- a/src/renderer/CursorRenderLayer.ts +++ b/src/renderer/CursorRenderLayer.ts @@ -15,23 +15,11 @@ const BLINK_INTERVAL = 600; export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLayer { private _state: [number, number]; private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void}; - private _animationFrame: number; - private _blinkInterval: number; - private _isVisible: boolean; - - /** - * The time at which the animation frame was restarted, this is used on the - * next render to restart the timers so they don't need to restart the timers - * multiple times over a short period. - */ - private _animationTimeRestarted: number; - private _cursorBlinkStateManager: CursorBlinkStateManager; constructor(container: HTMLElement, zIndex: number) { super(container, 'cursor', zIndex); this._state = null; - this._isVisible = true; this._cursorRenderers = { 'bar': this._renderBarCursor.bind(this), 'block': this._renderBlockCursor.bind(this), @@ -42,7 +30,11 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay public reset(terminal: ITerminal): void { this._clearCursor(); - this._isVisible = true; + if (this._cursorBlinkStateManager) { + this._cursorBlinkStateManager.dispose(); + this._cursorBlinkStateManager = null; + this.onOptionsChanged(terminal); + } } public onOptionsChanged(terminal: ITerminal): void { @@ -64,6 +56,12 @@ export class CursorRenderLayer extends BaseRenderLayer implements IDataRenderLay } } + public onCursorMove(terminal: ITerminal): void { + if (this._cursorBlinkStateManager) { + this._cursorBlinkStateManager.restartBlinkAnimation(terminal); + } + } + public render(terminal: ITerminal, startRow: number, endRow: number): void { // Only render if the animation frame is not active if (!this._cursorBlinkStateManager) { @@ -131,20 +129,22 @@ class CursorBlinkStateManager { public isCursorVisible: boolean; private _animationFrame: number; + private _blinkStartTimeout: number; private _blinkInterval: number; + /** + * The time at which the animation frame was restarted, this is used on the + * next render to restart the timers so they don't need to restart the timers + * multiple times over a short period. + */ + private _animationTimeRestarted: number; + constructor( terminal: ITerminal, private renderCallback: () => void ) { this.isCursorVisible = true; - this._blinkInterval = setInterval(() => { - this.isCursorVisible = !this.isCursorVisible; - this._animationFrame = window.requestAnimationFrame(() => { - this.renderCallback(); - this._animationFrame = null; - }); - }, BLINK_INTERVAL); + this._restartInterval(); } public dispose(): void { @@ -156,10 +156,71 @@ class CursorBlinkStateManager { } } - private _restartBlinkAnimation(terminal: ITerminal): void { - // TODO: Restart the blink animation when input is received - // How can this be done efficiently, without thrashing with restarting the timers? - // Could record the time it was restarted and diff that on next render? + public restartBlinkAnimation(terminal: ITerminal): void { + console.log('restartBlinkAnimation'); + // Save a timestamp so that the restart can be done on the next interval + this._animationTimeRestarted = Date.now(); + // Force a cursor render to ensure it's visible and in the correct position + this.isCursorVisible = true; + if (!this._animationFrame) { + this._animationFrame = window.requestAnimationFrame(() => { + this.renderCallback(); + this._animationFrame = null; + }); + } + } + + private _restartInterval(timeToStart: number = BLINK_INTERVAL): void { + // Clear any existing interval + if (this._blinkInterval) { + window.clearInterval(this._blinkInterval); + } + + console.log('restartInterval'); + // Setup the initial timeout which will hide the cursor, this is done before + // the regular interval is setup in order to support restarting the blink + // animation in a lightweight way (without thrashing clearInterval and + // setInterval). + this._blinkStartTimeout = setTimeout(() => { + // Check if another animation restart was requested while this was being + // started + if (this._animationTimeRestarted) { + const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); + this._animationTimeRestarted = null; + this._restartInterval(time); + return; + } + + console.log('timeout'); + // Hide the cursor + this.isCursorVisible = false; + this._animationFrame = window.requestAnimationFrame(() => { + this.renderCallback(); + this._animationFrame = null; + }); + + // Setup the blink interval + this._blinkInterval = setInterval(() => { + console.log('interval'); + // Adjust the animation time if it was restarted + if (this._animationTimeRestarted) { + // calc time diff + // Make restart interval do a setTimeout initially? + const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); + this._animationTimeRestarted = null; + console.log(' restart in ', time); + this._restartInterval(time); + return; + } + + // Invert visibility and render + this.isCursorVisible = !this.isCursorVisible; + this._animationFrame = window.requestAnimationFrame(() => { + this.renderCallback(); + this._animationFrame = null; + }); + }, BLINK_INTERVAL); + }, timeToStart); } private _pauseBlinkAnimation(): void { diff --git a/src/renderer/Interfaces.ts b/src/renderer/Interfaces.ts index 55cffbc5..5da13109 100644 --- a/src/renderer/Interfaces.ts +++ b/src/renderer/Interfaces.ts @@ -1,6 +1,7 @@ import { ITerminal, ITerminalOptions } from '../Interfaces'; export interface IRenderLayer { + onCursorMove(options: ITerminal): void; onOptionsChanged(options: ITerminal): void; /** diff --git a/src/renderer/Renderer.ts b/src/renderer/Renderer.ts index 2903460a..7c6f6d40 100644 --- a/src/renderer/Renderer.ts +++ b/src/renderer/Renderer.ts @@ -56,6 +56,15 @@ export class Renderer { } } + public onCursorMove(): void { + for (let i = 0; i < this._dataRenderLayers.length; i++) { + this._dataRenderLayers[i].onCursorMove(this._terminal); + } + for (let i = 0; i < this._selectionRenderLayers.length; i++) { + this._selectionRenderLayers[i].onCursorMove(this._terminal); + } + } + public onOptionsChanged(): void { for (let i = 0; i < this._dataRenderLayers.length; i++) { this._dataRenderLayers[i].onOptionsChanged(this._terminal);