diff --git a/client/app/scripts/components/app.js b/client/app/scripts/components/app.js
index 4220d859d..3b9238402 100644
--- a/client/app/scripts/components/app.js
+++ b/client/app/scripts/components/app.js
@@ -72,14 +72,13 @@ class App extends React.Component {
}
onKeyPress(ev) {
- const { dispatch, searchFocused } = this.props;
+ const { dispatch, searchFocused, showingTerminal } = this.props;
//
// keyup gives 'key'
// keypress gives 'char'
// Distinction is important for international keyboard layouts where there
// is often a different {key: char} mapping.
- //
- if (!searchFocused) {
+ if (!searchFocused && !showingTerminal) {
keyPressLog('onKeyPress', 'keyCode', ev.keyCode, ev);
const char = String.fromCharCode(ev.charCode);
if (char === '<') {
diff --git a/client/app/scripts/components/debug-toolbar.js b/client/app/scripts/components/debug-toolbar.js
index 8a5220155..1888d9fda 100644
--- a/client/app/scripts/components/debug-toolbar.js
+++ b/client/app/scripts/components/debug-toolbar.js
@@ -261,6 +261,7 @@ class DebugToolbar extends React.Component {
+
diff --git a/client/app/scripts/components/terminal.js b/client/app/scripts/components/terminal.js
index 570cdd06c..d1989f28d 100644
--- a/client/app/scripts/components/terminal.js
+++ b/client/app/scripts/components/terminal.js
@@ -9,7 +9,7 @@ import { clickCloseTerminal } from '../actions/app-actions';
import { getNeutralColor } from '../utils/color-utils';
import { setDocumentTitle } from '../utils/title-utils';
import { getPipeStatus, basePath } from '../utils/web-api-utils';
-import Term from '../vendor/term.js';
+import Term from 'xterm';
const wsProto = location.protocol === 'https:' ? 'wss' : 'ws';
const wsUrl = `${wsProto}://${location.host}${basePath(location.pathname)}`;
@@ -24,6 +24,7 @@ const MDASH = '\u2014';
const reconnectTimerInterval = 2000;
+
function ab2str(buf) {
// http://stackoverflow.com/questions/17191945/conversion-between-utf-8-arraybuffer-and-string
const encodedString = String.fromCharCode.apply(null, new Uint8Array(buf));
@@ -54,6 +55,7 @@ function terminalCellSize(wrapperNode, rows, cols) {
return {pixelPerCol, pixelPerRow};
}
+
function openNewWindow(url, bcr, minWidth = 200) {
const screenLeft = window.screenX || window.screenLeft;
const screenTop = window.screenY || window.screenTop;
@@ -74,6 +76,7 @@ function openNewWindow(url, bcr, minWidth = 200) {
window.open(url, '', windowOptionsString);
}
+
class Terminal extends React.Component {
constructor(props, context) {
super(props, context);
@@ -117,7 +120,11 @@ class Terminal extends React.Component {
}
this.socket = null;
const wereConnected = this.state.connected;
- this.setState({connected: false});
+ if (this._isMounted) {
+ // Calling setState on an unmounted component will throw a warning.
+ // `connected` will get set to false by `componentWillUnmount`.
+ this.setState({connected: false});
+ }
if (this.term && this.props.pipe.get('status') !== 'PIPE_DELETED') {
if (wereConnected) {
this.createWebsocket(term);
@@ -148,19 +155,22 @@ class Terminal extends React.Component {
}
componentDidMount() {
+ this._isMounted = true;
if (this.props.connect) {
this.mountTerminal();
}
}
mountTerminal() {
+ const component = this;
this.term = new Term({
cols: this.state.cols,
rows: this.state.rows,
- convertEol: !this.props.raw
+ convertEol: !this.props.raw,
+ cursorBlink: true
});
- const innerNode = ReactDOM.findDOMNode(this.inner);
+ const innerNode = ReactDOM.findDOMNode(component.innerFlex);
this.term.open(innerNode);
this.term.on('data', (data) => {
if (this.socket) {
@@ -171,7 +181,7 @@ class Terminal extends React.Component {
this.createWebsocket(this.term);
const {pixelPerCol, pixelPerRow} = terminalCellSize(
- innerNode, this.state.rows, this.state.cols);
+ this.term.element, this.state.rows, this.state.cols);
window.addEventListener('resize', this.handleResize);
@@ -185,6 +195,8 @@ class Terminal extends React.Component {
}
componentWillUnmount() {
+ this._isMounted = false;
+ this.setState({connected: false});
log('cwu terminal');
clearTimeout(this.reconnectTimeout);
diff --git a/client/app/scripts/vendor/term.js b/client/app/scripts/vendor/term.js
deleted file mode 100644
index 7af1c5b72..000000000
--- a/client/app/scripts/vendor/term.js
+++ /dev/null
@@ -1,5992 +0,0 @@
-/**
- * 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 (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