From 3e1a60717967f2a661e1c1bfe77f9ff06f8debff Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 07:36:56 -0800 Subject: [PATCH 01/13] Convert browser and generic to TS Part of #335 --- src/utils/Browser.js | 22 ---------------------- src/utils/Browser.ts | 22 ++++++++++++++++++++++ src/utils/{Generic.js => Generic.ts} | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) delete mode 100644 src/utils/Browser.js create mode 100644 src/utils/Browser.ts rename src/utils/{Generic.js => Generic.ts} (88%) diff --git a/src/utils/Browser.js b/src/utils/Browser.js deleted file mode 100644 index cd13e027..00000000 --- a/src/utils/Browser.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Attributes and methods to help with identifying the current browser and platform. - * @module xterm/utils/Browser - * @license MIT - */ - -import { contains } from './Generic.js'; - -let isNode = (typeof navigator == 'undefined') ? true : false; -let userAgent = (isNode) ? 'node' : navigator.userAgent; -let platform = (isNode) ? 'node' : navigator.platform; - -export let isFirefox = !!~userAgent.indexOf('Firefox'); -export let isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident'); - -// Find the users platform. We use this to interpret the meta key -// and ISO third level shifts. -// http://stackoverflow.com/q/19877924/577598 -export let isMac = contains(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], platform); -export let isIpad = platform === 'iPad'; -export let isIphone = platform === 'iPhone'; -export let isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform); diff --git a/src/utils/Browser.ts b/src/utils/Browser.ts new file mode 100644 index 00000000..04da698e --- /dev/null +++ b/src/utils/Browser.ts @@ -0,0 +1,22 @@ +/** + * Attributes and methods to help with identifying the current browser and platform. + * @module xterm/utils/Browser + * @license MIT + */ + +import { contains } from './Generic'; + +const isNode = (typeof navigator === 'undefined') ? true : false; +const userAgent = (isNode) ? 'node' : navigator.userAgent; +const platform = (isNode) ? 'node' : navigator.platform; + +export const isFirefox = !!~userAgent.indexOf('Firefox'); +export const isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident'); + +// Find the users platform. We use this to interpret the meta key +// and ISO third level shifts. +// http://stackoverflow.com/q/19877924/577598 +export const isMac = contains(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], platform); +export const isIpad = platform === 'iPad'; +export const isIphone = platform === 'iPhone'; +export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform); diff --git a/src/utils/Generic.js b/src/utils/Generic.ts similarity index 88% rename from src/utils/Generic.js rename to src/utils/Generic.ts index 42f876f3..ce09c1be 100644 --- a/src/utils/Generic.js +++ b/src/utils/Generic.ts @@ -9,6 +9,6 @@ * @param {Array} array The array to search for the given element. * @param {Object} el The element to look for into the array */ -export let contains = function(arr, el) { +export function contains(arr: any[], el: any) { return arr.indexOf(el) >= 0; }; From 97feb3321c74b5fdc7aac35eca2bb852800d9120 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 14:06:26 -0800 Subject: [PATCH 02/13] Improve refresh queue Use requestAnimationFrame in addition to a queue to refresh every animation frame but only when a refresh is needed. Fixes #280 Fixes #290 --- src/xterm.js | 98 +++++++++++++++++++++++++--------------------------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 1d220652..79ca633b 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -137,14 +137,8 @@ function Terminal(options) { */ this.y = 0; - /** - * Used to debounce the refresh function - */ - this.isRefreshing = false; - - /** - * Whether there is a full terminal refresh queued - */ + /** A queue of the rows to be refreshed */ + this.refreshRowsQueue = []; this.cursorState = 0; this.cursorHidden = false; @@ -407,7 +401,7 @@ Terminal.prototype.blur = function() { */ Terminal.bindBlur = function (term) { on(term.textarea, 'blur', function (ev) { - term.refresh(term.y, term.y); + term.queueRefresh(term.y, term.y); if (term.sendFocus) { term.send('\x1b[O'); } @@ -585,8 +579,13 @@ Terminal.prototype.open = function(parent) { this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasureElement); - // Draw the screen. - this.refresh(0, this.rows - 1); + // Setup loop that draws to screen + this.queueRefresh(0, this.rows - 1); + function refreshLoop() { + self.refresh(); + window.requestAnimationFrame(refreshLoop); + } + window.requestAnimationFrame(refreshLoop); // Initialize global actions that // need to be taken on the document. @@ -997,6 +996,16 @@ Terminal.flags = { INVISIBLE: 16 } +/** + * Queues a refresh between two rows (inclusive), to be done on next animation + * frame. + * @param {number} start The start row. + * @param {number} end The end row. + */ +Terminal.prototype.queueRefresh = function(start, end) { + this.refreshRowsQueue.push({ start: start, end: end }); +} + /** * Refreshes (re-renders) terminal content within two rows (inclusive) * @@ -1019,44 +1028,33 @@ Terminal.flags = { * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) * @param {boolean} queue Whether the refresh should ran right now or be queued */ -Terminal.prototype.refresh = function(start, end, queue) { - var self = this; - - // queue defaults to true - queue = (typeof queue == 'undefined') ? true : queue; - - /** - * The refresh queue allows refresh to execute only approximately 30 times a second. For - * commands that pass a significant amount of output to the write function, this prevents the - * terminal from maxing out the CPU and making the UI unresponsive. While commands can still - * run beyond what they do on the terminal, it is far better with a debounce in place as - * every single terminal manipulation does not need to be constructed in the DOM. - * - * A side-effect of this is that it makes ^C to interrupt a process seem more responsive. - */ - if (queue) { - // If refresh should be queued, order the refresh and return. - if (this._refreshIsQueued) { - // If a refresh has already been queued, just order a full refresh next - this._fullRefreshNext = true; - } else { - setTimeout(function () { - self.refresh(start, end, false); - }, 34) - this._refreshIsQueued = true; - } +Terminal.prototype.refresh = function() { + if (this.refreshRowsQueue.length === 0) { + // Don't refresh if there were no row changes return; } - // If refresh should be run right now (not be queued), release the lock - this._refreshIsQueued = false; - - // If multiple refreshes were requested, make a full refresh. - if (this._fullRefreshNext) { + var start; + var end; + if (this.refreshRowsQueue.length > 4) { + // Just do a full refresh when 5+ refreshes are queued start = 0; end = this.rows - 1; - this._fullRefreshNext = false // reset lock + } else { + // Get start and end rows that need refreshing + start = this.refreshRowsQueue[0].start; + end = this.refreshRowsQueue[0].end; + for (var i = 1; i < this.refreshRowsQueue.length; i++) { + if (this.refreshRowsQueue[i].start < start) { + start = this.refreshRowsQueue[i].start; + } + if (this.refreshRowsQueue[i].end > end) { + end = this.refreshRowsQueue[i].end; + } + } } + this.refreshRowsQueue = []; + var self = this; var x, y, i, line, out, ch, ch_width, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement; @@ -1224,7 +1222,7 @@ Terminal.prototype.refresh = function(start, end, queue) { Terminal.prototype.showCursor = function() { if (!this.cursorState) { this.cursorState = 1; - this.refresh(this.y, this.y); + this.queueRefresh(this.y, this.y); } }; @@ -1311,7 +1309,7 @@ Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) { this.emit('scroll', this.ydisp); } - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); }; /** @@ -2379,7 +2377,7 @@ Terminal.prototype.write = function(data) { } this.updateRange(this.y); - this.refresh(this.refreshStart, this.refreshEnd); + this.queueRefresh(this.refreshStart, this.refreshEnd); }; /** @@ -2945,7 +2943,7 @@ Terminal.prototype.resize = function(x, y) { this.scrollTop = 0; this.scrollBottom = y - 1; - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.normal = null; @@ -3074,7 +3072,7 @@ Terminal.prototype.clear = function() { for (var i = 1; i < this.rows; i++) { this.lines.push(this.blankLine()); } - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.emit('scroll', this.ydisp); }; @@ -3205,7 +3203,7 @@ Terminal.prototype.reset = function() { var customKeydownHandler = this.customKeydownHandler; Terminal.call(this, this.options); this.customKeydownHandler = customKeydownHandler; - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.viewport.syncScrollArea(); }; @@ -4324,7 +4322,7 @@ Terminal.prototype.resetMode = function(params) { // this.x = this.savedX; // this.y = this.savedY; // } - this.refresh(0, this.rows - 1); + this.queueRefresh(0, this.rows - 1); this.viewport.syncScrollArea(); this.showCursor(); } From 7234bfb6eb637fe3ed388db5b0a78949de7f06d7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 14:58:17 -0800 Subject: [PATCH 03/13] Move row evaluation into refreshLoop This allows the refresh public API to still function --- src/xterm.js | 66 +++++++++++++++++++++++++++------------------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 79ca633b..cd304186 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -581,11 +581,7 @@ Terminal.prototype.open = function(parent) { // Setup loop that draws to screen this.queueRefresh(0, this.rows - 1); - function refreshLoop() { - self.refresh(); - window.requestAnimationFrame(refreshLoop); - } - window.requestAnimationFrame(refreshLoop); + this.refreshLoop(); // Initialize global actions that // need to be taken on the document. @@ -1006,6 +1002,38 @@ Terminal.prototype.queueRefresh = function(start, end) { this.refreshRowsQueue.push({ start: start, end: end }); } +/** + * Performs the refresh loop callback, calling refresh only if a refresh is + * necessary before queueing up the next one. + */ +Terminal.prototype.refreshLoop = function() { + // Don't refresh if there were no row changes + if (this.refreshRowsQueue.length > 0) { + var start; + var end; + if (this.refreshRowsQueue.length > 4) { + // Just do a full refresh when 5+ refreshes are queued + start = 0; + end = this.rows - 1; + } else { + // Get start and end rows that need refreshing + start = this.refreshRowsQueue[0].start; + end = this.refreshRowsQueue[0].end; + for (var i = 1; i < this.refreshRowsQueue.length; i++) { + if (this.refreshRowsQueue[i].start < start) { + start = this.refreshRowsQueue[i].start; + } + if (this.refreshRowsQueue[i].end > end) { + end = this.refreshRowsQueue[i].end; + } + } + } + this.refreshRowsQueue = []; + this.refresh(start, end); + } + window.requestAnimationFrame(this.refreshLoop.bind(this)); +} + /** * Refreshes (re-renders) terminal content within two rows (inclusive) * @@ -1026,34 +1054,8 @@ Terminal.prototype.queueRefresh = function(start, end) { * * @param {number} start The row to start from (between 0 and terminal's height terminal - 1) * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1) - * @param {boolean} queue Whether the refresh should ran right now or be queued */ -Terminal.prototype.refresh = function() { - if (this.refreshRowsQueue.length === 0) { - // Don't refresh if there were no row changes - return; - } - - var start; - var end; - if (this.refreshRowsQueue.length > 4) { - // Just do a full refresh when 5+ refreshes are queued - start = 0; - end = this.rows - 1; - } else { - // Get start and end rows that need refreshing - start = this.refreshRowsQueue[0].start; - end = this.refreshRowsQueue[0].end; - for (var i = 1; i < this.refreshRowsQueue.length; i++) { - if (this.refreshRowsQueue[i].start < start) { - start = this.refreshRowsQueue[i].start; - } - if (this.refreshRowsQueue[i].end > end) { - end = this.refreshRowsQueue[i].end; - } - } - } - this.refreshRowsQueue = []; +Terminal.prototype.refresh = function(start, end) { var self = this; var x, y, i, line, out, ch, ch_width, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement; From 188e197e20d3c678fc6f5d478e17699caea01128 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 15:19:54 -0800 Subject: [PATCH 04/13] Wrap wide chars in a span/class Part of #439 --- src/xterm.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/xterm.js b/src/xterm.js index 1d220652..fcb60bf5 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1182,6 +1182,9 @@ Terminal.prototype.refresh = function(start, end, queue) { } } + if (ch_width === 2) { + out += ''; + } switch (ch) { case '&': out += '&'; @@ -1200,6 +1203,9 @@ Terminal.prototype.refresh = function(start, end, queue) { } break; } + if (ch_width === 2) { + out += ''; + } attr = data; } From 4f18d842f9ee355387628b9f9f4cbb1b0ea02e9a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 15:46:17 -0800 Subject: [PATCH 05/13] Add CharMeasure util class --- src/utils/CharMeasure.ts | 52 ++++++++++++++++++++++++++++++++++++++++ src/xterm.js | 6 +++++ 2 files changed, 58 insertions(+) create mode 100644 src/utils/CharMeasure.ts diff --git a/src/utils/CharMeasure.ts b/src/utils/CharMeasure.ts new file mode 100644 index 00000000..60684a2c --- /dev/null +++ b/src/utils/CharMeasure.ts @@ -0,0 +1,52 @@ +/** + * @module xterm/utils/CharMeasure + * @license MIT + */ + +import { EventEmitter } from '../EventEmitter.js'; + +/** + * Utility class that measures the size of a character. + */ +export class CharMeasure extends EventEmitter { + private _parentElement: HTMLElement; + private _measureElement: HTMLElement; + private _width: number; + private _height: number; + + constructor(parentElement: HTMLElement) { + super(); + this._parentElement = parentElement; + } + + public get width(): number { + return this._width; + } + + public get height(): number { + return this._height; + } + + public measure(): void { + const oldWidth = this._width; + const oldHeight = this._height; + + if (!this._measureElement) { + this._measureElement = document.createElement('span'); + this._measureElement.style.position = 'absolute'; + this._measureElement.style.top = '0'; + this._measureElement.style.left = '-9999em'; + this._measureElement.textContent = 'W'; + } + + this._parentElement.appendChild(this._measureElement); + const geometry = this._measureElement.getBoundingClientRect(); + this._width = geometry.width; + this._height = geometry.height; + this._parentElement.removeChild(this._measureElement); + + if (this._width !== oldWidth || this._height !== oldHeight) { + this.emit('charsizechanged'); + } + } +} diff --git a/src/xterm.js b/src/xterm.js index fcb60bf5..826531e2 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -15,6 +15,7 @@ import { EventEmitter } from './EventEmitter.js'; import { Viewport } from './Viewport.js'; import { rightClickHandler, pasteHandler, copyHandler } from './handlers/Clipboard.js'; import { CircularList } from './utils/CircularList.js'; +import { CharMeasure } from './utils/CharMeasure.js'; import * as Browser from './utils/Browser'; import * as Keyboard from './utils/Keyboard'; @@ -583,6 +584,9 @@ Terminal.prototype.open = function(parent) { } this.parent.appendChild(this.element); + this.charMeasure = new CharMeasure(this.rowContainer); + this.charMeasure.measure(); + this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasureElement); // Draw the screen. @@ -2951,6 +2955,8 @@ Terminal.prototype.resize = function(x, y) { this.scrollTop = 0; this.scrollBottom = y - 1; + this.charMeasure.measure(); + this.refresh(0, this.rows - 1); this.normal = null; From 74483fb2948bbfcb42e6d22b6a3f764144195c95 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 15:56:53 -0800 Subject: [PATCH 06/13] Use CharMeasure in Viewport and to style wide chars Fixes #439 --- src/Viewport.ts | 25 ++++++++++++------------- src/xterm.css | 4 ++++ src/xterm.js | 18 +++++++++++++----- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/Viewport.ts b/src/Viewport.ts index 3aa1319f..013ae65a 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -3,6 +3,7 @@ */ import { ITerminal } from './Interfaces'; +import { CharMeasure } from './utils/CharMeasure'; /** * Represents the viewport of a terminal, the visible area within the larger buffer of output. @@ -24,7 +25,7 @@ export class Viewport { private terminal: ITerminal, private viewportElement: HTMLElement, private scrollArea: HTMLElement, - private charMeasureElement: HTMLElement + private charMeasure: CharMeasure ) { this.currentRowHeight = 0; this.lastRecordedBufferLength = 0; @@ -43,21 +44,20 @@ export class Viewport { * @param charSize A character size measurement bounding rect object, if it doesn't exist it will * be created. */ - private refresh(charSize?: ClientRect): void { - var size = charSize || this.charMeasureElement.getBoundingClientRect(); - if (size.height > 0) { - var rowHeightChanged = size.height !== this.currentRowHeight; + private refresh(): void { + if (this.charMeasure.height > 0) { + var rowHeightChanged = this.charMeasure.height !== this.currentRowHeight; if (rowHeightChanged) { - this.currentRowHeight = size.height; - this.viewportElement.style.lineHeight = size.height + 'px'; - this.terminal.rowContainer.style.lineHeight = size.height + 'px'; + this.currentRowHeight = this.charMeasure.height; + this.viewportElement.style.lineHeight = this.charMeasure.height + 'px'; + this.terminal.rowContainer.style.lineHeight = this.charMeasure.height + 'px'; } var viewportHeightChanged = this.lastRecordedViewportHeight !== this.terminal.rows; if (rowHeightChanged || viewportHeightChanged) { this.lastRecordedViewportHeight = this.terminal.rows; - this.viewportElement.style.height = size.height * this.terminal.rows + 'px'; + this.viewportElement.style.height = this.charMeasure.height * this.terminal.rows + 'px'; } - this.scrollArea.style.height = (size.height * this.lastRecordedBufferLength) + 'px'; + this.scrollArea.style.height = (this.charMeasure.height * this.lastRecordedBufferLength) + 'px'; } } @@ -74,9 +74,8 @@ export class Viewport { this.refresh(); } else { // If size has changed, refresh viewport - var size = this.charMeasureElement.getBoundingClientRect(); - if (size.height !== this.currentRowHeight) { - this.refresh(size); + if (this.charMeasure.height !== this.currentRowHeight) { + this.refresh(); } } diff --git a/src/xterm.css b/src/xterm.css index 4877f86e..3f4f4bbe 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -116,6 +116,10 @@ overflow-y: scroll; } +.terminal .xterm-wide-char { + display: inline-block; +} + .terminal .xterm-rows { position: absolute; left: 0; diff --git a/src/xterm.js b/src/xterm.js index 826531e2..999dc08b 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -574,10 +574,8 @@ Terminal.prototype.open = function(parent) { this.compositionHelper = new CompositionHelper(this.textarea, this.compositionView, this); this.helperContainer.appendChild(this.compositionView); - this.charMeasureElement = document.createElement('div'); - this.charMeasureElement.classList.add('xterm-char-measure-element'); - this.charMeasureElement.innerHTML = 'W'; - this.helperContainer.appendChild(this.charMeasureElement); + this.charSizeStyleElement = document.createElement('style'); + this.helperContainer.appendChild(this.charSizeStyleElement); for (; i < this.rows; i++) { this.insertRow(); @@ -585,9 +583,12 @@ Terminal.prototype.open = function(parent) { this.parent.appendChild(this.element); this.charMeasure = new CharMeasure(this.rowContainer); + this.charMeasure.on('charsizechanged', function () { + self.updateCharSizeCSS(); + }); this.charMeasure.measure(); - this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasureElement); + this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure); // Draw the screen. this.refresh(0, this.rows - 1); @@ -645,6 +646,13 @@ Terminal.loadAddon = function(addon, callback) { } }; +/** + * Updates the helper CSS class with any changes necessary after the terminal's + * character width has been changed. + */ +Terminal.prototype.updateCharSizeCSS = function() { + this.charSizeStyleElement.textContent = '.xterm-wide-char{width:' + (this.charMeasure.width * 2) + 'px;}'; +} /** * XTerm mouse events From 07a8b8f4f77d85c2a4f3b07a6ab7f730858abc9e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 18:06:49 -0800 Subject: [PATCH 07/13] Fix tests --- src/Viewport.test.ts | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts index 5b106b43..4fa77ec0 100644 --- a/src/Viewport.test.ts +++ b/src/Viewport.test.ts @@ -2,11 +2,11 @@ import { assert } from 'chai'; import { Viewport } from './Viewport'; describe('Viewport', () => { - var terminal; - var viewportElement; - var charMeasureElement; - var viewport; - var scrollAreaElement; + let terminal; + let viewportElement; + let charMeasure; + let viewport; + let scrollAreaElement; const CHARACTER_HEIGHT = 10; @@ -34,21 +34,17 @@ describe('Viewport', () => { height: 0 } }; - charMeasureElement = { - getBoundingClientRect: () => { - return { width: null, height: CHARACTER_HEIGHT }; - } + charMeasure = { + height: CHARACTER_HEIGHT }; - viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasureElement); + viewport = new Viewport(terminal, viewportElement, scrollAreaElement, charMeasure); }); describe('refresh', () => { it('should set the line-height of the terminal', () => { assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px'); assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px'); - charMeasureElement.getBoundingClientRect = () => { - return { width: null, height: 1 }; - }; + charMeasure.height = 1; viewport.refresh(); assert.equal(viewportElement.style.lineHeight, '1px'); assert.equal(terminal.rowContainer.style.lineHeight, '1px'); @@ -59,9 +55,7 @@ describe('Viewport', () => { terminal.rows = 1; viewport.refresh(); assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); - charMeasureElement.getBoundingClientRect = () => { - return { width: null, height: 20 }; - }; + charMeasure.height = 20; viewport.refresh(); assert.equal(viewportElement.style.height, 20 + 'px'); }); From 6ffb8f545b6a8f21883ebaadcb48f81e23e27ad0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 3 Jan 2017 11:18:38 -0800 Subject: [PATCH 08/13] Rate limit Viewport.refresh This prevents 1000 scroll events from firing when the buffer is not full Fixes #444 --- src/Viewport.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Viewport.ts b/src/Viewport.ts index 3aa1319f..b266dd3e 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -12,6 +12,7 @@ export class Viewport { private currentRowHeight: number; private lastRecordedBufferLength: number; private lastRecordedViewportHeight: number; + private isRefreshQueued: boolean; /** * Creates a new Viewport. @@ -29,12 +30,22 @@ export class Viewport { this.currentRowHeight = 0; this.lastRecordedBufferLength = 0; this.lastRecordedViewportHeight = 0; + this.isRefreshQueued = false; this.terminal.on('scroll', this.syncScrollArea.bind(this)); this.terminal.on('resize', this.syncScrollArea.bind(this)); this.viewportElement.addEventListener('scroll', this.onScroll.bind(this)); this.syncScrollArea(); + this.refreshLoop(); + } + + private refreshLoop(): void { + if (this.isRefreshQueued) { + this.refresh(); + this.isRefreshQueued = false; + } + window.requestAnimationFrame(this.refreshLoop.bind(this)); } /** @@ -68,15 +79,15 @@ export class Viewport { if (this.lastRecordedBufferLength !== this.terminal.lines.length) { // If buffer height changed this.lastRecordedBufferLength = this.terminal.lines.length; - this.refresh(); + this.isRefreshQueued = true; } else if (this.lastRecordedViewportHeight !== this.terminal.rows) { // If viewport height changed - this.refresh(); + this.isRefreshQueued = true; } else { // If size has changed, refresh viewport var size = this.charMeasureElement.getBoundingClientRect(); if (size.height !== this.currentRowHeight) { - this.refresh(size); + this.isRefreshQueued = true; } } From efdf37b887221d2b9a7951cffa59c9e5323b1022 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 3 Jan 2017 11:23:36 -0800 Subject: [PATCH 09/13] jsdoc --- src/Viewport.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Viewport.ts b/src/Viewport.ts index b266dd3e..32a71dde 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -40,6 +40,9 @@ export class Viewport { this.refreshLoop(); } + /** + * Queues a refresh to be done on next animation frame. + */ private refreshLoop(): void { if (this.isRefreshQueued) { this.refresh(); From ff523bfc790d915f23502d97b427497c8b62189d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 4 Jan 2017 10:30:04 -0800 Subject: [PATCH 10/13] Fix tests --- src/Viewport.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts index 5b106b43..fc2cd117 100644 --- a/src/Viewport.test.ts +++ b/src/Viewport.test.ts @@ -1,6 +1,11 @@ import { assert } from 'chai'; import { Viewport } from './Viewport'; +class MockWindow { + // Disable refreshLoop in test + public requestAnimationFrame() { } +} + describe('Viewport', () => { var terminal; var viewportElement; @@ -11,6 +16,7 @@ describe('Viewport', () => { const CHARACTER_HEIGHT = 10; beforeEach(() => { + (global).window = new MockWindow(); terminal = { lines: [], rows: 0, @@ -73,10 +79,14 @@ describe('Viewport', () => { terminal.rows = 1; assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px'); viewport.syncScrollArea(); + assert.ok(viewport.isRefreshQueued); + viewport.refresh(); assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); terminal.lines.push(''); viewport.syncScrollArea(); + assert.ok(viewport.isRefreshQueued); + viewport.refresh(); assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px'); }); From 996c25a3648a899a8bb05ad5bc9df3a0b4629868 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 7 Jan 2017 16:04:46 -0800 Subject: [PATCH 11/13] Keep previous terminal events after a reset This broke in the EventEmitter ts conversion, just reverts this line to what it used to be. Fixes #451 --- src/EventEmitter.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/EventEmitter.ts b/src/EventEmitter.ts index d05e3863..1db86763 100644 --- a/src/EventEmitter.ts +++ b/src/EventEmitter.ts @@ -11,7 +11,9 @@ export class EventEmitter { private _events: {[type: string]: ListenerType[]}; constructor() { - this._events = {}; + // Restore the previous events if available, this will happen if the + // constructor is called multiple times on the same object (terminal reset). + this._events = this._events || {}; } public on(type, listener): void { From 520f2bc9d6365c7034b9519cc2b0c98c71bd6f39 Mon Sep 17 00:00:00 2001 From: Tyler Jewell Date: Sun, 8 Jan 2017 09:44:14 -0800 Subject: [PATCH 12/13] Add additional products that use xTerm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hi xTerm team. Over the past 6 months, we have been studying xTerm as a replacement for Eclipse Che's term.js. It's taken us a bit of an effort, but the work is now done! You can expect that Eclipse Che engineers (abot 60 contributors) will make direct contributions back to xTerm ongoing now that we are fully integrated in. We integrate xTerm alongside GWT. As we more natively integrate the solution, we'll make additional PRs directly back to the project. As background on the research and integration work that we did, these are the postings from our engineering team's research from the past six months. Some of these issues have already been resolved, or in the process of being resolved. We also plan a couple of blog posts to our forums (~250K followers) about xTerm integration later in Q1 after we make our 5.0.0 announcements. I could also add Codenvy to this PR as well since it has been released with the version based upon xTerm, but with Sourcelair being the initiators we didn't want to make it seem like we were trying to be competitive. This PR is about the promotion and success of xTerm, for which we are fully committed to. ## Xtermjs This document is result of investigation using xtemjs ui terminal instead of current term.js. It contains analyze pluses and minuses using xtermjs (release version 2.2.3) and technical problems. Investigation issue: https://github.com/eclipse/che/issues/3210 ## Description For now we are using our own fork of the https://github.com/chjj/term.js for user interface websocket-terminal. But actually this project is no longer maintained. So we can not get new releases from this project and we need support this script on our own. Original project contains link for a maintained fork https://github.com/sourcelair/xterm.js. This fork uses MIT license and community intensive develops this project. It has such users like: Microsoft Visual Studio Code, SourceLair, ttyd. xterm.js had already done 46 releases https://github.com/sourcelair/xterm.js/releases. ## Technical Advantages of xTerm: * xterm.js has js tests. We have not any test for old term.js. * We can periodically update xtem.js by new release. * Added parameter to cancel browser events. * Default 256 colours moved from js to css. * Added ability to set terminal theme. * Improved resize mehanizm (added resize event). * Added fit.ts script to fits terminal size to original height and width of parent div. * Fixed lost text selection from current active line(when blink is enabled). * Improved copy/paster mehanizm. * Linkify URL feature. * Implement moving back and forward across words with "Alt + ←" and "Alt + →" respectively. * Improved special key handling. * Added addons to exdens xterm.js * Improved mehanizm copy/paster from clipboard. * Fixed cross platform input problems(For IPad, Iphone, MacIntel, MacPPC and so on). * Did some work to support UTF-8 symbols. * Implemented scrollbar. * Fixed incorrect mouse position for application with pseudo-graphic user interface (for example Midnight Commander). * Fix to prevent terminal scrolling when user is looking into scrollback(similar to gnome-terminal, if the user is scrolling up to look at past output and the currently running program adds output to the terminal, the viewport of xterm.js should not scroll and interrupt what they are looking at.) * Make right-click work on all browsers. ## Broken Changes: We can move on our changes to realize copy/paste by hotkeys Ctrl + C/Ctrl +V. But xterm.js support hotkeys more common for terminal: Ctrl + Insert and Shift + Insert and maybe that's enough. Drop support for old mouse wheel APIs: all browsers have supported the WheelEvent (onwheel) for sometime now, since Firefox does not support onmousewheel which is also non-standard but works with the standard interface, it makes sense to drop support now. ## Browser Support Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions to support: Chrome 48+ Edge 13+ Firefox 44+ Internet Explorer 11+ Opera 35+ Safari 8+ Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers xterm.js developers strive to keep working. ## Known major bugs: https://github.com/sourcelair/xterm.js/issues/307 https://github.com/sourcelair/xterm.js/issues/362 https://github.com/sourcelair/xterm.js/issues/348 https://github.com/sourcelair/xterm.js/issues/325 Data loss when resizing terminal (but this bug is exist in the current terminal ui in the CHE too). Community has pull request to fix this issue for xtermjs https://github.com/sourcelair/xterm.js/pull/404 . Additional information: Code base xterm.js consist of files written on native javascript and typescript. For manage js dependency and configuration used npm, typings, bower and node. Development tendency: rewrite xtemr.js completely on the Typescript(information about this included to the release notes 2.2.3). --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index b5d30041..608c48ac 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js - [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js - [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies. +- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. @@ -117,6 +118,10 @@ Visit https://lair.io/sourcelair/xterm and follow the instructions. All developm [Download Visual Studio Code](http://code.visualstudio.com/Download), clone xterm.js and you are all set. +#### [Eclipse Che](http://www.eclipse.org/che) + +You can start Eclipse Che with `docker run eclipse/che start` + ## License Agreement If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work. From 06852cd9043df87e248a498dd0186bb4ab4ed58b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 8 Jan 2017 16:46:23 -0800 Subject: [PATCH 13/13] Revert "Rate limit Viewport.refresh" --- src/Viewport.test.ts | 10 ---------- src/Viewport.ts | 20 +++----------------- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/src/Viewport.test.ts b/src/Viewport.test.ts index fc2cd117..5b106b43 100644 --- a/src/Viewport.test.ts +++ b/src/Viewport.test.ts @@ -1,11 +1,6 @@ import { assert } from 'chai'; import { Viewport } from './Viewport'; -class MockWindow { - // Disable refreshLoop in test - public requestAnimationFrame() { } -} - describe('Viewport', () => { var terminal; var viewportElement; @@ -16,7 +11,6 @@ describe('Viewport', () => { const CHARACTER_HEIGHT = 10; beforeEach(() => { - (global).window = new MockWindow(); terminal = { lines: [], rows: 0, @@ -79,14 +73,10 @@ describe('Viewport', () => { terminal.rows = 1; assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px'); viewport.syncScrollArea(); - assert.ok(viewport.isRefreshQueued); - viewport.refresh(); assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); terminal.lines.push(''); viewport.syncScrollArea(); - assert.ok(viewport.isRefreshQueued); - viewport.refresh(); assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px'); assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px'); }); diff --git a/src/Viewport.ts b/src/Viewport.ts index 32a71dde..3aa1319f 100644 --- a/src/Viewport.ts +++ b/src/Viewport.ts @@ -12,7 +12,6 @@ export class Viewport { private currentRowHeight: number; private lastRecordedBufferLength: number; private lastRecordedViewportHeight: number; - private isRefreshQueued: boolean; /** * Creates a new Viewport. @@ -30,25 +29,12 @@ export class Viewport { this.currentRowHeight = 0; this.lastRecordedBufferLength = 0; this.lastRecordedViewportHeight = 0; - this.isRefreshQueued = false; this.terminal.on('scroll', this.syncScrollArea.bind(this)); this.terminal.on('resize', this.syncScrollArea.bind(this)); this.viewportElement.addEventListener('scroll', this.onScroll.bind(this)); this.syncScrollArea(); - this.refreshLoop(); - } - - /** - * Queues a refresh to be done on next animation frame. - */ - private refreshLoop(): void { - if (this.isRefreshQueued) { - this.refresh(); - this.isRefreshQueued = false; - } - window.requestAnimationFrame(this.refreshLoop.bind(this)); } /** @@ -82,15 +68,15 @@ export class Viewport { if (this.lastRecordedBufferLength !== this.terminal.lines.length) { // If buffer height changed this.lastRecordedBufferLength = this.terminal.lines.length; - this.isRefreshQueued = true; + this.refresh(); } else if (this.lastRecordedViewportHeight !== this.terminal.rows) { // If viewport height changed - this.isRefreshQueued = true; + this.refresh(); } else { // If size has changed, refresh viewport var size = this.charMeasureElement.getBoundingClientRect(); if (size.height !== this.currentRowHeight) { - this.isRefreshQueued = true; + this.refresh(size); } }