diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..b085a7bd --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: node_js +node_js: + - 4 +env: + - CXX=g++-4.8 +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 +notifications: + email: false diff --git a/README.md b/README.md index 14044416..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. @@ -8,7 +10,35 @@ 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. + +## 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. 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/demo/app.js b/demo/app.js index a7b79478..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); @@ -45,5 +48,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); 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/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/src/xterm.css b/src/xterm.css index 055c0fc0..5a325ed9 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 */ @@ -2123,13 +2129,5 @@ * in order to allow child elements to adjust. */ .terminal .xterm-rows > div { - line-height: 1.3; -} - -/** - * All styled spans inside terminal lines should be inline-blocks, - * in orde to achieve better height adjustment. - */ -.terminal .xterm-rows span { - display: inline-block; + line-height: normal; } diff --git a/src/xterm.js b/src/xterm.js index a0d4fd09..7f01e2a4 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--) { @@ -401,7 +421,6 @@ this.showCursor(); this.element.focus(); - this.emit('focus', {terminal: this}); }; Terminal.prototype.blur = function() { @@ -417,7 +436,6 @@ this.send('\x1b[O'); } Terminal.focus = null; - this.emit('blur', {terminal: this}); }; /** @@ -501,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(); + } }); /** @@ -526,17 +549,41 @@ }, 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 + * @returns {string} + * @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. Stript trailing whitespaces from selection. + * Binds copy functionality to the given terminal. + * @static */ Terminal.bindCopy = function(term) { on(term.element, 'copy', function(ev) { - var selectedText = window.getSelection().toString(), - copiedText = selectedText.split('\n').map(function (element) { - return element.replace(/\s+$/g, ''); - }).join('\n'); - ev.clipboardData.setData('text/plain', copiedText); + var copiedText = window.getSelection().toString(), + text = Terminal.prepareCopiedTextForClipboard(copiedText); + + ev.clipboardData.setData('text/plain', text); ev.preventDefault(); }); }; @@ -612,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 */ @@ -627,6 +689,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 @@ -1036,11 +1104,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 } /* @@ -1078,9 +1146,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 { @@ -2277,219 +2344,226 @@ // 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 (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) { + return true; + } + + this.emit('keydown', ev); + this.emit('key', result.key, ev); + this.showCursor(); + this.handler(result.key); + + 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 + // 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 - break; - } else if (this.applicationCursor) { - key = '\x1bOD'; // SS3 as ^[O for 7-bit + result.key = '\x1bb' // Jump a word back + result.cancel = true; break; } - key = '\x1b[D'; + if (ev.ctrlKey) { + result.key = '\x1b[5D'; // Jump a word back + break; + } + if (this.applicationCursor) { + result.key = '\x1bOD'; // SS3 as ^[O for 7-bit + break; + } + result.key = '\x1b[D'; break; // right-arrow case 39: if (ev.altKey) { - this.cancel(ev, true); - key = '\x1bf' // Jump a word forward - break; - } else if (this.applicationCursor) { - key = '\x1bOC'; + result.key = '\x1bf' // Jump a word forward + result.cancel = true; break; } - key = '\x1b[C'; + if (ev.ctrlKey) { + result.key = '\x1b[5C'; // Jump a word forward + break; + } + if (this.applicationCursor) { + result.key = '\x1bOC'; + break; + } + 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~'; - break; // insert case 45: - key = '\x1b[2~'; + if (!ev.shiftKey && !ev.ctrlKey) { + // or + are used to + // copy-paste on some systems. + result.key = '\x1b[2~'; + } break; + // delete + case 46: result.key = '\x1b[3~'; 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)) { + } 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) { - 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) { @@ -2517,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; } @@ -2583,7 +2659,12 @@ , el , i , j - , ch; + , ch + , addToY; + + if (x === this.cols && y === this.rows) { + return; + } if (x < 1) x = 1; if (y < 1) y = 1; @@ -2598,7 +2679,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) { @@ -2611,20 +2692,39 @@ // 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(); } } - } else if (j > y) { + } else { // (j > y) while (j-- > y) { if (this.lines.length > y + this.ybase) { - this.lines.shift(); + 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, scroll down + this.ybase++; + this.ydisp++; + } } if (this.children.length > y) { el = this.children.shift(); @@ -2641,6 +2741,9 @@ if (this.y >= y) { this.y = y - 1; } + if (addToY) { + this.y += addToY; + } if (this.x >= x) { this.x = x - 1; @@ -4317,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]; @@ -4373,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/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); diff --git a/test/test.js b/test/test.js new file mode 100644 index 00000000..6f7a0791 --- /dev/null +++ b/test/test.js @@ -0,0 +1,193 @@ +var assert = require('chai').assert; +var Terminal = require('../src/xterm'); + +describe('xterm.js', function() { + var xterm; + + beforeEach(function () { + xterm = new Terminal(); + }); + + describe('evaluateKeyEscapeSequence', function() { + 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[5D for ctrl+left', function() { + assert.equal(xterm.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[5D'); // CSI 5 D + }); + it('should return \\x1b[5C for ctrl+right', 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); + }); + }); + + 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 }) + ); + }); + }); + }); +});