import Tabs from './tabs';
import Term from './term';
import RPC from './rpc';
import Mousetrap from 'mousetrap';
import classes from 'classnames';
import shallowCompare from 'react-addons-shallow-compare';
import React, { Component } from 'react';
import UpdateChecker from './update-checker';
export default class HyperTerm extends Component {
constructor () {
super();
this.state = {
cols: null,
rows: null,
hpadding: 10,
vpadding: 5,
sessions: [],
titles: {},
urls: {},
active: null,
activeMarkers: [],
mac: /Mac/.test(navigator.userAgent),
resizeIndicatorShowing: false,
updateVersion: null,
updateNote: null
};
// we set this to true when the first tab
// has been initialized and ack'd by the
// node server for the *first time*
this.init = false;
// we keep track of activity in tabs to avoid
// placing an activity marker right after
// opening
this.tabWasActive = {};
this.onResize = this.onResize.bind(this);
this.onChange = this.onChange.bind(this);
this.openExternal = this.openExternal.bind(this);
this.focusActive = this.focusActive.bind(this);
this.closeBrowser = this.closeBrowser.bind(this);
this.onHeaderMouseDown = this.onHeaderMouseDown.bind(this);
this.moveLeft = this.moveLeft.bind(this);
this.moveRight = this.moveRight.bind(this);
this.resetFontSize = this.resetFontSize.bind(this);
this.increaseFontSize = this.increaseFontSize.bind(this);
this.decreaseFontSize = this.decreaseFontSize.bind(this);
}
render () {
return
{
const title = this.state.titles[uid];
return null != title ? title : 'Shell';
})}
onChange={this.onChange}
/>
{
this.state.sessions.map((uid, i) => {
const active = i === this.state.active;
return
;
})
}
{ this.state.cols }x{ this.state.rows }
Update available (
{ this.state.updateVersion }).
{this.state.updateNote ? ` ${this.state.updateNote}. ` : ' '}
Download
;
}
openExternal (ev) {
ev.preventDefault();
this.rpc.emit('open external', { url: ev.target.href });
}
requestTab () {
// we send the hterm default size
this.rpc.emit('new', { cols: this.state.cols, rows: this.state.rows });
}
closeTab () {
if (this.state.sessions.length) {
const uid = this.state.sessions[this.state.active];
this.rpc.emit('exit', { uid });
this.onSessionExit(uid);
}
}
closeBrowser () {
const uid = this.state.sessions[this.state.active];
if (this.state.urls[uid]) {
const urls = Object.assign({}, this.state.urls);
delete urls[uid];
this.setState({ urls });
}
}
write (uid, data) {
this.rpc.emit('data', { uid, data });
}
onURL (uid, url) {
const urls = Object.assign({}, this.state.urls, { [uid]: url });
this.setState({ urls });
}
onRemoteTitle ({ uid, title }) {
this.setTitle(uid, title);
}
setTitle (uid, title) {
const { titles: _titles } = this.state;
const titles = Object.assign({}, _titles, { [uid]: title });
this.setState({ titles });
}
onActive (uid) {
const i = this.state.sessions.indexOf(uid);
// we ignore activity markers all the way
// up to the tab's been active
const wasActive = this.tabWasActive[uid];
if (!wasActive) {
console.log('ignoring active, tab has not been focused', uid);
this.tabWasActive[uid] = true;
return;
}
if (this.state.active !== i && !~this.state.activeMarkers.indexOf(i)) {
const activeMarkers = this.state.activeMarkers.slice();
activeMarkers.push(i);
this.setState({ activeMarkers });
}
}
shouldComponentUpdate (nextProps, nextState) {
if (this.state.active !== nextState.active) {
const curUid = this.state.sessions[this.state.active];
// make sure that the blurred uid has not been
// optimistically removed
if (curUid && ~nextState.sessions.indexOf(curUid)) {
this.rpc.emit('blur', { uid: curUid });
}
const nextUid = nextState.sessions[nextState.active];
this.rpc.emit('focus', { uid: nextUid });
this.shouldInitKeys = true;
} else {
this.shouldInitKeys = false;
}
return shallowCompare(this, nextProps, nextState);
}
componentDidMount () {
this.rpc = new RPC();
this.updateChecker = new UpdateChecker(this.onUpdateAvailable.bind(this));
// open a new tab upon mounting
this.rpc.once('ready', () => this.requestTab());
this.rpc.on('new session', ({ uid }) => {
const { sessions: _sessions } = this.state;
const sessions = _sessions.concat(uid);
const state = { sessions };
state.active = sessions.length - 1;
this.setState(state, () => {
if (this.state.sessions.length && !this.init) {
this.rpc.emit('init');
this.init = true;
}
});
});
this.rpc.on('clear', this.clearCurrentTerm.bind(this));
this.rpc.on('exit', this.onSessionExit.bind(this));
this.rpc.on('data', ({ uid, data }) => {
if (this.ignoreActivity) {
// we ignore activity for up to 300ms after triggering
// a resize to avoid setting up markers incorrectly
if (Date.now() - this.ignoreActivity < 300) {
console.log('ignore activity after resizing');
} else {
this.ignoreActivity = null;
this.onActive(uid);
}
} else {
this.onActive(uid);
}
this.refs[`term-${uid}`].write(data);
});
this.rpc.on('new tab', this.requestTab.bind(this));
this.rpc.on('close tab', this.closeTab.bind(this));
this.rpc.on('title', this.onRemoteTitle.bind(this));
this.rpc.on('move left', this.moveLeft);
this.rpc.on('move right', this.moveRight);
this.rpc.on('increase font size', this.increaseFontSize);
this.rpc.on('decrease font size', this.decreaseFontSize);
this.rpc.on('reset font size', this.resetFontSize);
}
clearCurrentTerm () {
const uid = this.state.sessions[this.state.active];
const term = this.refs[`term-${uid}`];
term.clear();
}
onUpdateAvailable (updateVersion, updateNote = '') {
updateNote = updateNote.replace(/\.$/, '');
this.setState({ updateVersion, updateNote });
}
moveTo (n) {
if (this.state.sessions[n]) {
this.setActive(n);
}
}
moveLeft () {
const next = this.state.active - 1;
if (this.state.sessions[next]) {
this.setActive(next);
} else if (this.state.sessions.length > 1) {
// go to the end
this.setActive(this.state.sessions.length - 1);
}
}
moveRight () {
const next = this.state.active + 1;
if (this.state.sessions[next]) {
this.setActive(next);
} else if (this.state.sessions.length > 1) {
// go to the beginning
this.setActive(0);
}
}
changeFontSize (value) {
const uid = this.state.sessions[this.state.active];
const term = this.refs[`term-${uid}`];
if (term) {
try {
const size = term.term.prefs_.get('font-size');
term.term.prefs_.set('font-size', size + value);
} catch (e) {
alert(e);
}
}
}
resetFontSize () {
const uid = this.state.sessions[this.state.active];
const term = this.refs[`term-${uid}`];
if (term) {
//TODO: once we have preferences, we need to read from it
term.term.prefs_.set('font-size', 12);
}
}
increaseFontSize () {
this.changeFontSize(1);
}
decreaseFontSize () {
this.changeFontSize(-1);
}
onSessionExit ({ uid }) {
if (!~this.state.sessions.indexOf(uid)) {
console.log('ignore exit of', uid);
return;
}
const {
sessions: _sessions,
titles: _titles,
active: _active,
activeMarkers
} = this.state;
const titles = Object.assign({}, _titles);
delete titles[uid];
delete this.tabWasActive[uid];
const i = _sessions.indexOf(uid);
const sessions = _sessions.slice();
sessions.splice(i, 1);
if (!sessions.length) {
return window.close();
}
const ai = activeMarkers.indexOf(i);
if (~ai) {
activeMarkers.splice(ai, 1);
}
let active;
if (i === _active) {
if (sessions.length) {
active = sessions[i - 1] ? i - 1 : i;
} else {
active = null;
}
} else if (i < _active) {
active = _active - 1;
}
if (~activeMarkers.indexOf(active)) {
activeMarkers.splice(active, 1);
}
this.setState({
sessions,
titles,
active,
activeMarkers
});
}
componentDidUpdate () {
if (this.shouldInitKeys) {
if (this.keys) {
this.keys.reset();
}
const uid = this.state.sessions[this.state.active];
const term = this.refs[`term-${uid}`];
const keys = new Mousetrap(term.getTermDocument());
keys.bind('command+1', this.moveTo.bind(this, 0));
keys.bind('command+2', this.moveTo.bind(this, 1));
keys.bind('command+3', this.moveTo.bind(this, 2));
keys.bind('command+4', this.moveTo.bind(this, 3));
keys.bind('command+5', this.moveTo.bind(this, 4));
keys.bind('command+6', this.moveTo.bind(this, 5));
keys.bind('command+7', this.moveTo.bind(this, 6));
keys.bind('command+8', this.moveTo.bind(this, 7));
keys.bind('command+9', this.moveTo.bind(this, 8));
keys.bind('command+shift+left', this.moveLeft);
keys.bind('command+shift+right', this.moveRight);
keys.bind('command+shift+[', this.moveLeft);
keys.bind('command+shift+]', this.moveRight);
keys.bind('command+alt+left', this.moveLeft);
keys.bind('command+alt+right', this.moveRight);
keys.bind('command+=', this.increaseFontSize);
keys.bind('command+-', this.decreaseFontSize);
keys.bind('command+0', this.resetFontSize);
this.keys = keys;
}
this.focusActive();
}
focusActive () {
// get active uid and term
const uid = this.state.sessions[this.state.active];
const term = this.refs[`term-${uid}`];
if (term) {
term.focus();
}
}
onResize (dim) {
if (dim.rows !== this.state.rows || dim.cols !== this.state.cols) {
this.ignoreActivity = Date.now();
this.rpc.emit('resize', dim);
const state = Object.assign({}, dim,
// if it's the first time we hear about the resize we
// don't show the indicator
null === this.state.rows ? {} : { resizeIndicatorShowing: true }
);
this.setState(state);
clearTimeout(this.resizeIndicatorTimeout);
this.resizeIndicatorTimeout = setTimeout(() => {
this.setState({ resizeIndicatorShowing: false });
}, 1500);
}
}
onChange (active) {
// we ignore clicks if they're a byproduct of a drag
// motion to move the window
if (window.screenX !== this.headerMouseDownWindowX ||
window.screenY !== this.headerMouseDownWindowY) {
return;
}
this.setActive(active);
}
setActive (active) {
if (~this.state.activeMarkers.indexOf(active)) {
const { activeMarkers } = this.state;
activeMarkers.splice(activeMarkers.indexOf(active), 1);
this.setState({ active, activeMarkers });
} else {
this.setState({ active });
}
}
onHeaderMouseDown () {
this.headerMouseDownWindowX = window.screenX;
this.headerMouseDownWindowY = window.screenY;
this.clicks = this.clicks || 1;
if (this.clicks++ >= 2) {
if (this.maximized) {
this.rpc.emit('unmaximize');
} else {
this.rpc.emit('maximize');
}
this.clicks = 0;
this.maximized = !this.maximized;
} else {
// http://www.quirksmode.org/dom/events/click.html
// https://en.wikipedia.org/wiki/Double-click
this.clickTimer = setTimeout(() => this.clicks = 0, 500);
}
}
componentWillUnmount () {
this.rpc.destroy();
clearTimeout(this.resizeIndicatorTimeout);
if (this.keys) this.keys.reset();
delete this.clicks;
clearTimeout(this.clickTimer);
this.updateChecker.destroy();
}
}