diff --git a/static/js/term.js b/static/js/term.js index f27112d27..c29cad454 100644 --- a/static/js/term.js +++ b/static/js/term.js @@ -1,19 +1,34 @@ /** - * tty.js - an xterm emulator - * Christopher Jeffrey (https://github.com/chjj/tty.js) + * term.js - an xterm emulator + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * https://github.com/chjj/term.js + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. * * Originally forked from (with the author's permission): - * - * Fabrice Bellard's javascript vt100 for jslinux: - * http://bellard.org/jslinux/ - * Copyright (c) 2011 Fabrice Bellard - * (Redistribution or commercial use is prohibited - * without the author's permission.) - * - * The original design remains. The terminal itself - * has been extended to include xterm CSI codes, among - * other features. -*/ + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * The original design remains. The terminal itself + * has been extended to include xterm CSI codes, among + * other features. + */ ;(function() { @@ -42,7 +57,7 @@ var window = this */ function EventEmitter() { - this._events = {}; + this._events = this._events || {}; } EventEmitter.prototype.addListener = function(type, listener) { @@ -99,6 +114,54 @@ EventEmitter.prototype.listeners = function(type) { return this._events[type] = this._events[type] || []; }; +/** + * Stream + */ + +function Stream() { + EventEmitter.call(this); +} + +inherits(Stream, EventEmitter); + +Stream.prototype.pipe = function(dest, options) { + var src = this + , ondata + , onerror + , onend; + + function unbind() { + src.removeListener('data', ondata); + src.removeListener('error', onerror); + src.removeListener('end', onend); + dest.removeListener('error', onerror); + dest.removeListener('close', unbind); + } + + src.on('data', ondata = function(data) { + dest.write(data); + }); + + src.on('error', onerror = function(err) { + unbind(); + if (!this.listeners('error').length) { + throw err; + } + }); + + src.on('end', onend = function() { + dest.end(); + unbind(); + }); + + dest.on('error', onerror); + dest.on('close', unbind); + + dest.emit('pipe', src); + + return dest; +}; + /** * States */ @@ -109,33 +172,75 @@ var normal = 0 , osc = 3 , charset = 4 , dcs = 5 - , ignore = 6; + , ignore = 6 + , UDK = { type: 'udk' }; /** * Terminal */ -function Terminal(cols, rows, handler) { - EventEmitter.call(this); +function Terminal(options) { + var self = this; - var options; - if (typeof cols === 'object') { - options = cols; - cols = options.cols; - rows = options.rows; - handler = options.handler; + if (!(this instanceof Terminal)) { + return new Terminal(arguments[0], arguments[1], arguments[2]); } - this._options = options || {}; - this.cols = cols || Terminal.geometry[0]; - this.rows = rows || Terminal.geometry[1]; + Stream.call(this); - //if (handler) { - // this.on('data', handler); - //} - this.handler = handler; - if (this.handler){ - this.on('data', this.handler); + if (typeof options === 'number') { + options = { + cols: arguments[0], + rows: arguments[1], + handler: arguments[2] + }; + } + + options = options || {}; + + each(keys(Terminal.defaults), function(key) { + if (options[key] == null) { + options[key] = Terminal.options[key]; + // Legacy: + if (Terminal[key] !== Terminal.defaults[key]) { + options[key] = Terminal[key]; + } + } + self[key] = options[key]; + }); + + if (options.colors.length === 8) { + options.colors = options.colors.concat(Terminal._colors.slice(8)); + } else if (options.colors.length === 16) { + options.colors = options.colors.concat(Terminal._colors.slice(16)); + } else if (options.colors.length === 10) { + options.colors = options.colors.slice(0, -2).concat( + Terminal._colors.slice(8, -2), options.colors.slice(-2)); + } else if (options.colors.length === 18) { + options.colors = options.colors.slice(0, -2).concat( + Terminal._colors.slice(16, -2), options.colors.slice(-2)); + } + this.colors = options.colors; + + this.options = options; + + // this.context = options.context || window; + // this.document = options.document || document; + this.parent = options.body || options.parent + || (document ? document.getElementsByTagName('body')[0] : null); + + this.cols = options.cols || options.geometry[0]; + this.rows = options.rows || options.geometry[1]; + + // Act as though we are a node TTY stream: + this.setRawMode; + this.isTTY = true; + this.isRaw = true; + this.columns = this.cols; + this.rows = this.rows; + + if (options.handler) { + this.on('data', options.handler); } this.ybase = 0; @@ -144,7 +249,7 @@ function Terminal(cols, rows, handler) { this.y = 0; this.cursorState = 0; this.cursorHidden = false; - this.convertEol = false; + this.convertEol; this.state = 0; this.queue = ''; this.scrollTop = 0; @@ -152,11 +257,24 @@ function Terminal(cols, rows, handler) { // modes this.applicationKeypad = false; + this.applicationCursor = false; this.originMode = false; this.insertMode = false; this.wraparoundMode = false; this.normal = null; + // select modes + this.prefixMode = false; + this.selectMode = false; + this.visualMode = false; + this.searchMode = false; + this.searchDown; + this.entry = ''; + this.entryPrefix = 'Search: '; + this._real; + this._selected; + this._textarea; + // charset this.charset = null; this.gcharset = null; @@ -188,7 +306,7 @@ function Terminal(cols, rows, handler) { this.readable = true; this.writable = true; - this.defAttr = (257 << 9) | 256; + this.defAttr = (0 << 18) | (257 << 9) | (256 << 0); this.curAttr = this.defAttr; this.params = []; @@ -206,14 +324,14 @@ function Terminal(cols, rows, handler) { this.setupStops(); } -inherits(Terminal, EventEmitter); +inherits(Terminal, Stream); /** * Colors */ // Colors 0-15 -Terminal.colors = [ +Terminal.tangoColors = [ // dark: '#2e3436', '#cc0000', @@ -234,10 +352,31 @@ Terminal.colors = [ '#eeeeec' ]; -// Colors 16-255 +Terminal.xtermColors = [ + // dark: + '#000000', // black + '#cd0000', // red3 + '#00cd00', // green3 + '#cdcd00', // yellow3 + '#0000ee', // blue2 + '#cd00cd', // magenta3 + '#00cdcd', // cyan3 + '#e5e5e5', // gray90 + // bright: + '#7f7f7f', // gray50 + '#ff0000', // red + '#00ff00', // green + '#ffff00', // yellow + '#5c5cff', // rgb:5c/5c/ff + '#ff00ff', // magenta + '#00ffff', // cyan + '#ffffff' // white +]; + +// Colors 0-15 + 16-255 // Much thanks to TooTallNate for writing this. Terminal.colors = (function() { - var colors = Terminal.colors + var colors = Terminal.tangoColors.slice() , r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] , i; @@ -267,27 +406,55 @@ Terminal.colors = (function() { })(); // Default BG/FG -Terminal.defaultColors = { - bg: '#000000', - fg: '#f0f0f0' -}; +Terminal.colors[256] = '#000000'; +Terminal.colors[257] = '#f0f0f0'; -Terminal.colors[256] = Terminal.defaultColors.bg; -Terminal.colors[257] = Terminal.defaultColors.fg; +Terminal._colors = Terminal.colors.slice(); + +Terminal.vcolors = (function() { + var out = [] + , colors = Terminal.colors + , i = 0 + , color; + + for (; i < 256; i++) { + color = parseInt(colors[i].substring(1), 16); + out.push([ + (color >> 16) & 0xff, + (color >> 8) & 0xff, + color & 0xff + ]); + } + + return out; +})(); /** * Options */ -Terminal.termName = 'xterm'; -Terminal.geometry = [80, 30]; -Terminal.cursorBlink = true; -Terminal.visualBell = false; -Terminal.popOnBell = false; -Terminal.scrollback = 1000; -Terminal.screenKeys = false; -Terminal.programFeatures = false; -Terminal.debug = false; +Terminal.defaults = { + colors: Terminal.colors, + convertEol: false, + termName: 'xterm', + geometry: [80, 24], + cursorBlink: true, + visualBell: false, + popOnBell: false, + scrollback: 1000, + screenKeys: false, + debug: false, + useStyle: false + // programFeatures: false, + // focusKeys: false, +}; + +Terminal.options = {}; + +each(keys(Terminal.defaults), function(key) { + Terminal[key] = Terminal.defaults[key]; + Terminal.options[key] = Terminal.defaults[key]; +}); /** * Focused Terminal @@ -296,115 +463,640 @@ Terminal.debug = false; Terminal.focus = null; Terminal.prototype.focus = function() { - if (Terminal.focus === this) return; - if (Terminal.focus) { - Terminal.focus.cursorState = 0; - Terminal.focus.refresh(Terminal.focus.y, Terminal.focus.y); - if (Terminal.focus.sendFocus) Terminal.focus.send('\x1b[O'); + if (this._textarea) { + this._textarea.focus(); } - Terminal.focus = this; + + if (Terminal.focus === this) return; + + if (Terminal.focus) { + Terminal.focus.blur(); + } + if (this.sendFocus) this.send('\x1b[I'); this.showCursor(); + + // try { + // this.element.focus(); + // } catch (e) { + // ; + // } + + // this.emit('focus'); + + Terminal.focus = this; +}; + +Terminal.prototype.blur = function() { + if (Terminal.focus !== this) return; + + this.cursorState = 0; + this.refresh(this.y, this.y); + if (this.sendFocus) this.send('\x1b[O'); + + // try { + // this.element.blur(); + // } catch (e) { + // ; + // } + + // this.emit('blur'); + + Terminal.focus = null; +}; + +/** + * Initialize global behavior + */ + +Terminal.prototype.initGlobal = function() { + var document = this.document; + + Terminal._boundDocs = Terminal._boundDocs || []; + if (~indexOf(Terminal._boundDocs, document)) { + return; + } + Terminal._boundDocs.push(document); + + Terminal.bindPaste(document); + + Terminal.bindKeys(document); + + Terminal.bindCopy(document); + + if (this.useStyle) { + Terminal.insertStyle(document, this.colors[256], this.colors[257]); + } +}; + +/** + * Bind to paste event + */ + +Terminal.bindPaste = function(document) { + // This seems to work well for ctrl-V and middle-click, + // even without the contentEditable workaround. + var window = document.defaultView; + on(window, 'paste', function(ev) { + var term = Terminal.focus; + if (!term) return; + if (term._textarea) return; + if (ev.clipboardData) { + term.send(ev.clipboardData.getData('text/plain')); + } else if (term.context.clipboardData) { + term.send(term.context.clipboardData.getData('Text')); + } + // Not necessary. Do it anyway for good measure. + term.element.contentEditable = 'inherit'; + return cancel(ev); + }); }; /** * Global Events for key handling */ -Terminal.bindKeys = function() { - if (Terminal.focus) return; - - // We could put an "if (Terminal.focus)" check - // here, but it shouldn't be necessary. +Terminal.bindKeys = function(document) { + // We should only need to check `target === body` below, + // but we can check everything for good measure. on(document, 'keydown', function(ev) { - return Terminal.focus.keyDown(ev); + if (!Terminal.focus) return; + var target = ev.target || ev.srcElement; + if (!target) return; + if (target === Terminal.focus.element + || target === Terminal.focus.context + || target === Terminal.focus.document + || target === Terminal.focus.body + || target === Terminal.focus._textarea + || target === Terminal.focus.parent) { + return Terminal.focus.keyDown(ev); + } }, true); on(document, 'keypress', function(ev) { - return Terminal.focus.keyPress(ev); + if (!Terminal.focus) return; + var target = ev.target || ev.srcElement; + if (!target) return; + if (target === Terminal.focus.element + || target === Terminal.focus.context + || target === Terminal.focus.document + || target === Terminal.focus.body + || target === Terminal.focus._textarea + || target === Terminal.focus.parent) { + return Terminal.focus.keyPress(ev); + } }, true); + + // If we click somewhere other than a + // terminal, unfocus the terminal. + on(document, 'mousedown', function(ev) { + if (!Terminal.focus) return; + + var el = ev.target || ev.srcElement; + if (!el) return; + if (!el.parentNode) return; + if (!el.parentNode.parentNode) return; + + do { + if (el === Terminal.focus.element) return; + } while (el = el.parentNode); + + Terminal.focus.blur(); + }); +}; + +/** + * Copy Selection w/ Ctrl-C (Select Mode) + */ + +Terminal.bindCopy = function(document) { + var window = document.defaultView; + + // if (!('onbeforecopy' in document)) { + // // Copies to *only* the clipboard. + // on(window, 'copy', function fn(ev) { + // var term = Terminal.focus; + // if (!term) return; + // if (!term._selected) return; + // var text = term.grabText( + // term._selected.x1, term._selected.x2, + // term._selected.y1, term._selected.y2); + // term.emit('copy', text); + // ev.clipboardData.setData('text/plain', text); + // }); + // return; + // } + + // Copies to primary selection *and* clipboard. + // NOTE: This may work better on capture phase, + // or using the `beforecopy` event. + on(window, 'copy', function(ev) { + var term = Terminal.focus; + if (!term) return; + if (!term._selected) return; + var textarea = term.getCopyTextarea(); + var text = term.grabText( + term._selected.x1, term._selected.x2, + term._selected.y1, term._selected.y2); + term.emit('copy', text); + textarea.focus(); + textarea.textContent = text; + textarea.value = text; + textarea.setSelectionRange(0, text.length); + setTimeout(function() { + term.element.focus(); + term.focus(); + }, 1); + }); +}; + +/** + * Fix Mobile + */ + +Terminal.prototype.getTextarea = function(document) { + var self = this; + + var textarea = document.createElement('textarea'); + textarea.style.position = 'absolute'; + textarea.style.left = '-32000px'; + textarea.style.top = '-32000px'; + textarea.style.width = '100em'; + textarea.style.height = '2em'; + textarea.style.padding = '0'; + textarea.style.opacity = '0'; + textarea.style.color = 'inherit'; + textarea.style.font = 'inherit'; + textarea.style.textIndent = '-1em'; /* Hide text cursor on IE */ + textarea.style.backgroundColor = 'transparent'; + textarea.style.borderStyle = 'none'; + textarea.style.outlineStyle = 'none'; + textarea.autocapitalize = 'none'; + textarea.autocorrect = 'off'; + + var onInputTimestamp; + + var onInput = function(ev){ + if(ev.timeStamp && ev.timeStamp === onInputTimestamp){ + return; + } + onInputTimestamp = ev.timeStamp; + + var value = textarea.textContent || textarea.value; + if (typeof self.select.startPos !== 'undefined'){ + self.select = {}; + self.clearSelectedText(); + self.refresh(0, self.rows - 1); + } + if (!self.compositionStatus) { + textarea.value = ''; + textarea.textContent = ''; + self.send(value); + } + }; + + on(textarea, 'compositionstart', function() { + textarea.style.opacity = "1.0"; + textarea.style.textIndent = "0"; + self.compositionStatus = true; + }); + on(textarea, 'compositionend', function(ev) { + textarea.style.opacity = "0.0"; + textarea.style.textIndent = "-1em"; + self.compositionStatus = false; + setTimeout(function(){ + onInput(ev); // for IE that does not trigger 'input' after the IME composition. + }, 1); + }); + + on(textarea, 'keydown', function(){ + var value = textarea.textContent || textarea.value; + }); + + on(textarea, 'input', onInput); + + if (Terminal.isAndroid) { + on(textarea, 'change', function() { + var value = textarea.textContent || textarea.value; + textarea.value = ''; + textarea.textContent = ''; + self.send(value + '\r'); + }); + } + return textarea; +}; + +/** + * Insert a default style + */ + +Terminal.insertStyle = function(document, bg, fg) { + var style = document.getElementById('term-style'); + if (style) return; + + var head = document.getElementsByTagName('head')[0]; + if (!head) return; + + var style = document.createElement('style'); + style.id = 'term-style'; + + // textContent doesn't work well with IE for