/** * 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. * * 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'; var Stream = require('stream'); /** * Shared */ var document = this.document; /** * States */ var normal = 0; var escaped = 1; var csi = 2; var osc = 3; var charset = 4; var dcs = 5; var ignore = 6; var 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 = false; this.isTTY = true; this.isRaw = true; this.columns = this.cols; 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 = false; 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 = true; this.entry = ''; this.entryPrefix = 'Search: '; this._real = null; this._selected = null; this._textarea = null; // charset this.charset = null; this.gcharset = null; this.glevel = 0; this.charsets = [null]; // mouse properties this.decLocator = null; this.x10Mouse = null; this.vt200Mouse = null; this.vt300Mouse = null; this.normalMouse = null; this.mouseEvents = null; this.sendFocus = null; this.utfMouse = null; this.sgrMouse = null; this.urxvtMouse = null; // misc this.element = null; this.children = null; this.refreshStart = null; this.refreshEnd = null; this.savedX = null; this.savedY = null; this.savedCols = null; // 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 = null; this.setupStops(); } inherits(Terminal, Stream); // 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 i; var colors = Terminal.tangoColors.slice(); var r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; // 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 = []; var colors = Terminal.colors; var i = 0; var 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 }; 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 (Terminal.focus === this) return; if (Terminal.focus) { Terminal.focus.blur(); } if (this.sendFocus) this.send('\x1b[I'); this.showCursor(); 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'); 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.isMobile) { this.fixMobile(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 (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._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._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; 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; // 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.fixMobile = 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 = '0px'; textarea.style.height = '0px'; textarea.style.opacity = '0'; textarea.style.backgroundColor = 'transparent'; textarea.style.borderStyle = 'none'; textarea.style.outlineStyle = 'none'; textarea.autocapitalize = 'none'; textarea.autocorrect = 'off'; document.getElementsByTagName('body')[0].appendChild(textarea); Terminal._textarea = textarea; setTimeout(function (){ textarea.focus(); }, 1000); if (this.isAndroid) { on(textarea, 'change', function (){ var value = textarea.textContent || textarea.value; textarea.value = ''; textarea.textContent = ''; self.send(value + '\r'); }); } }; /** * 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; style = document.createElement('style'); style.id = 'term-style'; // textContent doesn't work well with IE for