UNPKG

project-nexus

Version:

A hub for all your programming projects

2,063 lines (1,761 loc) 150 kB
/** * 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] || []; }; /** * States */ var normal = 0 , escaped = 1 , csi = 2 , osc = 3 , charset = 4 , dcs = 5 , ignore = 6; /** * Terminal */ function Terminal(options) { var self = this; if (!(this instanceof Terminal)) { return new Terminal(arguments[0], arguments[1], arguments[2]); } EventEmitter.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]; 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, EventEmitter); // back_color_erase feature for xterm. Terminal.prototype.eraseAttr = function() { // if (this.is('screen')) return this.defAttr; return (this.defAttr & ~0x1ff) | (this.curAttr & 0x1ff); }; /** * 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 (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.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; // 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.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; var style = document.createElement('style'); style.id = 'term-style'; // textContent doesn't work well with IE for <style> elements. style.innerHTML = '' + '.terminal {\n' + ' float: left;\n' + ' border: ' + bg + ' solid 5px;\n' + ' font-family: "DejaVu Sans Mono", "Liberation Mono", monospace;\n' + ' font-size: 11px;\n' + ' color: ' + fg + ';\n' + ' background: ' + bg + ';\n' + '}\n' + '\n' + '.terminal-cursor {\n' + ' color: ' + bg + ';\n' + ' background: ' + fg + ';\n' + '}\n'; // var out = ''; // each(Terminal.colors, function(color, i) { // if (i === 256) { // out += '\n.term-bg-color-default { background-color: ' + color + '; }'; // } // if (i === 257) { // out += '\n.term-fg-color-default { color: ' + color + '; }'; // } // out += '\n.term-bg-color-' + i + ' { background-color: ' + color + '; }'; // out += '\n.term-fg-color-' + i + ' { color: ' + color + '; }'; // }); // style.innerHTML += out + '\n'; head.insertBefore(style, head.firstChild); }; /** * Open Terminal */ Terminal.prototype.open = function(parent) { var self = this , i = 0 , div; this.parent = parent || this.parent; if (!this.parent) { throw new Error('Terminal requires a parent element.'); } // Grab global elements. this.context = this.parent.ownerDocument.defaultView; this.document = this.parent.ownerDocument; this.body = this.document.getElementsByTagName('body')[0]; // Parse user-agent strings. 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.isAndroid = !!~this.context.navigator.userAgent.indexOf('Android'); this.isMobile = this.isIpad || this.isIphone || this.isAndroid; this.isMSIE = !!~this.context.navigator.userAgent.indexOf('MSIE'); } // Create our main terminal element. this.element = this.document.createElement('div'); this.element.className = 'terminal'; this.element.style.outline = 'none'; this.element.setAttribute('tabindex', 0); this.element.setAttribute('spellcheck', 'false'); this.element.style.backgroundColor = this.colors[256]; this.element.style.color = this.colors[257]; // Create the lines for our terminal. this.children = []; for (; i < this.rows; i++) { div = this.document.createElement('div'); this.element.appendChild(div); this.children.push(div); } this.parent.appendChild(this.element); // Draw the screen. this.refresh(0, this.rows - 1); // Initialize global actions that // need to be taken on the document. this.initGlobal(); // Ensure there is a Terminal.focus. this.focus(); // Start blinking the cursor. this.startBlink(); // Bind to DOM events related // to focus and paste behavior. on(this.element, 'focus', function() { self.focus(); if (self.isMobile) { Terminal._textarea.focus(); } }); // This causes slightly funky behavior. // on(this.element, 'blur', function() { // self.blur(); // }); on(this.element, 'mousedown', function() { self.focus(); }); // Clickable paste workaround, using contentEditable. // This probably shouldn't work, // ... but it does. Firefox's paste // event seems to only work for textareas? on(this.element, 'mousedown', function(ev) { var button = ev.button != null ? +ev.button : ev.which != null ? ev.which - 1 : null; // Does IE9 do this? if (self.isMSIE) { button = button === 1 ? 0 : button === 4 ? 1 : button; } if (button !== 2) return; self.element.contentEditable = 'true'; setTimeout(function() { self.element.contentEditable = 'inherit'; // 'false'; }, 1); }, true); // Listen for mouse events and translate // them into terminal mouse protocols. this.bindMouse(); // Figure out whether boldness affects // the character width of monospace fonts. if (Terminal.brokenBold == null) { Terminal.brokenBold = isBoldBroken(this.document); } // this.emit('open'); // This can be useful for pasting, // as well as the iPad fix. setTimeout(function() { self.element.focus(); }, 100); }; // XTerm mouse events // http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking // To better understand these // the xterm code is very helpful: // Relevant files: // button.c, charproc.c, misc.c // Relevant functions in xterm/button.c: // BtnCode, EmitButtonCode, EditorButton, SendMousePosition Terminal.prototype.bindMouse = function() { var el = this.element , self = this , pressed = 32; var wheelEvent = 'onmousewheel' in this.context ? 'mousewheel' : 'DOMMouseScroll'; // mouseup, mousedown, mousewheel // left click: ^[[M 3<^[[M#3< // mousewheel up: ^[[M`3> function sendButton(ev) { var button , pos; // get the xterm-style button button = getButton(ev); // get mouse coordinates pos = getCoords(ev); if (!pos) return; sendEvent(button, pos); switch (ev.type) { case 'mousedown': pressed = button; break; case 'mouseup': // keep it at the left // button, just in case. pressed = 32; break; case wheelEvent: // nothing. don't // interfere with // `pressed`. break; } } // motion example of a left click: // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< function sendMove(ev) { var button = pressed , pos; pos = getCoords(ev); if (!pos) return; // buttons marked as motions // are incremented by 32 button += 32; sendEvent(button, pos); } // encode button and // position to characters function encode(data, ch) { if (!self.utfMouse) { if (ch === 255) return data.push(0); if (ch > 127) ch = 127; data.push(ch); } else { if (ch === 2047) return data.push(0); if (ch < 127) { data.push(ch); } else { if (ch > 2047) ch = 2047; data.push(0xC0 | (ch >> 6)); data.push(0x80 | (ch & 0x3F)); } } } // send a mouse event: // regular/utf8: ^[[M Cb Cx Cy // urxvt: ^[[ Cb ; Cx ; Cy M // sgr: ^[[ Cb ; Cx ; Cy M/m // vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r // locator: CSI P e ; P b ; P r ; P c ; P p & w function sendEvent(button, pos) { // self.emit('mouse', { // x: pos.x - 32, // y: pos.x - 32, // button: button // }); if (self.vt300Mouse) { // NOTE: Unstable. // http://www.vt100.net/docs/vt3xx-gp/chapter15.html button &= 3; pos.x -= 32; pos.y -= 32; var data = '\x1b[24'; if (button === 0) data += '1'; else if (button === 1) data += '3'; else if (button === 2) data += '5'; else if (button === 3) return; else data += '0'; data += '~[' + pos.x + ',' + pos.y + ']\r'; self.send(data); return; } if (self.decLocator) { // NOTE: Unstable. button &= 3; pos.x -= 32; pos.y -= 32; if (button === 0) button = 2; else if (button === 1) button = 4; else if (button === 2) button = 6; else if (button === 3) button = 3; self.send('\x1b[' + button + ';' + (button === 3 ? 4 : 0) + ';' + pos.y + ';' + pos.x + ';' + (pos.page || 0) + '&w'); return; } if (self.urxvtMouse) { pos.x -= 32; pos.y -= 32; pos.x++; pos.y++; self.send('\x1b[' + button + ';' + pos.x + ';' + pos.y + 'M'); return; } if (self.sgrMouse) { pos.x -= 32; pos.y -= 32; self.send('\x1b[<' + ((button & 3) === 3 ? button & ~3 : button) + ';' + pos.x + ';' + pos.y + ((button & 3) === 3 ? 'm' : 'M')); return; } var data = []; encode(data, button); encode(data, pos.x); encode(data, pos.y); self.send('\x1b[M' + String.fromCharCode.apply(String, data)); } function getButton(ev) { var button , shift , meta , ctrl , mod; // two low bits: // 0 = left // 1 = middle // 2 = right // 3 = release // wheel up/down: // 1, and 2 - with 64 added switch (ev.type) { case 'mousedown': button = ev.button != null ? +ev.button : ev.which != null ? ev.which - 1 : null; if (self.isMSIE) { button = button === 1 ? 0 : button === 4 ? 1 : button; } break; case 'mouseup': button = 3; break; case 'DOMMouseScroll': button = ev.detail < 0 ? 64 : 65; break; case 'mousewheel': button = ev.wheelDeltaY > 0 ? 64 : 65; break; } // next three bits are the modifiers: // 4 = shift, 8 = meta, 16 = control shift = ev.shiftKey ? 4 : 0; meta = ev.metaKey ? 8 : 0; ctrl = ev.ctrlKey ? 16 : 0; mod = shift | meta | ctrl; // no mods if (self.vt200Mouse) { // ctrl only mod &= ctrl; } else if (!self.normalMouse) { mod = 0; } // increment to SP button = (32 + (mod << 2)) + button; return button; } // mouse coordinates measured in cols/rows function getCoords(ev) { var x, y, w, h, el; // ignore browsers without pageX for now if (ev.pageX == null) return; x = ev.pageX; y = ev.pageY; el = self.element; // should probably check offsetParent // but this is more portable while (el && el !== self.document.documentElement) { x -= el.offsetLeft; y -= el.offsetTop; el = 'offsetParent' in el ? el.offsetParent : el.parentNode; } // convert to cols/rows w = self.element.clientWidth; h = self.element.clientHeight; x = Math.round((x / w) * self.cols); y = Math.round((y / h) * self.rows); // be sure to avoid sending // bad positions to the program if (x < 0) x = 0; if (x > self.cols) x = self.cols; if (y < 0) y = 0; if (y > self.rows) y = self.rows; // xterm sends raw bytes and // starts at 32 (SP) for each. x += 32; y += 32; return { x: x, y: y, type: ev.type === wheelEvent ? 'mousewheel' : ev.type }; } on(el, 'mousedown', function(ev) { if (!self.mouseEvents) return; // send the button sendButton(ev); // ensure focus self.focus(); // fix for odd bug //if (self.vt200Mouse && !self.normalMouse) { if (self.vt200Mouse) { sendButton({ __proto__: ev, type: 'mouseup' }); return cancel(ev); } // bind events if (self.normalMouse) on(self.document, 'mousemove', sendMove); // x10 compatibility mode can't send button releases if (!self.x10Mouse) { on(self.document, 'mouseup', function up(ev) { sendButton(ev); if (self.normalMouse) off(self.document, 'mousemove', sendMove); off(self.document, 'mouseup', up); return cancel(ev); }); } return cancel(ev); }); //if (self.normalMouse) { // on(self.document, 'mousemove', sendMove); //} on(el, wheelEvent, function(ev) { if (!self.mouseEvents) return; if (self.x10Mouse || self.vt300Mouse || self.decLocator) return; sendButton(ev); return cancel(ev); }); // allow mousewheel scrolling in // the shell for example on(el, wheelEvent, function(ev) { if (self.mouseEvents) return; if (self.applicationKeypad) return; if (ev.type === 'DOMMouseScroll') { self.scrollDisp(ev.detail < 0 ? -5 : 5); } else { self.scrollDisp(ev.wheelDeltaY > 0 ? -5 : 5); } return cancel(ev); }); }; /** * Destroy Terminal */ Terminal.prototype.destroy = function() { this.readable = false; this.writable = false; this._events = {}; this.handler = function() {}; this.write = function() {}; if (this.element.parentNode) { this.element.parentNode.removeChild(this.element); } //this.emit('close'); }; /** * Rendering Engine */ // In the screen buffer, each character // is stored as a an array with a character // and a 32-bit integer. // First value: a utf-16 character. // Second value: // Next 9 bits: background color (0-511). // Next 9 bits: foreground color (0-511). // Next 14 bits: a mask for misc. flags: // 1=bold, 2=underline, 4=blink, 8=inverse, 16=invisible Terminal.prototype.refresh = function(start, end) { var x , y , i , line , out , ch , width , data , attr , bg , fg , flags , row , parent; if (end - start >= this.rows / 2) { parent = this.element.parentNode; if (parent) parent.removeChild(this.element); } width = this.cols; y = start; if (end >= this.lines.length) { this.log('`end` is too large. Most likely a bad CSR.'); end = this.lines.length - 1; } for (; y <= end; y++) { row = y + this.ydisp; line = this.lines[row]; out = ''; if (y === this.y && this.cursorState && (this.ydisp === this.ybase || this.selectMode) && !this.cursorHidden) { x = this.x; } else { x = -1; } attr = this.defAttr; i = 0; for (; i < width; i++) { data = line[i][0]; ch = line[i][1]; if (i === x) data = -1; if (data !== attr) { if (attr !== this.defAttr) { out += '</span>'; } if (data !== this.defAttr) { if (data === -1) { out += '<span class="reverse-video terminal-cursor">'; } else { out += '<span style="'; bg = data & 0x1ff; fg = (data >> 9) & 0x1ff; flags = data >> 18; // bold if (flags & 1) { if (!Terminal.brokenBold) { out += 'font-weight:bold;'; } // See: XTerm*boldColors if (fg < 8) fg += 8; } // underline if (flags & 2) { out += 'text-decoration:underline;'; } // blink if (flags & 4) { if (flags & 2) { out = out.slice(0, -1); out += ' blink;'; } else { out += 'text-decoration:blink;'; } } // inverse if (flags & 8) { bg = (data >> 9) & 0x1ff; fg = data & 0x1ff; // Should inverse just be before the // above boldColors effect instead? if ((flags & 1) && fg < 8) fg += 8; } // invisible if (flags & 16) { out += 'visibility:hidden;'; } // out += '" class="' // + 'term-bg-color-' + bg // + ' ' // + 'term-fg-color-' + fg // + '">'; if (bg !== 256) { out += 'background-color:' + this.colors[bg] + ';'; } if (fg !== 257) { out += 'color:' + this.colors[fg] + ';'; } out += '">'; } } } switch (ch) { case '&': out += '&amp;'; break; case '<': out += '&lt;'; break; case '>': out += '&gt;'; break; default: if (ch <= ' ') { out += '&nbsp;'; } else { if (isWide(ch)) i++; out += ch; } break; } attr = data; } if (attr !== this.defAttr) { out += '</span>'; } this.children[y].innerHTML = out; } if (parent) parent.appendChild(this.element); }; Terminal.prototype._cursorBlink = function() { if (Terminal.focus !== this) return; this.cursorState ^= 1; this.refresh(this.y, this.y); }; Terminal.prototype.showCursor = function() { if (!this.cursorState) { this.cursorState = 1; this.refresh(this.y, this.y); } else { // Temporarily disabled: // this.refreshBlink(); } }; Terminal.prototype.startBlink = function() { if (!this.cursorBlink) return; var self = this; this._blinker = function() { self._cursorBlink(); }; this._blink = setInterval(this._blinker, 500); }; Terminal.prototype.refreshBlink = function() { if (!this.cursorBlink) return; clearInterval(this._blink); this._blink = setInterval(this._blinker, 500); }; Terminal.prototype.scroll = function() { var row; if (++this.ybase === this.scrollback) { this.ybase = this.ybase / 2 | 0; this.lines = this.lines.slice(-(this.ybase + this.rows) + 1); } this.ydisp = this.ybase; // last line row = this.ybase + this.rows - 1; // subtract the bottom scroll region row -= this.rows - 1 - this.scrollBottom; if (row === this.lines.length) { // potential optimization: // pushing is faster than splicing // when they amount to the same // behavior. this.lines.push(this.blankLine()); } else { // add our new line this.lines.splice(row, 0, this.blankLine()); } if (this.scrollTop !== 0) { if (this.ybase !== 0) { this.ybase--; this.ydisp = this.ybase; } this.lines.splice(this.ybase + this.scrollTop, 1); } // this.maxRange(); this.updateRange(this.scrollTop); this.updateRange(this.scrollBottom); }; Terminal.prototype.scrollDisp = function(disp) { this.ydisp += disp; if (this.ydisp > this.ybase) { this.ydisp = this.ybase; } else if (this.ydisp < 0) { this.ydisp = 0; } this.refresh(0, this.rows - 1); }; Terminal.prototype.write = function(data) { var l = data.length , i = 0 , j , cs , ch; this.refreshStart = this.y; this.refreshEnd = this.y; if (this.ybase !== this.ydisp) { this.ydisp = this.ybase; this.maxRange(); } // this.log(JSON.stringify(data.replace(/\x1b/g, '^['))); for (; i < l; i++) { ch = data[i]; switch (this.state) { case normal: switch (ch) { // '\0' // case '\0': // case '\200': // break; // '\a' case '\x07': this.bell(); break; // '\n', '\v', '\f' case '\n': case '\x0b': case '\x0c': if (this.convertEol) { this.x = 0; } // TODO: Implement eat_newline_glitch. // if (this.realX >= this.cols) break; // this.realX = 0; this.y++; if (this.y > this.scrollBottom) { this.y--; this.scroll(); } break; // '\r' case '\r': this.x = 0; break; // '\b' case '\x08': if (this.x > 0) { this.x--; } break; // '\t' case '\t': this.x = this.nextStop(); break; // shift out case '\x0e': this.setgLevel(1); break; // shift in case '\x0f': this.setgLevel(0); break; // '\e' case '\x1b': this.state = escaped; break; default: // ' ' if (ch >= ' ') { if (this.charset && this.charset[ch]) { ch = this.charset[ch]; } if (this.x >= this.cols) { this.x = 0; this.y++; if (this.y > this.scrollBottom) { this.y--; this.scroll(); } } this.lines[this.y + this.ybase][this.x] = [this.curAttr, ch]; this.x++; this.updateRange(this.y); if (isWide(ch)) { j = this.y + this.ybase; if (this.cols < 2 || this.x >= this.cols) { this.lines[j][this.x - 1] = [this.curAttr, ' ']; break; } this.lines[j][this.x] = [this.curAttr, ' ']; this.x++; } } break; } break; case escaped: switch (ch) { // ESC [ Control Sequence Introducer ( CSI is 0x9b). case '[': this.params = []; this.currentParam = 0; this.state = csi; break; // ESC ] Operating System Command ( OSC is 0x9d). case ']': this.params = []; this.currentParam = 0; this.state = osc; break; // ESC P Device Control String ( DCS is 0x90). case 'P': this.params = []; this.currentParam = 0; this.state = dcs; break; // ESC _ Application Program Command ( APC is 0x9f). case '_': this.state = ignore; break; // ESC ^ Privacy Message ( PM is 0x9e). case '^': this.state = ignore; break; // ESC c Full Reset (RIS). case 'c': this.reset(); break; // ESC E Next Line ( NEL is 0x85). // ESC D Index ( IND is 0x84). case 'E': this.x = 0; ; case 'D': this.index(); break; // ESC M Reverse Index ( RI is 0x8d). case 'M': this.reverseIndex(); break; // ESC % Select default/utf-8 character set. // @ = default, G = utf-8 case '%': //this.charset = null; this.setgLevel(0); this.setgCharset(0, Terminal.charsets.US); this.state = normal; i++; break; // ESC (,),*,+,-,. Designate G0-G2 Character Set. case '(': // <-- this seems to get all the attention case ')': case '*': case '+': case '-': case '.': switch (ch) { case '(': this.gcharset = 0; break; case ')': this.gcharset = 1; break; case '*': this.gcharset = 2; break; case '+': this.gcharset = 3; break; case '-': this.gcharset = 1; break; case '.': this.gcharset = 2; break; } this.state = charset; break; // Designate G3 Character Set (VT300). // A = ISO Latin-1 Supplemental. // Not implemented. case '/': this.gcharset = 3; this.state = charset; i--; break; // ESC N // Single Shift Select of G2 Character Set // ( SS2 is 0x8e). This affects next character only. case 'N': break; // ESC O // Single Shift Select of G3 Character Set // ( SS3 is 0x8f). This affects next character only. case 'O': break; // ESC n // Invoke the G2 Character Set as GL (LS2). case 'n': this.setgLevel(2); break; // ESC o // Invoke the G3 Character Set as GL (LS3). case 'o': this.setgLevel(3); break; // ESC | // Invoke the G3 Character Set as GR (LS3R). case '|': this.setgLevel(3); break; // ESC } // Invoke the G2 Character Set as GR (LS2R). case '}': this.setgLevel(2); break; // ESC ~ // Invoke the G1 Character Set as GR (LS1R). case '~': this.setgLevel(1); break; // ESC 7 Save Cursor (DECSC). case '7': this.saveCursor(); this.state = normal; break; // ESC 8 Restore Cursor (DECRC). case '8': this.restoreCursor(); this.state = normal; break; // ESC # 3 DEC line height/width case '#': this.state = normal; i++; break; // ESC H Tab Set (HTS is 0x88). case 'H': this.tabSet(); break; // ESC = Application Keypad (DECPAM). case '=': this.log('Serial port requested application keypad.'); this.applicationKeypad = true; this.state = normal; break; // ESC > Normal Keypad (DECPNM). case '>': this.log('Switching back to normal keypad.'); this.applicationKeypad = false; this.state = normal; break; default: this.state = normal; this.error('Unknown ESC control: %s.', ch); break; } break; case charset: switch (ch) { case '0': // DEC Special Character and Line Drawing Set. cs = Terminal.charsets.SCLD; break; case 'A': // UK cs = Terminal.charsets.UK; break; case 'B': // United States (USASCII). cs = Terminal.charsets.US; break; case '4': // Dutch cs = Terminal.charsets.Dutch; break; case 'C': // Finnish case '5': cs = Terminal.charsets.Finnish; break; case 'R': // French cs = Terminal.charsets.French; break; case 'Q': // FrenchCanadian cs = Terminal.charsets.FrenchCanadian; break; case 'K': // German cs = Terminal.charsets.German; break; case 'Y': // Italian cs = Terminal.charsets.Italian; break; case 'E': // NorwegianDanish case '6': cs = Terminal.charsets.NorwegianDanish; break; case 'Z': // Spanish cs = Terminal.charsets.Spanish; break; case 'H': // Swedish case '7': cs = Terminal.charsets.Swedish; break; case '=': // Swiss cs = Terminal.charsets.Swiss; break; case '/': // ISOLatin (actually /A) cs = Terminal.charsets.ISOLatin; i++; break; default: // Default cs = Terminal.charsets.US; break; } this.setgCharset(this.gcharset, cs); this.gcharset = null; this.state = normal; break; case osc: // OSC Ps ; Pt ST // OSC Ps ; Pt BEL // Set Text Parameters. if (ch === '\x1b' || ch === '\x07') { if (ch === '\x1b') i++; this.params.push(this.currentParam); switch (this.params[0]) { case 0: case 1: case 2: if (this.params[1]) { this.title = this.params[1]; this.handleTitle(this.title); } break; case 3: // set X property break; case 4: case 5: // change dynamic colors break; case 10: case 11: case 12: case 13: case 14: case 15: case 16: case 17: case 18: case 19: // change dynamic ui colors break; case 46: // change log file break; case 50: // dynamic font break; case 51: // emacs shell break; case 52: // manipulate selection data break; case 104: case 105: case 110: case 111: case 112: case 113: case 114: case 115: case 116: case 117: case 118: // reset colors break; } this.params = []; this.currentParam = 0; this.state = normal; } else { if (!this.params.length) { if (ch >= '0' && ch <= '9') { this.currentParam = this.currentParam * 10 + ch.charCodeAt(0) - 48; } else if (ch === ';') { this.params.push(this.currentParam); this.currentParam = ''; } } else { this.currentParam += ch; } } break; case csi: // '?', '>', '!' if (ch === '?' || ch === '>' || ch === '!') { this.prefix = ch; break; } // 0 - 9 if (ch >= '0' && ch <= '9') { this.currentParam = this.currentParam * 10 + ch.charCodeAt(0) - 48; break; } // '$', '"', ' ', '\'' if (ch === '$' || ch === '"' || ch === ' ' || ch === '\'') { this.postfix = ch; break; } this.params.push(this.currentParam); this.currentParam = 0; // ';' if (ch === ';') break; this.state = normal; switch (ch) { // CSI Ps A // Cursor Up Ps Times (default = 1) (CUU). case 'A': this.cursorUp(this.params); break; // CSI Ps B // Cursor Down Ps Times (default = 1) (CUD). case 'B': this.cursorDown(this.params); break; // CSI Ps C // Cursor Forward Ps Times (default = 1) (CUF). case 'C': this.cursorForward(this.params); break; // CSI Ps D // Cursor Backward Ps Times (default = 1) (CUB). case 'D': this.cursorBackward(this.params); break; // CSI Ps ; Ps H // Cursor Position [row;column] (default = [1,1]) (CUP). case 'H': this.cursorPos(this.params); break; // CSI Ps J Erase in Display (ED). case 'J': this.eraseInDisplay(this.params); break; // CSI Ps K Erase in Line (EL). case 'K': this.eraseInLine(this.params); break; // CSI Pm m Character Attributes (SGR). case 'm': if (!this.prefix) { this.charAttributes(this.params); } break; // CSI Ps n Device Status Report (DSR). case 'n': if (!this.prefix) { this.deviceStatus(this.params); } break; /** * Additions */ // CSI Ps @ // Insert Ps (Blank) Character(s) (default = 1) (ICH). case '@': this.insertChars(this.params); break; // CSI Ps E // Cursor Next Line Ps Times (default = 1) (CNL). case 'E': this.cursorNextLine(this.params); break; // CSI Ps F // Cursor Preceding Line Ps Times (default = 1) (CNL). case 'F': this.cursorPrecedingLine(this.params); break; // CSI Ps G // Cursor Character Absolute [column] (default = [row,1]) (CHA). case 'G': this.cursorCharAbsolute(this.params); break; // CSI Ps L // Insert Ps Line(s) (default = 1) (IL). case 'L': this.insertLines(this.params); break; // CSI Ps M // Delete Ps Line(s) (default = 1) (DL). case 'M': this.deleteLines(this.params); break; // CSI Ps P // Delete Ps Character(s) (default = 1) (DCH). case 'P': this.deleteChars(this.params); break; // CSI Ps X // Erase Ps Character(s) (default = 1) (ECH). case 'X': this.eraseChars(this.params); break; // CSI Pm ` Character Position Absolute // [column] (default = [row,1]) (HPA). case '`': this.charPosAbsolute(this.params); break; // 141 61 a * HPR - // Horizontal Position Relative case 'a': this.HPositionRelative(this.params); break; // CSI P s c // Send Device Attributes (Primary DA). // CSI > P s c // Send Device Attributes (Secondary DA) case 'c': this.sendDeviceAttributes(this.params); break; // CSI Pm d // Line Position Absolute [row] (default = [1,column]) (VPA). case 'd': this.linePosAbsolute(this.params); break; // 145 65 e * VPR - Vertical Position Relative case 'e': this.VPositionRelative(this.params); break; // CSI Ps ; Ps f // Horizontal and Vertical Position [row;column] (default = // [1,1]) (HVP). case 'f': this.HVPosition(this.params); break; // CSI Pm h Set Mode (SM). // CSI ? Pm h - mouse escape codes, cursor escape codes case 'h': this.setMode(this.params); break; // CSI Pm l Reset Mode (RM). // CSI ? Pm l case 'l': this.resetMode(this.params); break; // CSI Ps ; Ps r // Set Scrolling Region [top;bottom] (default = full size of win- // dow) (DECSTBM). // CSI ? Pm r case 'r': this.setScrollRegion(this.params); break; // CSI s // Save cursor (ANSI.SYS). case 's': this.saveCursor(this.params); break; // CSI u // Restore cursor (ANSI.SYS). case 'u': this.restoreCursor(this.params); break; /** * Lesser Used */ // CSI Ps I // Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). case 'I': this.cu