From edf897182ea53d2c941591eae8c1a1534a946323 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 10 Jun 2016 11:51:00 -0700 Subject: [PATCH 01/11] Bound refresh max rows to this.rows not this.lines this.row is the size of rows in the viewport, this.lines is the buffer. It's only possible to refresh `0` to `this.rows - 1`. Fixes #86 --- src/xterm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 7f01e2a4..8fc154fe 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1135,9 +1135,9 @@ width = this.cols; y = start; - if (end >= this.lines.length) { + if (end >= this.rows.length) { this.log('`end` is too large. Most likely a bad CSR.'); - end = this.lines.length - 1; + end = this.rows.length - 1; } for (; y <= end; y++) { From 4a71a413807526ca7d4cc8bf8699d5430694c911 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 10 Jun 2016 18:01:34 -0700 Subject: [PATCH 02/11] Fix cursor blinking when enabled Fixes #119 --- src/xterm.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 7f01e2a4..3be3244d 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1283,7 +1283,7 @@ }; Terminal.prototype._cursorBlink = function() { - if (Terminal.focus !== this) return; + if (document.activeElement !== this.element) return; this.cursorState ^= 1; this.refresh(this.y, this.y); }; @@ -1293,8 +1293,7 @@ this.cursorState = 1; this.refresh(this.y, this.y); } else { - // Temporarily disabled: - // this.refreshBlink(); + this.refreshBlink(); } }; From cb4728f8f5beb9b5459fa9d623ad98fa8a186ec6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 10 Jun 2016 18:05:24 -0700 Subject: [PATCH 03/11] Tidy up code --- src/xterm.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 3be3244d..22106e5e 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1299,17 +1299,13 @@ Terminal.prototype.startBlink = function() { if (!this.cursorBlink) return; - var self = this; - this._blinker = function() { - self._cursorBlink(); - }; - this._blink = setInterval(this._blinker, 500); + this._blink = setInterval(this._cursorBlink.bind(this), 500); }; Terminal.prototype.refreshBlink = function() { if (!this.cursorBlink) return; clearInterval(this._blink); - this._blink = setInterval(this._blinker, 500); + this._blink = setInterval(this._cursorBlink.bind(this), 500); }; Terminal.prototype.scroll = function() { From 57300f51598305a436b53368f67bc20d8517bc87 Mon Sep 17 00:00:00 2001 From: Paris Date: Mon, 13 Jun 2016 19:07:29 +0300 Subject: [PATCH 04/11] Implement addon loader (CommonJS + RequireJS) Closes #96 --- src/xterm.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/xterm.js b/src/xterm.js index 3d29a66f..5c20dccf 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -759,6 +759,26 @@ this.emit('open'); }; + + /** + * Attempts to load an add-on using CommonJS or RequireJS (whichever is available). + * @param {string} addon The name of the addon to load + * @static + */ + Terminal.loadAddon = function(addon, callback) { + if (typeof exports === 'object' && typeof module === 'object') { + // CommonJS + return require(__dirname + '/../addons/' + addon); + } else if (typeof define == 'function') { + // RequireJS + return require(['../addons/' + addon + '/' + addon], callback); + } else { + console.error('Cannot load a module without a CommonJS or RequireJS environment.'); + return false; + } + }; + + // XTerm mouse events // http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking // To better understand these From af29effbf3f743eb35c15980c25192cb4e59ba27 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 13 Jun 2016 12:37:16 -0700 Subject: [PATCH 05/11] Allow refresh to execute 30 times a second maximum For commands that pass a significant amount of output to the write function, this prevents the terminal 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. Fixes #127 Fixes #126 --- src/xterm.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/xterm.js b/src/xterm.js index 3d29a66f..5a275b8b 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -230,6 +230,16 @@ */ this.y = 0; + /** + * Used to debounce the refresh function + */ + this.isRefreshing = false; + + /** + * Whether there is a full terminal refresh queued + */ + this.queuedRefresh = false; + this.cursorState = 0; this.cursorHidden = false; this.convertEol; @@ -297,6 +307,7 @@ this.tabs; this.setupStops(); + this.debounceRefresh(); } inherits(Terminal, EventEmitter); @@ -307,6 +318,26 @@ return (this.defAttr & ~0x1ff) | (this.curAttr & 0x1ff); }; + /** + * Allow 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. + */ + Terminal.prototype.debounceRefresh = function () { + var self = this; + window.setInterval(function () { + self.isRefreshing = false; + if (self.queuedRefresh) { + // Do a full refresh in case multiple refreshes were requested. + self.refresh(0, self.rows - 1); + } + }, 34); + }; + /** * Colors */ @@ -1151,6 +1182,12 @@ Terminal.prototype.refresh = function(start, end) { var x, y, i, line, out, ch, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement; + if (this.isRefreshing) { + this.queuedRefresh = true; + return; + } + this.isRefreshing = true; + if (end - start >= this.rows / 2) { parent = this.element.parentNode; if (parent) parent.removeChild(this.element); From 00f380a75c6ee4d379a39a9bddd082e801ee4da1 Mon Sep 17 00:00:00 2001 From: Paris Date: Tue, 14 Jun 2016 00:05:47 +0300 Subject: [PATCH 06/11] Implement test --- test/addons/test.js | 10 ++++++++++ test/test.js | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 test/addons/test.js diff --git a/test/addons/test.js b/test/addons/test.js new file mode 100644 index 00000000..ba689e6e --- /dev/null +++ b/test/addons/test.js @@ -0,0 +1,10 @@ +var assert = require('chai').assert; +var Terminal = require('../../src/xterm'); + +describe('xterm.js addons', function() { + it('should load addons with Terminal.loadAddon', function () { + Terminal.loadAddon('attach'); + // Test that function was loaded successfully + assert.equal(typeof Terminal.prototype.attach, 'function'); + }); +}); diff --git a/test/test.js b/test/test.js index 6f7a0791..fcfd8832 100644 --- a/test/test.js +++ b/test/test.js @@ -79,7 +79,7 @@ describe('xterm.js', function() { xterm.handler = function() {}; xterm.showCursor = function() {}; xterm.clearSelection = function() {}; - }) + }); describe('On Mac OS', function() { beforeEach(function() { From be304c6ee7e1755be6db48fa7bba81ac4f031a77 Mon Sep 17 00:00:00 2001 From: Paris Date: Tue, 14 Jun 2016 00:21:17 +0300 Subject: [PATCH 07/11] Implement docs building --- .gitignore | 2 +- conf.json | 14 -------------- jsdoc.json | 23 +++++++++++++++++++++++ package.json | 7 +++++-- 4 files changed, 29 insertions(+), 17 deletions(-) delete mode 100644 conf.json create mode 100644 jsdoc.json diff --git a/.gitignore b/.gitignore index af5c7e2e..029efd56 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,5 @@ Makefile.gyp *.target.gyp.mk *.node example/*.log -docs/_build +docs/ npm-debug.log diff --git a/conf.json b/conf.json deleted file mode 100644 index b5c8e525..00000000 --- a/conf.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "source": { - "include": [ - "src/xterm.js", - "addons/attach/attach.js", - "addons/fit/fit.js", - "addons/fullscreen/fullscreen.js", - "addons/linkify/linkify.js" - ] - }, - "opts": { - "readme": "README.md" - } -} \ No newline at end of file diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 00000000..28ca16cb --- /dev/null +++ b/jsdoc.json @@ -0,0 +1,23 @@ +{ + "source": { + "include": [ + "src/xterm.js", + "addons/attach/attach.js", + "addons/fit/fit.js", + "addons/fullscreen/fullscreen.js", + "addons/linkify/linkify.js" + ] + }, + "opts": { + "readme": "README.md", + "template": "node_modules/docdash", + "encoding": "utf8", + "destination": "docs/", + "recurse": true, + "verbose": true + }, + "templates": { + "cleverLinks": false, + "monospaceLinks": false + } +} diff --git a/package.json b/package.json index f1d3e577..1b98517c 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,13 @@ "express-ws": "2.0.0-rc.1", "pty.js": "0.3.0", "mocha": "2.5.3", - "chai": "3.5.0" + "chai": "3.5.0", + "jsdoc": "3.4.0", + "docdash": "0.4.0" }, "scripts": { "start": "bash bin/server", - "test": "bash bin/test --recursive" + "test": "bash bin/test --recursive", + "build:docs": "node_modules/.bin/jsdoc -c jsdoc.json" } } From fac964bcf5737c9cbd18e9edda0917c88edc0d2b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 14 Jun 2016 10:33:24 -0700 Subject: [PATCH 08/11] Only refresh when asked Fixes #132 --- src/xterm.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/xterm.js b/src/xterm.js index 5d1fba5c..f77f6902 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -332,6 +332,7 @@ window.setInterval(function () { self.isRefreshing = false; if (self.queuedRefresh) { + self.queuedRefresh = false; // Do a full refresh in case multiple refreshes were requested. self.refresh(0, self.rows - 1); } From 0d803ac895e33b48ade3d30a664c99787362401f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 14 Jun 2016 10:52:49 -0700 Subject: [PATCH 09/11] Use CSS animations --- src/xterm.css | 15 +++++++++++++++ src/xterm.js | 28 +++++----------------------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/xterm.css b/src/xterm.css index 5a325ed9..1f49d690 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -57,6 +57,21 @@ background-color: transparent; } +.terminal .terminal-cursor.blinking { + animation: blink-cursor 1.2s infinite step-end; +} + +@keyframes blink-cursor { + 0% { + background-color: #fff; + color: #000; + } + 50% { + background-color: transparent; + color: #FFF; + } +} + /* * Determine default colors for xterm.js */ diff --git a/src/xterm.js b/src/xterm.js index 1ac384c8..a11c81ea 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -765,9 +765,6 @@ // Ensure there is a Terminal.focus. this.focus(); - // Start blinking the cursor. - this.startBlink(); - on(this.element, 'mouseup', function() { var selection = document.getSelection(), collapsed = selection.isCollapsed, @@ -1250,7 +1247,11 @@ } if (data !== this.defAttr) { if (data === -1) { - out += ''; + out += ''; } else { var classNames = []; @@ -1363,32 +1364,13 @@ this.emit('refresh', {element: this.element, start: start, end: end}); }; - Terminal.prototype._cursorBlink = function() { - if (document.activeElement !== this.element) return; - this.cursorState ^= 1; - this.refresh(this.y, this.y); - }; - Terminal.prototype.showCursor = function() { if (!this.cursorState) { this.cursorState = 1; this.refresh(this.y, this.y); - } else { - this.refreshBlink(); } }; - Terminal.prototype.startBlink = function() { - if (!this.cursorBlink) return; - this._blink = setInterval(this._cursorBlink.bind(this), 500); - }; - - Terminal.prototype.refreshBlink = function() { - if (!this.cursorBlink) return; - clearInterval(this._blink); - this._blink = setInterval(this._cursorBlink.bind(this), 500); - }; - Terminal.prototype.scroll = function() { var row; From a6e85ad5f5aa224cd5770a57806f1219779719d4 Mon Sep 17 00:00:00 2001 From: Paris Date: Thu, 16 Jun 2016 11:41:41 +0300 Subject: [PATCH 10/11] Progress with #120 --- src/xterm.js | 135 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 79 insertions(+), 56 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index d454c4bf..96e89e3b 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -238,7 +238,6 @@ /** * Whether there is a full terminal refresh queued */ - this.queuedRefresh = false; this.cursorState = 0; this.cursorHidden = false; @@ -307,7 +306,6 @@ this.tabs; this.setupStops(); - this.debounceRefresh(); } inherits(Terminal, EventEmitter); @@ -318,27 +316,6 @@ return (this.defAttr & ~0x1ff) | (this.curAttr & 0x1ff); }; - /** - * Allow 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. - */ - Terminal.prototype.debounceRefresh = function () { - var self = this; - window.setInterval(function () { - self.isRefreshing = false; - if (self.queuedRefresh) { - self.queuedRefresh = false; - // Do a full refresh in case multiple refreshes were requested. - self.refresh(0, self.rows - 1); - } - }, 34); - }; - /** * Colors */ @@ -448,36 +425,53 @@ }); /** - * Focus the terminal. + * Focus the terminal. Delegates focus handling to the terminal's DOM element. * * @public */ Terminal.prototype.focus = function() { - if (document.activeElement === this.element) { - return; - } - - if (this.sendFocus) { - this.send('\x1b[I'); - } - - this.showCursor(); - this.element.focus(); + return this.element.focus(); }; + /** + * Binds the desired focus behavior on a given terminal object. + * + * @static + */ + Terminal.bindFocus = function (term) { + on(term.element, 'focus', function (ev) { + if (term.sendFocus) { + term.send('\x1b[I'); + } + + term.showCursor(); + Terminal.focus = term; + term.emit('focus', {terminal: term}); + }); + }; + + /** + * Blur the terminal. Delegates blur handling to the terminal's DOM element. + * + * @public + */ Terminal.prototype.blur = function() { - if (Terminal.focus !== this) { - return; - } + return terminal.element.blur(); + }; - this.cursorState = 0; - this.refresh(this.y, this.y); - this.element.blur(); - - if (this.sendFocus) { - this.send('\x1b[O'); - } - Terminal.focus = null; + /** + * Binds the desired blur behavior on a given terminal object. + * + * @static + */ + Terminal.bindBlur = function (term) { + on(term.element, 'blur', function (ev) { + if (term.sendFocus) { + term.send('\x1b[O'); + } + Terminal.focus = null; + term.emit('blur', {terminal: term}); + }); }; /** @@ -489,6 +483,8 @@ Terminal.bindCopy(this); Terminal.bindCut(this); Terminal.bindDrop(this); + Terminal.bindFocus(this); + Terminal.bindBlur(this); }; /** @@ -735,12 +731,6 @@ this.element.classList.add('xterm-theme-' + this.theme); this.element.setAttribute('tabindex', 0); this.element.spellcheck = 'false'; - this.element.onfocus = function() { - self.emit('focus', {terminal: this}); - }; - this.element.onblur = function() { - self.emit('blur', {terminal: this}); - }; /* * Create the container that will hold the lines of the terminal and then @@ -1194,17 +1184,50 @@ * * @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 * * @public */ - Terminal.prototype.refresh = function(start, end) { - var x, y, i, line, out, ch, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement; + Terminal.prototype.refresh = function(start, end, queue) { + var self = this; - if (this.isRefreshing) { - this.queuedRefresh = true; + // 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; + } return; } - this.isRefreshing = true; + + // 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) { + start = 0; + end = this.rows - 1; + this._fullRefreshNext = false // reset lock + } + + var x, y, i, line, out, ch, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement; if (end - start >= this.rows / 2) { parent = this.element.parentNode; From af7588ef16503f8fcf09c06a143d24cbe629ef72 Mon Sep 17 00:00:00 2001 From: Paris Date: Thu, 16 Jun 2016 11:47:05 +0300 Subject: [PATCH 11/11] On big refreshes remove `term.rowContainer` instead of `term.element` from DOM Fix #120 --- src/xterm.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 96e89e3b..6319570d 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1229,9 +1229,12 @@ var x, y, i, line, out, ch, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement; + // If this is a big refresh, remove the terminal rows from the DOM for faster calculations if (end - start >= this.rows / 2) { parent = this.element.parentNode; - if (parent) parent.removeChild(this.element); + if (parent) { + this.element.removeChild(this.rowContainer); + } } width = this.cols; @@ -1378,13 +1381,9 @@ } if (parent) { - parent.appendChild(this.element); + this.element.appendChild(this.rowContainer); } - /* - * Return focus to previously focused element - */ - focused.focus(); this.emit('refresh', {element: this.element, start: start, end: end}); };