From 3e1a60717967f2a661e1c1bfe77f9ff06f8debff Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 31 Dec 2016 07:36:56 -0800 Subject: [PATCH 1/7] 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 cd13e02758..0000000000 --- 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 0000000000..04da698ec2 --- /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 42f876f372..ce09c1bef0 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 2/7] 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 1d2206529e..79ca633b37 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 3/7] 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 79ca633b37..cd3041867e 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 6ffb8f545b6a8f21883ebaadcb48f81e23e27ad0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 3 Jan 2017 11:18:38 -0800 Subject: [PATCH 4/7] 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 3aa1319fb1..b266dd3e78 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 5/7] jsdoc --- src/Viewport.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Viewport.ts b/src/Viewport.ts index b266dd3e78..32a71dde0b 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 6/7] 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 5b106b4345..fc2cd1174c 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 520f2bc9d6365c7034b9519cc2b0c98c71bd6f39 Mon Sep 17 00:00:00 2001 From: Tyler Jewell Date: Sun, 8 Jan 2017 09:44:14 -0800 Subject: [PATCH 7/7] 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 b5d30041cd..608c48ac6e 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.