From 670b17914041b71613bf93019630126e34108416 Mon Sep 17 00:00:00 2001 From: Paris Date: Tue, 7 Jun 2016 12:23:28 +0300 Subject: [PATCH 01/28] Set line height of terminal rows to normal Fix #76 --- src/xterm.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xterm.css b/src/xterm.css index 055c0fc0..ba2298af 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -2123,7 +2123,7 @@ * in order to allow child elements to adjust. */ .terminal .xterm-rows > div { - line-height: 1.3; + line-height: normal; } /** From d82c008052d06276a113be85de086c65f708d59c Mon Sep 17 00:00:00 2001 From: Paris Date: Tue, 7 Jun 2016 12:06:34 +0300 Subject: [PATCH 02/28] Make cursor hollow on blur Fix #79 --- demo/style.css | 2 +- src/xterm.css | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/demo/style.css b/demo/style.css index dae6e914..c93ca74f 100644 --- a/demo/style.css +++ b/demo/style.css @@ -21,6 +21,6 @@ h1 { padding: 2px; } -#terminal-container .terminal .terminal-cursor { +#terminal-container .terminal:focus .terminal-cursor { background-color: #fafafa; } diff --git a/src/xterm.css b/src/xterm.css index ba2298af..06edf045 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -51,6 +51,12 @@ color: #000; } +.terminal:not(:focus) .terminal-cursor { + outline: 1px solid #fff; + outline-offset: -1px; + background-color: transparent; +} + /* * Determine default colors for xterm.js */ From 6bc1881727ea3b44436495f05c9033528057aa76 Mon Sep 17 00:00:00 2001 From: TDaglis Date: Tue, 7 Jun 2016 13:58:00 +0300 Subject: [PATCH 03/28] Fix emission of terminal focus&blur events --- src/xterm.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index a0d4fd09..9f9b85ae 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -401,7 +401,6 @@ this.showCursor(); this.element.focus(); - this.emit('focus', {terminal: this}); }; Terminal.prototype.blur = function() { @@ -417,7 +416,6 @@ this.send('\x1b[O'); } Terminal.focus = null; - this.emit('blur', {terminal: this}); }; /** @@ -627,6 +625,12 @@ 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 From 47b01786e2e4dc47dc90a986793482827a710ff5 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 7 Jun 2016 16:43:33 -0700 Subject: [PATCH 04/28] Add http:// to demo output address This makes it easy to launch the demo in a browser by right clicking the URL in gnome-terminal --- demo/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/app.js b/demo/app.js index a7b79478..77038cc2 100644 --- a/demo/app.js +++ b/demo/app.js @@ -45,5 +45,5 @@ app.ws('/bash', function(ws, req) { var port = process.env.PORT || 3000, host = '0.0.0.0'; -console.log('App listening to ' + host + ':' + port); +console.log('App listening to http://' + host + ':' + port); app.listen(port, host); From 289ff8092dcc94434dc13749725a83dd693fbd23 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 7 Jun 2016 17:08:37 -0700 Subject: [PATCH 05/28] Add demo section to README Fixes #87 --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 14044416..86ac0412 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,18 @@ Xterm.js supplies a modular, event-based interface that lets developers build ad ![xterm.js screenshot](xtermjs.png) -### Contribution and License Agreement +## Demo + +To launch the demo simply run: + +``` +npm install +npm start +``` + +Then open http://0.0.0.0:3000 in a web browser. + +## Contribution and 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 dab80b58ae8745000f6057c57dcaee78985616c7 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 7 Jun 2016 22:31:39 -0700 Subject: [PATCH 06/28] Add section on addons to README --- README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.md b/README.md index 14044416..80e55ad7 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,23 @@ Xterm.js supplies a modular, event-based interface that lets developers build ad ![xterm.js screenshot](xtermjs.png) +## Addons + +Addons are JavaScript modules that attach functions to the `Terminal` prototype to extend its functionality. There are a handful available in the main repository in the `addons` directory, you can even write your own (though they may break when the internals of xterm.js change across versions). + +To use an addon, just include the JavaScript file after xterm.js and before the `Terminal` object has been instantiated. The function should then be exposed on the `Terminal` object: + +```html + + +``` + +```js +var xterm = new Terminal(); +// init code... +xterm.linkify(); +``` + ### Contribution and 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 383d59bbc30af72497edf6c0ae3fda5c5ca1ebf8 Mon Sep 17 00:00:00 2001 From: Paris Date: Wed, 8 Jun 2016 22:03:45 +0300 Subject: [PATCH 07/28] Stop marking spans as `inline-block` (not needed after 670b179140) Fix #85 --- src/xterm.css | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/xterm.css b/src/xterm.css index 06edf045..5a325ed9 100644 --- a/src/xterm.css +++ b/src/xterm.css @@ -2131,11 +2131,3 @@ .terminal .xterm-rows > div { line-height: normal; } - -/** - * All styled spans inside terminal lines should be inline-blocks, - * in orde to achieve better height adjustment. - */ -.terminal .xterm-rows span { - display: inline-block; -} From e721bdc9f07a11868250dfb3cdced59d9b7203f4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 8 Jun 2016 17:38:34 -0700 Subject: [PATCH 08/28] Don't resize when unnecessary --- src/xterm.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index a0d4fd09..fbfcd3da 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -2585,6 +2585,10 @@ , j , ch; + if (x === this.cols && y === this.rows) { + return; + } + if (x < 1) x = 1; if (y < 1) y = 1; @@ -2598,7 +2602,7 @@ this.lines[i].push(ch); } } - } else if (j > x) { + } else { // (j > x) i = this.lines.length; while (i--) { while (this.lines[i].length > x) { @@ -2621,7 +2625,7 @@ this.insertRow(); } } - } else if (j > y) { + } else { // (j > y) while (j-- > y) { if (this.lines.length > y + this.ybase) { this.lines.shift(); From df268ad5cdf7f6154769585e0f4242f01bcfee70 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 8 Jun 2016 19:08:30 -0700 Subject: [PATCH 09/28] Fix buffer corruption after resizing rows Lines from the buffer were incorrectly being removed when the viewport was resized. This change removes the lines when the cursor is above them, if not it shifts the viewport down. Some basic jsdoc comments were added to some Terminal properties for future reference. Fixes #98 Fixes #99 --- src/xterm.js | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/xterm.js b/src/xterm.js index 9f9b85ae..8093043a 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -202,10 +202,26 @@ this.on('data', options.handler); } + /** + * The scroll position of the y cursor, ie. ybase + y = the y position within the entire + */ this.ybase = 0; + + /** + * The scroll position of the viewport + */ this.ydisp = 0; + + /** + * The cursor's x position after ybase + */ this.x = 0; + + /** + * The cursor's y position after ybase + */ this.y = 0; + this.cursorState = 0; this.cursorHidden = false; this.convertEol; @@ -261,6 +277,10 @@ this.prefix = ''; this.postfix = ''; + /** + * An array of all lines in the entire buffer, including the prompt. The lines are array of + * characters which are 2-length arrays where [0] is an attribute and [1] is the character. + */ this.lines = []; var i = this.rows; while (i--) { @@ -2628,7 +2648,14 @@ } else if (j > y) { while (j-- > y) { if (this.lines.length > y + this.ybase) { - this.lines.shift(); + if (this.y + this.ybase < j) { + // The line is after the cursor, remove it + this.lines.pop(); + } else { + // The line is the cursor, push the viewport down + this.ybase++; + this.ydisp++; + } } if (this.children.length > y) { el = this.children.shift(); From 7a80efd8bd8f82ea8f59993c1a775371925fb6d1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 8 Jun 2016 19:28:55 -0700 Subject: [PATCH 10/28] Don't crash demo server on refresh Fixes #100 --- demo/app.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/demo/app.js b/demo/app.js index 77038cc2..c48f6091 100644 --- a/demo/app.js +++ b/demo/app.js @@ -29,9 +29,12 @@ app.ws('/bash', function(ws, req) { cwd: process.env.PWD, env: process.env }); - term.on('data', function(data) { - ws.send(data); + try { + ws.send(data); + } catch (ex) { + // The WebSocket is not open, ignore + } }); ws.on('message', function(msg) { term.write(msg); From f87300d30e8bcbefc5e7b62545fb956598de99e2 Mon Sep 17 00:00:00 2001 From: Paris Date: Thu, 9 Jun 2016 09:49:11 +0300 Subject: [PATCH 11/28] Remove old test-suite completely --- test/bench.js | 49 ------------------------------------------------- test/data.diff | 10 ---------- test/index.html | 49 ------------------------------------------------- test/index.js | 33 --------------------------------- 4 files changed, 141 deletions(-) delete mode 100644 test/bench.js delete mode 100644 test/data.diff delete mode 100644 test/index.html delete mode 100644 test/index.js diff --git a/test/bench.js b/test/bench.js deleted file mode 100644 index 17d54df0..00000000 --- a/test/bench.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * term.js - * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) - */ - -var element = { - createElement: function() { return element; }, - appendChild: function() {}, - removeChild: function() {}, - addEventListener: function() {}, - removeEventListener: function() {}, - setAttribute: function() {}, - style: {} -}; - -global.window = global; -window.navigator = { userAgent: '' }; -window.document = element; -window.document.body = element; - -element.ownerDocument = window.document; -window.document.defaultView = window; - -var Terminal = require('../src/term'); -Terminal.cursorBlink = false; - -var data = require('./data').data; - -var term = new Terminal({ - cols: 250, - rows: 100 -}); - -term.open(element); - -var time = new Date; -var t = 10; - -while (t--) { - var l = data.length - , i = 0; - - for (; i < l; i++) { - term.write(data[i]); - } -} - -console.log('Completed: %d.', new Date - time); -console.log('Average (?): 13.5k (for ~2.7k writes).'); diff --git a/test/data.diff b/test/data.diff deleted file mode 100644 index fcd8e61a..00000000 --- a/test/data.diff +++ /dev/null @@ -1,10 +0,0 @@ -167a168,170 -> var stream = fs.createWriteStream(__dirname + '/../test/data.js'); -> stream.write('this.data = [\n'); -> -169a173 -> stream.write(' ' + JSON.stringify(data) + ',\n'); -182a187,189 -> -> stream.write('];\n'); -> stream.end(); diff --git a/test/index.html b/test/index.html deleted file mode 100644 index b3c517fb..00000000 --- a/test/index.html +++ /dev/null @@ -1,49 +0,0 @@ - -term.js test - -

term.js test

- - - diff --git a/test/index.js b/test/index.js deleted file mode 100644 index d798195e..00000000 --- a/test/index.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * term.js - * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) - */ - -var http = require('http') - , path = require('path') - , fs = require('fs'); - -var express = require('express') - , term = require('../'); - -var app = express() - , server = http.createServer(app); - -app.use(function(req, res, next) { - var setHeader = res.setHeader; - res.setHeader = function(name) { - switch (name) { - case 'Cache-Control': - case 'Last-Modified': - case 'ETag': - return; - } - return setHeader.apply(res, arguments); - }; - next(); -}); - -app.use(express.static(__dirname)); -app.use(term.middleware()); - -server.listen(8080); From fa093e2bd2b2ab4e4d7c8aa5bc0c7544804136b7 Mon Sep 17 00:00:00 2001 From: Paris Date: Thu, 9 Jun 2016 10:04:53 +0300 Subject: [PATCH 12/28] Set up environment for testing --- bin/test | 5 +++++ package.json | 7 +++++-- test/test.js | 6 ++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100755 bin/test create mode 100644 test/test.js diff --git a/bin/test b/bin/test new file mode 100755 index 00000000..d7d2ed24 --- /dev/null +++ b/bin/test @@ -0,0 +1,5 @@ +#! /bin/bash +# +# Testing script for xterm.js + +node_modules/.bin/mocha $@ diff --git a/package.json b/package.json index 59461ed2..73fde3c7 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,12 @@ "devDependencies": { "express": "4.13.4", "express-ws": "2.0.0-rc.1", - "pty.js": "0.3.0" + "pty.js": "0.3.0", + "mocha": "2.5.3", + "chai": "3.5.0" }, "scripts": { - "start": "bash bin/server" + "start": "bash bin/server", + "test": "bash bin/test" } } diff --git a/test/test.js b/test/test.js new file mode 100644 index 00000000..d746eb28 --- /dev/null +++ b/test/test.js @@ -0,0 +1,6 @@ +var assert = require('chai').assert; + + +describe('xterm', function() { + // Just a dummy first test +}); From b91e737017026e1307662113c02b0288e2d0bac6 Mon Sep 17 00:00:00 2001 From: Paris Date: Thu, 9 Jun 2016 10:08:44 +0300 Subject: [PATCH 13/28] Add .travis.yml --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..03a4ff85 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - 4 +notifications: + email: false From ab1526b0850e4d5979eac9168791c15b6b914ba4 Mon Sep 17 00:00:00 2001 From: Paris Date: Thu, 9 Jun 2016 10:10:36 +0300 Subject: [PATCH 14/28] Add travis ci build status in xterm.js --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 5e2b00e1..d6b40e3d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # xterm.js +![xterm.js build status](https://api.travis-ci.org/sourcelair/xterm.js.svg) + Xterm.js is a full xterm clone, written in JavaScript. It is used at [SourceLair](https://www.sourcelair.com/home) to help people develop their applications in their browsers. From beff0db3017c13289a3743d2ddd613a0da2c5fda Mon Sep 17 00:00:00 2001 From: Paris Date: Thu, 9 Jun 2016 10:18:39 +0300 Subject: [PATCH 15/28] Add apt addon in travis for node-gyp dependency of pty.js --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index 03a4ff85..3849b59e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,11 @@ language: node_js node_js: - 4 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 notifications: email: false From abb7a495846ff8c70a2f6c573648b06a75624e6d Mon Sep 17 00:00:00 2001 From: Paris Date: Thu, 9 Jun 2016 10:22:49 +0300 Subject: [PATCH 16/28] Add CXX environment --- .travis.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3849b59e..b085a7bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ language: node_js node_js: - - 4 + - 4 +env: + - CXX=g++-4.8 addons: apt: sources: @@ -8,4 +10,4 @@ addons: packages: - g++-4.8 notifications: - email: false + email: false From 524db02228f4bf9eababb7f2932f2ed898147bc7 Mon Sep 17 00:00:00 2001 From: runarberg Date: Thu, 9 Jun 2016 13:05:50 +0000 Subject: [PATCH 17/28] Fix Ctrl/Shift + insert copy/paste Many systems (including MS Windows and many linuxes) map `` + `` to copy and ` + ` to paste. That serves as a handy fallback when the more common ` + C` and ` + V` keybindings have their default prevented to send signals to the terminal. Currently all keydown-events with the insert key send `\x1b[2~` to the terminal. This commit won't send that key if either the `shiftKey` or the `ctrlKey` are present. Instead it will enable `contentEditable` to allow for pasting. --- src/xterm.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/xterm.js b/src/xterm.js index b17437cc..3a8da861 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -519,6 +519,11 @@ term.leaseContentEditable(); } } + + if (!term.isMac && ev.keyCode == 45 && ev.shiftKey && !ev.ctrlKey) { + // Shift + Insert pastes on windows and many linuxes + term.leaseContentEditable(); + } }); /** @@ -2387,7 +2392,11 @@ break; // insert case 45: - key = '\x1b[2~'; + if (!ev.shiftKey && !ev.ctrlKey) { + // or + are used to + // copy-paste on some systems. + key = '\x1b[2~'; + } break; // home case 36: From e2aaa8d362331efdc9939af366dcaaf2ab23ee96 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 9 Jun 2016 18:01:12 -0700 Subject: [PATCH 18/28] Jump over words with ctrl+left/right Escape codes used: 5=ctrl, C=right, D=left Fixes #65 --- src/xterm.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index b17437cc..d8bdd7ac 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -2337,7 +2337,12 @@ this.cancel(ev, true); key = '\x1bb' // Jump a word back break; - } else if (this.applicationCursor) { + } + if (ev.ctrlKey) { + key = '\x1b[5D'; // Jump a word back + break; + } + if (this.applicationCursor) { key = '\x1bOD'; // SS3 as ^[O for 7-bit break; } @@ -2349,7 +2354,12 @@ this.cancel(ev, true); key = '\x1bf' // Jump a word forward break; - } else if (this.applicationCursor) { + } + if (ev.ctrlKey) { + key = '\x1b[5C'; // Jump a word forward + break; + } + if (this.applicationCursor) { key = '\x1bOC'; break; } From 3a866cf202bbf5a797d927547cf27b391db607a9 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 9 Jun 2016 18:41:54 -0700 Subject: [PATCH 19/28] Refactor escape sequence code and add tests --- src/xterm.js | 200 ++++++++++++++++++++++++--------------------------- test/test.js | 19 ++++- 2 files changed, 109 insertions(+), 110 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index d8bdd7ac..f4a1d3d0 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -2301,229 +2301,213 @@ // Key Resources: // https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent Terminal.prototype.keyDown = function(ev) { - var self = this, key; + var self = this; + var result = this.evaluateKeyEscapeSequence(ev); + if (result.scrollDisp) { + this.scrollDisp(result.scrollDisp); + return this.cancel(ev); + } + + if (result.cancel) { + // The event is canceled at the end already, is this necessary? + this.cancel(ev, true); + } + + if (!result.key || (this.isMac && ev.metaKey)) { + return true; + } + + this.emit('keydown', ev); + this.emit('key', result.key, ev); + this.showCursor(); + this.handler(result.key); + + return this.cancel(ev, true); + }; + + Terminal.prototype.evaluateKeyEscapeSequence = function(ev) { + var result = { + // Whether to cancel event propogation (NOTE: this may not be needed since the event is + // canceled at the end of keyDown + cancel: false, + // The new key even to emit + key: undefined, + // The number of characters to scroll, if this is defined it will cancel the event + scrollDisp: undefined + }; switch (ev.keyCode) { // backspace case 8: if (ev.shiftKey) { - key = '\x08'; // ^H + result.key = '\x08'; // ^H break; } - key = '\x7f'; // ^? + result.key = '\x7f'; // ^? break; // tab case 9: if (ev.shiftKey) { - key = '\x1b[Z'; + result.key = '\x1b[Z'; break; } - key = '\t'; - this.cancel(ev, true); + result.key = '\t'; + result.cancel = true; break; // return/enter case 13: - key = '\r'; - this.cancel(ev, true); + result.key = '\r'; + result.cancel = true; break; // escape case 27: - key = '\x1b'; - this.cancel(ev, true); + result.key = '\x1b'; + result.cancel = true; break; // left-arrow case 37: if (ev.altKey) { - this.cancel(ev, true); - key = '\x1bb' // Jump a word back + result.key = '\x1bb' // Jump a word back + result.cancel = true; break; } if (ev.ctrlKey) { - key = '\x1b[5D'; // Jump a word back + result.key = '\x1b[5D'; // Jump a word back break; } if (this.applicationCursor) { - key = '\x1bOD'; // SS3 as ^[O for 7-bit + result.key = '\x1bOD'; // SS3 as ^[O for 7-bit break; } - key = '\x1b[D'; + result.key = '\x1b[D'; break; // right-arrow case 39: if (ev.altKey) { - this.cancel(ev, true); - key = '\x1bf' // Jump a word forward + result.key = '\x1bf' // Jump a word forward + result.cancel = true; break; } if (ev.ctrlKey) { - key = '\x1b[5C'; // Jump a word forward + result.key = '\x1b[5C'; // Jump a word forward break; } if (this.applicationCursor) { - key = '\x1bOC'; + result.key = '\x1bOC'; break; } - key = '\x1b[C'; + result.key = '\x1b[C'; break; // up-arrow case 38: if (this.applicationCursor) { - key = '\x1bOA'; + result.key = '\x1bOA'; break; } if (ev.ctrlKey) { - this.scrollDisp(-1); - return this.cancel(ev); + result.scrollDisp = -1; } else { - key = '\x1b[A'; + result.key = '\x1b[A'; } break; // down-arrow case 40: if (this.applicationCursor) { - key = '\x1bOB'; + result.key = '\x1bOB'; break; } if (ev.ctrlKey) { - this.scrollDisp(1); - return this.cancel(ev); + result.scrollDisp = 1; } else { - key = '\x1b[B'; + result.key = '\x1b[B'; } break; // delete case 46: - key = '\x1b[3~'; + result.key = '\x1b[3~'; break; // insert case 45: - key = '\x1b[2~'; + result.key = '\x1b[2~'; break; // home case 36: if (this.applicationKeypad) { - key = '\x1bOH'; + result.key = '\x1bOH'; break; } - key = '\x1bOH'; + result.key = '\x1bOH'; break; // end case 35: if (this.applicationKeypad) { - key = '\x1bOF'; + result.key = '\x1bOF'; break; } - key = '\x1bOF'; + result.key = '\x1bOF'; break; // page up case 33: if (ev.shiftKey) { - this.scrollDisp(-(this.rows - 1)); - return this.cancel(ev); + result.scrollDisp = -(this.rows - 1); } else { - key = '\x1b[5~'; + result.key = '\x1b[5~'; } break; // page down case 34: if (ev.shiftKey) { - this.scrollDisp(this.rows - 1); - return this.cancel(ev); + result.scrollDisp = this.rows - 1; } else { - key = '\x1b[6~'; + result.key = '\x1b[6~'; } break; - // F1 - case 112: - key = '\x1bOP'; - break; - // F2 - case 113: - key = '\x1bOQ'; - break; - // F3 - case 114: - key = '\x1bOR'; - break; - // F4 - case 115: - key = '\x1bOS'; - break; - // F5 - case 116: - key = '\x1b[15~'; - break; - // F6 - case 117: - key = '\x1b[17~'; - break; - // F7 - case 118: - key = '\x1b[18~'; - break; - // F8 - case 119: - key = '\x1b[19~'; - break; - // F9 - case 120: - key = '\x1b[20~'; - break; - // F10 - case 121: - key = '\x1b[21~'; - break; - // F11 - case 122: - key = '\x1b[23~'; - break; - // F12 - case 123: - key = '\x1b[24~'; - break; + // F1-F12 + case 112: result.key = '\x1bOP'; break; + case 113: result.key = '\x1bOQ'; break; + case 114: result.key = '\x1bOR'; break; + case 115: result.key = '\x1bOS'; break; + case 116: result.key = '\x1b[15~'; break; + case 117: result.key = '\x1b[17~'; break; + case 118: result.key = '\x1b[18~'; break; + case 119: result.key = '\x1b[19~'; break; + case 120: result.key = '\x1b[20~'; break; + case 121: result.key = '\x1b[21~'; break; + case 122: result.key = '\x1b[23~'; break; + case 123: result.key = '\x1b[24~'; break; default: // a-z and space if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { if (ev.keyCode >= 65 && ev.keyCode <= 90) { - key = String.fromCharCode(ev.keyCode - 64); + result.key = String.fromCharCode(ev.keyCode - 64); } else if (ev.keyCode === 32) { // NUL - key = String.fromCharCode(0); + result.key = String.fromCharCode(0); } else if (ev.keyCode >= 51 && ev.keyCode <= 55) { // escape, file sep, group sep, record sep, unit sep - key = String.fromCharCode(ev.keyCode - 51 + 27); + result.key = String.fromCharCode(ev.keyCode - 51 + 27); } else if (ev.keyCode === 56) { // delete - key = String.fromCharCode(127); + result.key = String.fromCharCode(127); } else if (ev.keyCode === 219) { // ^[ - escape - key = String.fromCharCode(27); + result.key = String.fromCharCode(27); } else if (ev.keyCode === 221) { // ^] - group sep - key = String.fromCharCode(29); + result.key = String.fromCharCode(29); } } else if ((!this.isMac && ev.altKey) || (this.isMac && ev.metaKey)) { if (ev.keyCode >= 65 && ev.keyCode <= 90) { - key = '\x1b' + String.fromCharCode(ev.keyCode + 32); + result.key = '\x1b' + String.fromCharCode(ev.keyCode + 32); } else if (ev.keyCode === 192) { - key = '\x1b`'; + result.key = '\x1b`'; } else if (ev.keyCode >= 48 && ev.keyCode <= 57) { - key = '\x1b' + (ev.keyCode - 48); + result.key = '\x1b' + (ev.keyCode - 48); } } break; } - - if (!key || (this.isMac && ev.metaKey)) { - return true; - } - - this.emit('keydown', ev); - this.emit('key', key, ev); - this.showCursor(); - this.handler(key); - - return this.cancel(ev, true); + return result; }; Terminal.prototype.setgLevel = function(g) { diff --git a/test/test.js b/test/test.js index d746eb28..7297d8af 100644 --- a/test/test.js +++ b/test/test.js @@ -1,6 +1,21 @@ var assert = require('chai').assert; +var Terminal = require('../src/xterm'); +describe('xterm.js', function() { + var xterm; -describe('xterm', function() { - // Just a dummy first test + beforeEach(function () { + xterm = new Terminal(); + }); + + describe('evaluateKeyEscapeSequence', function() { + it('should return \\x1b[5D when ctrl+left is passed', function() { + var event = { ctrlKey: true, keyCode: 37 }; + assert.equal(xterm.evaluateKeyEscapeSequence(event).key, '\x1b[5D'); + }); + it('should return \\x1b[5C when ctrl+right is passed', function() { + var event = { ctrlKey: true, keyCode: 39 }; + assert.equal(xterm.evaluateKeyEscapeSequence(event).key, '\x1b[5C'); + }); + }); }); From c86fd8781998c59b88cfcceec31fd9a457475248 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 9 Jun 2016 19:00:40 -0700 Subject: [PATCH 20/28] Add tests for F-keys --- test/test.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/test/test.js b/test/test.js index 7297d8af..12fba1d0 100644 --- a/test/test.js +++ b/test/test.js @@ -10,12 +10,24 @@ describe('xterm.js', function() { describe('evaluateKeyEscapeSequence', function() { it('should return \\x1b[5D when ctrl+left is passed', function() { - var event = { ctrlKey: true, keyCode: 37 }; - assert.equal(xterm.evaluateKeyEscapeSequence(event).key, '\x1b[5D'); + assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[5D'); }); it('should return \\x1b[5C when ctrl+right is passed', function() { - var event = { ctrlKey: true, keyCode: 39 }; - assert.equal(xterm.evaluateKeyEscapeSequence(event).key, '\x1b[5C'); + assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[5C'); + }); + it('should return the correct escape sequence for the F-keys', function() { + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 112 }).key, '\x1bOP'); + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 113 }).key, '\x1bOQ'); + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 114 }).key, '\x1bOR'); + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 115 }).key, '\x1bOS'); + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 116 }).key, '\x1b[15~'); + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 117 }).key, '\x1b[17~'); + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 118 }).key, '\x1b[18~'); + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 119 }).key, '\x1b[19~'); + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 120 }).key, '\x1b[20~'); + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 121 }).key, '\x1b[21~'); + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 122 }).key, '\x1b[23~'); + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 123 }).key, '\x1b[24~'); }); }); }); From 0535f9425bf96985d3b1e3ce45df965dc6d8cc6b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 9 Jun 2016 19:25:29 -0700 Subject: [PATCH 21/28] More escape sequence tests --- src/xterm.js | 16 ++++++++------- test/test.js | 58 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index f4a1d3d0..ac06960d 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -2326,6 +2326,12 @@ return this.cancel(ev, true); }; + /** + * Returns an object that determines how a KeyboardEvent should be handled. The key of the + * returned value is the new key code to pass to the PTY. + * + * Reference: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html + */ Terminal.prototype.evaluateKeyEscapeSequence = function(ev) { var result = { // Whether to cancel event propogation (NOTE: this may not be needed since the event is @@ -2422,14 +2428,10 @@ result.key = '\x1b[B'; } break; - // delete - case 46: - result.key = '\x1b[3~'; - break; // insert - case 45: - result.key = '\x1b[2~'; - break; + case 45: result.key = '\x1b[2~'; break; + // delete + case 46: result.key = '\x1b[3~'; break; // home case 36: if (this.applicationKeypad) { diff --git a/test/test.js b/test/test.js index 12fba1d0..e603d65f 100644 --- a/test/test.js +++ b/test/test.js @@ -9,25 +9,49 @@ describe('xterm.js', function() { }); describe('evaluateKeyEscapeSequence', function() { - it('should return \\x1b[5D when ctrl+left is passed', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[5D'); + it('should return the correct escape sequence for unmodified keys', function() { + // Backspace + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 8 }).key, '\x7f'); // ^? + // Tab + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 9 }).key, '\t'); + // Return/enter + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 13 }).key, '\r'); // CR + // Escape + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 27 }).key, '\x1b'); + // Page up, page down + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 33 }).key, '\x1b[5~'); // CSI 5 ~ + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 34 }).key, '\x1b[6~'); // CSI 6 ~ + // End, Home + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 35 }).key, '\x1bOF'); // SS3 F + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 36 }).key, '\x1bOH'); // SS3 H + // Left, up, right, down arrows + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 37 }).key, '\x1b[D'); // CSI D + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 38 }).key, '\x1b[A'); // CSI A + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 39 }).key, '\x1b[C'); // CSI C + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 40 }).key, '\x1b[B'); // CSI B + // Insert + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 45 }).key, '\x1b[2~'); // CSI 2 ~ + // Delete + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 46 }).key, '\x1b[3~'); // CSI 3 ~ + // F1-F12 + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 112 }).key, '\x1bOP'); // SS3 P + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 113 }).key, '\x1bOQ'); // SS3 Q + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 114 }).key, '\x1bOR'); // SS3 R + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 115 }).key, '\x1bOS'); // SS3 S + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 116 }).key, '\x1b[15~'); // CSI 1 5 ~ + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 117 }).key, '\x1b[17~'); // CSI 1 7 ~ + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 118 }).key, '\x1b[18~'); // CSI 1 8 ~ + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 119 }).key, '\x1b[19~'); // CSI 1 9 ~ + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 120 }).key, '\x1b[20~'); // CSI 2 0 ~ + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 121 }).key, '\x1b[21~'); // CSI 2 1 ~ + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 122 }).key, '\x1b[23~'); // CSI 2 3 ~ + assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 123 }).key, '\x1b[24~'); // CSI 2 4 ~ }); - it('should return \\x1b[5C when ctrl+right is passed', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[5C'); + it('should return \\x1b[5D for ctrl+left', function() { + assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[5D'); // CSI 5 D }); - it('should return the correct escape sequence for the F-keys', function() { - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 112 }).key, '\x1bOP'); - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 113 }).key, '\x1bOQ'); - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 114 }).key, '\x1bOR'); - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 115 }).key, '\x1bOS'); - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 116 }).key, '\x1b[15~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 117 }).key, '\x1b[17~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 118 }).key, '\x1b[18~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 119 }).key, '\x1b[19~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 120 }).key, '\x1b[20~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 121 }).key, '\x1b[21~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 122 }).key, '\x1b[23~'); - assert.equal(xterm.evaluateKeyEscapeSequence({ keyCode: 123 }).key, '\x1b[24~'); + it('should return \\x1b[5C for ctrl+right', function() { + assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[5C'); // CSI 5 C }); }); }); From da9f86f1a7764b50a03ee8fcd77d12a660c8b5b0 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 9 Jun 2016 19:59:59 -0700 Subject: [PATCH 22/28] Draw cursor at correct position when scrolling Fixes #64 --- src/xterm.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index b17437cc..f75989ee 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1102,9 +1102,8 @@ line = this.lines[row]; out = ''; - if (y === this.y + if (this.y === y - (this.ybase - this.ydisp) && this.cursorState - && (this.ydisp === this.ybase) && !this.cursorHidden) { x = this.x; } else { From eee99f62e15f9449321b82bb890d47fc472d9743 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 9 Jun 2016 21:05:45 -0700 Subject: [PATCH 23/28] Improve scroll to work with blank lines after the cursor This commit works fixes scrolling when there were blank lines after the cursor. Here is what it does (blank rows are those added by running `clear`): when increasing rows: if there are blank rows below the cursor: add a blank row to the bottom else if there is room in the buffer above the viewport scroll up else add a blank row to the bottom when decreasing rows: if there are blank rows below the cursor: remove a blank row from the bottom else scroll down Fixes #111 --- src/xterm.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index b17437cc..f07dcc75 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -2607,7 +2607,8 @@ , el , i , j - , ch; + , ch + , addToY; if (x === this.cols && y === this.rows) { return; @@ -2639,11 +2640,23 @@ // resize rows j = this.rows; + addToY = 0; if (j < y) { el = this.element; while (j++ < y) { + // y is rows, not this.y if (this.lines.length < y + this.ybase) { - this.lines.push(this.blankLine()); + if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { + // There is room above the buffer and there are no empty elements below the line, + // scroll up + this.ybase--; + this.ydisp--; + addToY++ + } else { + // Add a blank line if there is no buffer left at the top to scroll to, or if there + // are blank lines after the cursor + this.lines.push(this.blankLine()); + } } if (this.children.length < y) { this.insertRow(); @@ -2652,11 +2665,11 @@ } else { // (j > y) while (j-- > y) { if (this.lines.length > y + this.ybase) { - if (this.y + this.ybase < j) { - // The line is after the cursor, remove it + if (this.lines.length > this.ybase + this.y + 1) { + // The line is a blank line below the cursor, remove it this.lines.pop(); } else { - // The line is the cursor, push the viewport down + // The line is the cursor, scroll down this.ybase++; this.ydisp++; } @@ -2676,6 +2689,9 @@ if (this.y >= y) { this.y = y - 1; } + if (addToY) { + this.y += addToY; + } if (this.x >= x) { this.x = x - 1; From fa1cd89aa9feae2e5dfb5aa669ed6964dbf8db48 Mon Sep 17 00:00:00 2001 From: Paris Date: Fri, 10 Jun 2016 16:17:14 +0300 Subject: [PATCH 24/28] Fix copying of non-breaking spaces --- src/xterm.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index a806a03d..5984e7db 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -551,13 +551,22 @@ /** - * Bind copy event. Stript trailing whitespaces from selection. + * Bind copy event. */ Terminal.bindCopy = function(term) { on(term.element, 'copy', function(ev) { - var selectedText = window.getSelection().toString(), + var space = String.fromCharCode(32), + nonBreakingSpace = String.fromCharCode(160), + allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'), + selectedText = window.getSelection().toString(), copiedText = selectedText.split('\n').map(function (element) { - return element.replace(/\s+$/g, ''); + /** + * Strip all trailing white spaces and convert all non-breaking spaces to regular + * spaces. + */ + var line = element.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space); + + return line; }).join('\n'); ev.clipboardData.setData('text/plain', copiedText); ev.preventDefault(); From 00f4232ecca1270131ce822a1576ee2e539c22d8 Mon Sep 17 00:00:00 2001 From: Paris Date: Fri, 10 Jun 2016 16:27:58 +0300 Subject: [PATCH 25/28] Export copied text processing to static method --- src/xterm.js | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 5984e7db..ce0a246c 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -549,26 +549,40 @@ }, true); }; + /** + * Prepares text copied from terminal selection, to be saved in the clipboard by: + * 1. stripping all trailing white spaces + * 2. converting all non-breaking spaces to regular spaces + * @param {string} text The copied text that needs processing for storing in clipboard + * @static + */ + Terminal.prepareCopiedTextForClipboard = function (text) { + var space = String.fromCharCode(32), + nonBreakingSpace = String.fromCharCode(160), + allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'), + processedText = text.split('\n').map(function (line) { + /** + * Strip all trailing white spaces and convert all non-breaking spaces to regular + * spaces. + */ + var processedLine = line.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space); + + return processedLine; + }).join('\n'); + + return processedText; + }; /** - * Bind copy event. + * Binds copy functionality to the given terminal. + * @static */ Terminal.bindCopy = function(term) { on(term.element, 'copy', function(ev) { - var space = String.fromCharCode(32), - nonBreakingSpace = String.fromCharCode(160), - allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'), - selectedText = window.getSelection().toString(), - copiedText = selectedText.split('\n').map(function (element) { - /** - * Strip all trailing white spaces and convert all non-breaking spaces to regular - * spaces. - */ - var line = element.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space); + var copiedText = window.getSelection().toString(), + text = Terminal.prepareCopiedTextForClipboard(copiedText); - return line; - }).join('\n'); - ev.clipboardData.setData('text/plain', copiedText); + ev.clipboardData.setData('text/plain', text); ev.preventDefault(); }); }; From fed92ac5c8a312b31b061184fe1f1470e740825b Mon Sep 17 00:00:00 2001 From: Paris Date: Fri, 10 Jun 2016 16:35:56 +0300 Subject: [PATCH 26/28] Implement tests --- src/xterm.js | 1 + test/test.js | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/xterm.js b/src/xterm.js index ce0a246c..273fdca5 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -554,6 +554,7 @@ * 1. stripping all trailing white spaces * 2. converting all non-breaking spaces to regular spaces * @param {string} text The copied text that needs processing for storing in clipboard + * @returns {string} * @static */ Terminal.prepareCopiedTextForClipboard = function (text) { diff --git a/test/test.js b/test/test.js index e603d65f..ffbc285f 100644 --- a/test/test.js +++ b/test/test.js @@ -54,4 +54,18 @@ describe('xterm.js', function() { assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[5C'); // CSI 5 C }); }); + + describe('evaluateCopiedTextProcessing', function () { + it('should strip trailing whitespaces and replace nbsps with spaces', function () { + var nonBreakingSpace = String.fromCharCode(160), + copiedText = 'echo' + nonBreakingSpace + 'hello' + nonBreakingSpace, + processedText = Terminal.prepareCopiedTextForClipboard(copiedText); + + // No trailing spaces + assert.equal(processedText.match(/\s+$/), null); + + // No non-breaking space + assert.equal(processedText.indexOf(nonBreakingSpace), -1); + }); + }); }); From 5a56849df8b52fb000b31d7150cf1613f81ec6ca Mon Sep 17 00:00:00 2001 From: Paris Date: Fri, 10 Jun 2016 19:02:49 +0300 Subject: [PATCH 27/28] Stop using binary literals. Non ES6 compatible --- src/xterm.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 273fdca5..2ba64e26 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -1089,11 +1089,11 @@ * Flags used to render terminal text properly */ Terminal.flags = { - BOLD: 0b00001, - UNDERLINE: 0b00010, - BLINK: 0b00100, - INVERSE: 0b01000, - INVISIBLE: 0b10000 + BOLD: 1, + UNDERLINE: 2, + BLINK: 4, + INVERSE: 8, + INVISIBLE: 16 } /* From b01165c1f8fc15415ea30b5e1c00278fb230cf13 Mon Sep 17 00:00:00 2001 From: runarberg Date: Wed, 1 Jun 2016 17:06:57 +0000 Subject: [PATCH 28/28] Fix third level shifts for Mac OS and windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ISO third level keys were not working. That prevented some inputting important characters (like pipe `|` and caret `^`) on some keyboard layouts. Instead of parsing the user-agent string to find the users os, we now look into the `platform` attribute of the `navigator` object. Removed the hijacking of the command key `⌘` on Mac OS as the `Alt` key on other systems. --- src/xterm.js | 54 ++++++++++++++++++++--- test/test.js | 122 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 7 deletions(-) diff --git a/src/xterm.js b/src/xterm.js index 2ba64e26..7f01e2a4 100644 --- a/src/xterm.js +++ b/src/xterm.js @@ -659,12 +659,27 @@ * Parse User-Agent */ if (this.context.navigator && this.context.navigator.userAgent) { - this.isMac = !!~this.context.navigator.userAgent.indexOf('Mac'); - this.isIpad = !!~this.context.navigator.userAgent.indexOf('iPad'); - this.isIphone = !!~this.context.navigator.userAgent.indexOf('iPhone'); this.isMSIE = !!~this.context.navigator.userAgent.indexOf('MSIE'); } + /* + * Find the users platform. We use this to interpret the meta key + * and ISO third level shifts. + * http://stackoverflow.com/questions/19877924/what-is-the-list-of-possible-values-for-navigator-platform-as-of-today + */ + if (this.context.navigator && this.context.navigator.platform) { + this.isMac = contains( + this.context.navigator.platform, + ['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'] + ); + this.isIpad = this.context.navigator.platform === 'iPad'; + this.isIphone = this.context.navigator.platform === 'iPhone'; + this.isMSWindows = contains( + this.context.navigator.platform, + ['Windows', 'Win16', 'Win32', 'WinCE'] + ); + } + /* * Create main element container */ @@ -2337,12 +2352,16 @@ return this.cancel(ev); } - if (result.cancel) { + if (isThirdLevelShift(this, ev)) { + return true; + } + + if (result.cancel ) { // The event is canceled at the end already, is this necessary? this.cancel(ev, true); } - if (!result.key || (this.isMac && ev.metaKey)) { + if (!result.key) { return true; } @@ -2532,7 +2551,8 @@ // ^] - group sep result.key = String.fromCharCode(29); } - } else if ((!this.isMac && ev.altKey) || (this.isMac && ev.metaKey)) { + } else if (!this.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) { + // On Mac this is a third level shift. Use instead. if (ev.keyCode >= 65 && ev.keyCode <= 90) { result.key = '\x1b' + String.fromCharCode(ev.keyCode + 32); } else if (ev.keyCode === 192) { @@ -2571,7 +2591,9 @@ return false; } - if (!key || ev.ctrlKey || ev.altKey || ev.metaKey) { + if (!key || ( + (ev.altKey || ev.ctrlKey || ev.metaKey) && !isThirdLevelShift(this, ev) + )) { return false; } @@ -4398,6 +4420,15 @@ * Helpers */ + function contains(el, arr) { + for (var i = 0; i < arr.length; i += 1) { + if (el === arr[i]) { + return true; + } + } + return false; + } + function on(el, type, handler, capture) { if (!Array.isArray(el)) { el = [el]; @@ -4454,6 +4485,15 @@ return -1; } + function isThirdLevelShift(term, ev) { + var thirdLevelKey = + (term.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) || + (term.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey); + + // Don't invoke for arrows, pageDown, home, backspace, etc. + return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47); + } + function isWide(ch) { if (ch <= '\uff00') return false; return (ch >= '\uff01' && ch <= '\uffbe') diff --git a/test/test.js b/test/test.js index ffbc285f..6f7a0791 100644 --- a/test/test.js +++ b/test/test.js @@ -68,4 +68,126 @@ describe('xterm.js', function() { assert.equal(processedText.indexOf(nonBreakingSpace), -1); }); }); + + describe('Third level shift', function() { + var ev = { + preventDefault: function() {}, + stopPropagation: function() {} + }; + + beforeEach(function() { + xterm.handler = function() {}; + xterm.showCursor = function() {}; + xterm.clearSelection = function() {}; + }) + + describe('On Mac OS', function() { + beforeEach(function() { + xterm.isMac = true; + }); + + it('should not interfere with the alt key on keyDown', function() { + assert.equal( + xterm.keyDown(Object.assign({}, ev, { altKey: true, keyCode: 81 })), + true + ); + assert.equal( + xterm.keyDown(Object.assign({}, ev, { altKey: true, keyCode: 192 })), + true + ); + }); + + it('should interefere with the alt + arrow keys', function() { + assert.equal( + xterm.keyDown(Object.assign({}, ev, { altKey: true, keyCode: 37 })), + false + ); + assert.equal( + xterm.keyDown(Object.assign({}, ev, { altKey: true, keyCode: 39 })), + false + ); + }); + + it('should emit key with alt + key on keyPress', function(done) { + var keys = ['@', '@', '\\', '\\', '|', '|']; + + xterm.on('keypress', function(key) { + if (key) { + var index = keys.indexOf(key); + assert(index !== -1, "Emitted wrong key: " + key); + keys.splice(index, 1); + } + if (keys.length === 0) done(); + }); + + xterm.keyPress(Object.assign({}, ev, { altKey: true, keyCode: 64 })); // @ + // Firefox + xterm.keyPress(Object.assign({}, ev, { altKey: true, charCode: 64, keyCode: 0 })); + xterm.keyPress(Object.assign({}, ev, { altKey: true, keyCode: 92 })); // \ + xterm.keyPress(Object.assign({}, ev, { altKey: true, charCode: 92, keyCode: 0 })); + xterm.keyPress(Object.assign({}, ev, { altKey: true, keyCode: 124 })); // | + xterm.keyPress(Object.assign({}, ev, { altKey: true, charCode: 124, keyCode: 0 })); + }); + }); + + describe('On MS Windows', function() { + beforeEach(function() { + xterm.isMSWindows = true; + }); + + it('should not interfere with the alt + ctrl key on keyDown', function() { + assert.equal( + xterm.keyDown(Object.assign({}, ev, { altKey: true, ctrlKey: true, keyCode: 81 })), + true + ); + assert.equal( + xterm.keyDown(Object.assign({}, ev, { altKey: true, ctrlKey: true, keyCode: 192 })), + true + ); + }); + + it('should interefere with the alt + ctrl + arrow keys', function() { + assert.equal( + xterm.keyDown(Object.assign({}, ev, { altKey: true, ctrlKey: true, keyCode: 37 })), + false + ); + assert.equal( + xterm.keyDown(Object.assign({}, ev, { altKey: true, ctrlKey: true, keyCode: 39 })), + false + ); + }); + + it('should emit key with alt + ctrl + key on keyPress', function(done) { + var keys = ['@', '@', '\\', '\\', '|', '|']; + + xterm.on('keypress', function(key) { + if (key) { + var index = keys.indexOf(key); + assert(index !== -1, "Emitted wrong key: " + key); + keys.splice(index, 1); + } + if (keys.length === 0) done(); + }); + + xterm.keyPress( + Object.assign({}, ev, { altKey: true, ctrlKey: true, keyCode: 64 }) + ); // @ + xterm.keyPress( + Object.assign({}, ev, { altKey: true, ctrlKey: true, charCode: 64, keyCode: 0 }) + ); + xterm.keyPress( + Object.assign({}, ev, { altKey: true, ctrlKey: true, keyCode: 92 }) + ); // \ + xterm.keyPress( + Object.assign({}, ev, { altKey: true, ctrlKey: true, charCode: 92, keyCode: 0 }) + ); + xterm.keyPress( + Object.assign({}, ev, { altKey: true, ctrlKey: true, keyCode: 124 }) + ); // | + xterm.keyPress( + Object.assign({}, ev, { altKey: true, ctrlKey: true, charCode: 124, keyCode: 0 }) + ); + }); + }); + }); });