/** * 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 * The original design remains. The terminal itself * has been extended to include xterm CSI codes, among * other features. */ ;(function() { /** * Terminal Emulation References: * http://vt100.net/ * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html * http://invisible-island.net/vttest/ * http://www.inwap.com/pdp10/ansicode.txt * http://linux.die.net/man/4/console_codes * http://linux.die.net/man/7/urxvt */ 'use strict'; /** * Shared */ var window = this , document = this.document; /** * EventEmitter */ function EventEmitter() { this._events = this._events || {}; } EventEmitter.prototype.addListener = function(type, listener) { this._events[type] = this._events[type] || []; this._events[type].push(listener); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.removeListener = function(type, listener) { if (!this._events[type]) return; var obj = this._events[type] , i = obj.length; while (i--) { if (obj[i] === listener || obj[i].listener === listener) { obj.splice(i, 1); return; } } }; EventEmitter.prototype.off = EventEmitter.prototype.removeListener; EventEmitter.prototype.removeAllListeners = function(type) { if (this._events[type]) delete this._events[type]; }; EventEmitter.prototype.once = function(type, listener) { function on() { var args = Array.prototype.slice.call(arguments); this.removeListener(type, on); return listener.apply(this, args); } on.listener = listener; return this.on(type, on); }; EventEmitter.prototype.emit = function(type) { if (!this._events[type]) return; var args = Array.prototype.slice.call(arguments, 1) , obj = this._events[type] , l = obj.length , i = 0; for (; i < l; i++) { obj[i].apply(this, args); } }; 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 */ var normal = 0 , escaped = 1 , csi = 2 , osc = 3 , charset = 4 , dcs = 5 , ignore = 6 , UDK = { type: 'udk' }; /** * Terminal */ function Terminal(options) { var self = this; if (!(this instanceof Terminal)) { return new Terminal(arguments[0], arguments[1], arguments[2]); } Stream.call(this); 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; this.ydisp = 0; this.x = 0; this.y = 0; this.cursorState = 0; this.cursorHidden = false; this.convertEol; this.state = 0; this.queue = ''; this.scrollTop = 0; this.scrollBottom = this.rows - 1; // 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; this.glevel = 0; this.charsets = [null]; // mouse properties this.decLocator; this.x10Mouse; this.vt200Mouse; this.vt300Mouse; this.normalMouse; this.mouseEvents; this.sendFocus; this.utfMouse; this.sgrMouse; this.urxvtMouse; // misc this.element; this.children; this.refreshStart; this.refreshEnd; this.savedX; this.savedY; this.savedCols; // stream this.readable = true; this.writable = true; this.defAttr = (0 << 18) | (257 << 9) | (256 << 0); this.curAttr = this.defAttr; this.params = []; this.currentParam = 0; this.prefix = ''; this.postfix = ''; this.lines = []; var i = this.rows; while (i--) { this.lines.push(this.blankLine()); } this.tabs; this.setupStops(); } inherits(Terminal, Stream); /** * Colors */ // Colors 0-15 Terminal.tangoColors = [ // dark: '#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', // bright: '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec' ]; 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.tangoColors.slice() , r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] , i; // 16-231 i = 0; for (; i < 216; i++) { out(r[(i / 36) % 6 | 0], r[(i / 6) % 6 | 0], r[i % 6]); } // 232-255 (grey) i = 0; for (; i < 24; i++) { r = 8 + i * 10; out(r, r, r); } function out(r, g, b) { colors.push('#' + hex(r) + hex(g) + hex(b)); } function hex(c) { c = c.toString(16); return c.length < 2 ? '0' + c : c; } return colors; })(); // Default BG/FG Terminal.colors[256] = '#000000'; Terminal.colors[257] = '#f0f0f0'; 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.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 */ Terminal.focus = null; Terminal.prototype.focus = function() { if (this._textarea) { this._textarea.focus(); } 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(document) { // We should only need to check `target === body` below, // but we can check everything for good measure. on(document, 'keydown', function(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) { 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