From 23cfa3d8240291d3d854392f0c50db7877818d63 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 7 Jun 2016 22:12:38 -0700 Subject: [PATCH 01/10] Add IP addresses to linkify Fixes #91 --- addons/linkify/linkify.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/addons/linkify/linkify.js b/addons/linkify/linkify.js index 152404d4..8b222564 100644 --- a/addons/linkify/linkify.js +++ b/addons/linkify/linkify.js @@ -30,8 +30,10 @@ negatedDomainCharacterSet = '[^\\da-z\\.-]+', domainBodyClause = '(' + domainCharacterSet + ')', tldClause = '([a-z\\.]{2,6})', - hostClause = domainBodyClause + '\\.' + tldClause, - pathClause = '([\\/\\w\\.-]*)*\\/?', + ipClause = '((\\d{1,3}\\.){3}\\d{1,3})', + portClause = '(:\\d{1,5})', + hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|(' + ipClause + portClause + '?))', + pathClause = '(\\/[\\/\\w\\.-]*)*', negatedPathCharacterSet = '[^\\/\\w\\.-]+', bodyClause = hostClause + pathClause, start = '(?:^|' + negatedDomainCharacterSet + ')(', @@ -41,7 +43,6 @@ lenientUrlRegex = new RegExp(lenientUrlClause), strictUrlRegex = new RegExp(strictUrlClause); - /** * Converts all valid URLs found in the given terminal line into * hyperlinks. The terminal line can be either the HTML element itself From edf897182ea53d2c941591eae8c1a1534a946323 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 10 Jun 2016 11:51:00 -0700 Subject: [PATCH 02/10] 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 9c3b1105e28e829d82f70690fb1170eb89a467d1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 10 Jun 2016 13:15:59 -0700 Subject: [PATCH 03/10] Check optional port clause after domain too --- addons/linkify/linkify.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/linkify/linkify.js b/addons/linkify/linkify.js index 8b222564..344bc711 100644 --- a/addons/linkify/linkify.js +++ b/addons/linkify/linkify.js @@ -32,7 +32,7 @@ tldClause = '([a-z\\.]{2,6})', ipClause = '((\\d{1,3}\\.){3}\\d{1,3})', portClause = '(:\\d{1,5})', - hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|(' + ipClause + portClause + '?))', + hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + ')' + portClause + '?', pathClause = '(\\/[\\/\\w\\.-]*)*', negatedPathCharacterSet = '[^\\/\\w\\.-]+', bodyClause = hostClause + pathClause, From f2f0f460c4ecf5dd61097937bf4292cf77d516c7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 10 Jun 2016 18:53:32 -0700 Subject: [PATCH 04/10] Add a bunch of tests --- addons/linkify/linkify.js | 29 ++++++++++++---------- package.json | 2 +- test/addons/linkify-test.js | 48 +++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 test/addons/linkify-test.js diff --git a/addons/linkify/linkify.js b/addons/linkify/linkify.js index 344bc711..f02c2023 100644 --- a/addons/linkify/linkify.js +++ b/addons/linkify/linkify.js @@ -91,22 +91,13 @@ continue; } + var url = exports.findLinkMatch(node.data, lenient); - if (lenient) { - match = node.data.match(lenientUrlRegex); - } else { - match = node.data.match(strictUrlRegex); - } - - /** - * If no URL was found in the current text, return. - */ - if (!match) { + if (!url) { continue; } - var url = match[1], - startsWithProtocol = new RegExp('^' + protocolClause), + var startsWithProtocol = new RegExp('^' + protocolClause), urlHasProtocol = url.match(startsWithProtocol), href = (urlHasProtocol) ? url : 'http://' + url, link = '' + url + '', @@ -125,6 +116,20 @@ terminal.emit('linkify:line', line); }; + /** + * Finds a link within a block of text. + * + * @param {string} text - The text to search . + * @param {boolean} lenient - Whether to use the lenient search. + * @return {string} A URL. + */ + exports.findLinkMatch = function (text, lenient) { + var match = text.match(lenient ? lenientUrlRegex : strictUrlRegex); + if (!match || match.length === 0) { + return null; + } + return match[1]; + } /** * Converts all valid URLs found in the terminal view into hyperlinks. diff --git a/package.json b/package.json index 73fde3c7..f1d3e577 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,6 @@ }, "scripts": { "start": "bash bin/server", - "test": "bash bin/test" + "test": "bash bin/test --recursive" } } diff --git a/test/addons/linkify-test.js b/test/addons/linkify-test.js new file mode 100644 index 00000000..7cb23186 --- /dev/null +++ b/test/addons/linkify-test.js @@ -0,0 +1,48 @@ +var assert = require('chai').assert; +var Terminal = require('../../src/xterm'); +var linkify = require('../../addons/linkify/linkify'); + +describe('linkify addon', function () { + var xterm; + + describe('API', function () { + it('should define Terminal.prototype.linkify', function () { + assert.isDefined(Terminal.prototype.linkify); + }); + it('should define Terminal.prototype.linkifyTerminalLine', function () { + assert.isDefined(Terminal.prototype.linkifyTerminalLine); + }); + }); + + describe('findUrlMatchOnLine', function () { + describe('strict regex', function () { + it('should match when the entire text is a match', function () { + assert.equal(linkify.findLinkMatch('http://github.com', false), 'http://github.com'); + assert.equal(linkify.findLinkMatch('http://127.0.0.1', false), 'http://127.0.0.1'); + }); + it('should match simple domains', function () { + assert.equal(linkify.findLinkMatch('foo http://github.com bar', false), 'http://github.com'); + assert.equal(linkify.findLinkMatch('foo http://www.github.com bar', false), 'http://www.github.com'); + assert.equal(linkify.findLinkMatch('foo https://github.com bar', false), 'https://github.com'); + assert.equal(linkify.findLinkMatch('foo https://www.github.com bar', false), 'https://www.github.com'); + }); + it('should match web addresses with alpha paths', function () { + assert.equal(linkify.findLinkMatch('foo http://github.com/a/b/c bar', false), 'http://github.com/a/b/c'); + assert.equal(linkify.findLinkMatch('foo http://www.github.com/a/b/c bar', false), 'http://www.github.com/a/b/c'); + }); + it('should not include whitespace surrounding a match', function () { + assert.equal(linkify.findLinkMatch(' http://github.com', false), 'http://github.com'); + assert.equal(linkify.findLinkMatch('http://github.com ', false), 'http://github.com'); + assert.equal(linkify.findLinkMatch(' http://github.com ', false), 'http://github.com'); + }); + it('should match IP addresses', function () { + assert.equal(linkify.findLinkMatch('foo http://127.0.0.1 bar', false), 'http://127.0.0.1'); + assert.equal(linkify.findLinkMatch('foo https://127.0.0.1 bar', false), 'https://127.0.0.1'); + }); + it('should match ports on both domains and IP addresses', function () { + assert.equal(linkify.findLinkMatch('foo http://127.0.0.1:8080 bar', false), 'http://127.0.0.1:8080'); + assert.equal(linkify.findLinkMatch('foo http://www.github.com:8080 bar', false), 'http://www.github.com:8080'); + }); + }); + }); +}); From e0c0fb6917cae9bf0d6dca45167170166f1afa1a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 10 Jun 2016 19:15:52 -0700 Subject: [PATCH 05/10] Add basic options selection to demo This will make it easier to test options without needing to modify the demo. --- demo/index.html | 6 +++++- demo/main.js | 43 ++++++++++++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/demo/index.html b/demo/index.html index 92ccc94c..ff10a5cb 100644 --- a/demo/index.html +++ b/demo/index.html @@ -9,12 +9,16 @@ -

xterm.js: xterm, in the browser

+
+

Options

+ +
+ diff --git a/demo/main.js b/demo/main.js index 70d64c59..59bdacdd 100644 --- a/demo/main.js +++ b/demo/main.js @@ -1,11 +1,36 @@ -var terminalContainer = document.getElementById('terminal-container'), - term = new Terminal(), - protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://', - socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/bash', - socket = new WebSocket(socketURL); +var term, + protocol, + socketURL, + socket; + +var terminalContainer = document.getElementById('terminal-container'); +var optionElements = { + cursorBlink: document.querySelector('#option-cursor-blink') +}; + +optionElements.cursorBlink.addEventListener('change', createTerminal); + +createTerminal(); + +function createTerminal() { + while (terminalContainer.children.length) { + terminalContainer.removeChild(terminalContainer.children[0]); + } + term = new Terminal({ + cursorBlink: optionElements.cursorBlink.checked + }); + protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://'; + socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/bash'; + socket = new WebSocket(socketURL); + + term.open(terminalContainer); + term.fit(); + + socket.onopen = runRealTerminal; + socket.onclose = runFakeTerminal; + socket.onerror = runFakeTerminal; +} -term.open(terminalContainer); -term.fit(); function runRealTerminal() { term.attach(socket); @@ -54,7 +79,3 @@ function runFakeTerminal() { term.write(data); }); } - -socket.onopen = runRealTerminal; -socket.onclose = runFakeTerminal; -socket.onerror = runFakeTerminal; From fd5be55d813ba80ef6b1d31fdcf74b331c4aeed3 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 10 Jun 2016 20:05:26 -0700 Subject: [PATCH 06/10] Add some jsdoc for important functions Part of #13 --- src/xterm.js | 58 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 7f01e2a4..3d29a66f 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -142,6 +142,14 @@ * Terminal */ + /** + * Creates a new `Terminal` object. + * + * @param {object} options An object containing a set of options, the available options are: + * - cursorBlink (boolean): Whether the terminal cursor blinks + * + * @public + */ function Terminal(options) { var self = this; @@ -408,7 +416,9 @@ }); /** - * Focused Terminal + * Focus the terminal. + * + * @public */ Terminal.prototype.focus = function() { if (document.activeElement === this.element) { @@ -636,8 +646,12 @@ }; - /* - * Open Terminal in the DOM + /** + * Opens the terminal within an element. + * + * @param {HTMLElement} parent The element to create the terminal within. + * + * @public */ Terminal.prototype.open = function(parent) { var self=this, i=0, div; @@ -1084,9 +1098,10 @@ }; /** - * Destroy Terminal + * Destroys the terminal. + * + * @public */ - Terminal.prototype.destroy = function() { this.readable = false; this.writable = false; @@ -1124,6 +1139,15 @@ * Next 14 bits: a mask for misc. flags: * 1=bold, 2=underline, 4=blink, 8=inverse, 16=invisible */ + + /** + * Refreshes terminal content within two rows (inclusive). + * + * @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) + * + * @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; @@ -1365,6 +1389,13 @@ this.refresh(0, this.rows - 1); }; + /** + * Writes text to the terminal. + * + * @param {string} text The text to write to the terminal. + * + * @public + */ Terminal.prototype.write = function(data) { var l = data.length, i = 0, j, cs, ch; @@ -2654,6 +2685,14 @@ this.context.console.error.apply(this.context.console, args); }; + /** + * Resizes the terminal. + * + * @param {number} x The number of columns to resize to. + * @param {number} y The number of rows to resize to. + * + * @public + */ Terminal.prototype.resize = function(x, y) { var line , el @@ -4576,6 +4615,15 @@ Terminal.EventEmitter = EventEmitter; Terminal.inherits = inherits; + + /** + * Adds an event listener to the terminal. + * + * @param {string} event The name of the event. TODO: Document all event types + * @param {function} callback The function to call when the event is triggered. + * + * @public + */ Terminal.on = on; Terminal.off = off; Terminal.cancel = cancel; From 57300f51598305a436b53368f67bc20d8517bc87 Mon Sep 17 00:00:00 2001 From: Paris Date: Mon, 13 Jun 2016 19:07:29 +0300 Subject: [PATCH 07/10] 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 08/10] 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 09/10] 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 10/10] 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" } }