mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-12 20:18:41 -09:00
refactor
This commit is contained in:
parent
8fac6bcec9
commit
477e40e433
58 changed files with 2751 additions and 1141 deletions
|
|
@ -1,40 +0,0 @@
|
|||
/* global Notification */
|
||||
/* eslint no-new:0 */
|
||||
import React from 'react';
|
||||
import { ipcRenderer, remote } from 'electron';
|
||||
|
||||
const config = remote.require('./config');
|
||||
|
||||
export default class Config extends React.Component {
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
this.state = {
|
||||
config: config.getConfig()
|
||||
};
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
onChange () {
|
||||
this.setState({ config: config.getConfig() });
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
ipcRenderer.on('config change', this.onChange);
|
||||
ipcRenderer.on('plugins change', this.onChange);
|
||||
}
|
||||
|
||||
// passes `config` as props to the decorated component
|
||||
render () {
|
||||
const child = React.Children.only(this.props.children);
|
||||
const { config } = this.state;
|
||||
const decorate = remote.require('./plugins').decorateConfig;
|
||||
return React.cloneElement(child, { config: decorate(config) });
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ipcRenderer.removeListener('config change', this.onChange);
|
||||
ipcRenderer.removeListener('plugins change', this.onChange);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
.main {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.mac.main {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
header {
|
||||
position: fixed;
|
||||
top: 1px;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
background: #000;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.mac header {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
|
||||
.terms {
|
||||
position: absolute;
|
||||
margin-top: 28px;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.term {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.term.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.resize-indicator,
|
||||
.update-indicator {
|
||||
cursor: default;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.resize-indicator {
|
||||
color: #fff;
|
||||
font: 11px Menlo;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
opacity: 0;
|
||||
transition: opacity 150ms ease-in;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.resize-indicator > div {
|
||||
background: rgba(255, 255, 255, .2);
|
||||
padding: 6px 14px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.resize-indicator.showing {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.update-indicator {
|
||||
background: #7ED321;
|
||||
padding: 6px 14px;
|
||||
color: #fff;
|
||||
font: 11px Menlo;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
opacity: 0;
|
||||
transition: opacity 150ms ease-in;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.update-indicator a {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.update-indicator .close {
|
||||
color: #528D11;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.update-indicator.showing {
|
||||
opacity: 1;
|
||||
pointer-events: inherit;
|
||||
}
|
||||
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
fill: currentColor;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
118
app/css/tabs.css
118
app/css/tabs.css
|
|
@ -1,118 +0,0 @@
|
|||
nav {
|
||||
font-size: 12px;
|
||||
font-family: -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans",
|
||||
"Droid Sans", "Helvetica Neue", sans-serif;
|
||||
height: 34px;
|
||||
line-height: 34px;
|
||||
vertical-align: middle;
|
||||
color: #9B9B9B;
|
||||
cursor: default;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.single {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
border-bottom: 1px solid #333;
|
||||
max-height: 34px;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
|
||||
.tabs li:first-child {
|
||||
margin-left: 76px;
|
||||
}
|
||||
|
||||
.tabs li {
|
||||
list-style-type: none;
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tabs li.is_active {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tabs li span {
|
||||
transition: color .2s ease;
|
||||
display: block;
|
||||
border-left: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
}
|
||||
|
||||
.tabs li.is_active span {
|
||||
border-left-color: #333;
|
||||
border-right-color: #333;
|
||||
}
|
||||
|
||||
.tabs li.is_active:last-child span {
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
.tabs li.is_active::before {
|
||||
position: absolute;
|
||||
content: ' ';
|
||||
border-bottom: 1px solid #000;
|
||||
display: block;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
.tabs li:not(.is_active):hover span {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.tabs li.has_activity, .tabs li.has_activity:hover {
|
||||
color: #50E3C2;
|
||||
}
|
||||
|
||||
/* The close button */
|
||||
|
||||
.tabs li i {
|
||||
transition: opacity .2s ease,
|
||||
color .2s ease,
|
||||
transform .25s ease,
|
||||
background-color .1s ease;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 7px;
|
||||
top: 8px;
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 100%;
|
||||
color: #e9e9e9;
|
||||
opacity: 0;
|
||||
transform: scale(.95);
|
||||
}
|
||||
|
||||
.tabs li:hover i {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.tabs li i:hover {
|
||||
background-color: rgba(255,255,255, .13);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tabs li i:active {
|
||||
background-color: rgba(255,255,255, .1);
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
.tabs li i .icon {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 4px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
532
app/hyperterm.js
532
app/hyperterm.js
|
|
@ -1,532 +0,0 @@
|
|||
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 decorate from './plugins';
|
||||
|
||||
// make subcomponents reload siwth plugin changes
|
||||
const Tabs = decorate(Tabs_);
|
||||
const Term = decorate(Term_);
|
||||
|
||||
export default class HyperTerm extends Component {
|
||||
constructor (props) {
|
||||
super();
|
||||
this.state = {
|
||||
cols: null,
|
||||
rows: null,
|
||||
hpadding: 12,
|
||||
vpadding: 12,
|
||||
sessions: [],
|
||||
titles: {},
|
||||
urls: {},
|
||||
active: null,
|
||||
activeMarkers: [],
|
||||
mac: /Mac/.test(navigator.userAgent),
|
||||
resizeIndicatorShowing: false,
|
||||
fontSizeIndicatorShowing: false,
|
||||
dismissedUpdate: false,
|
||||
updateVersion: null,
|
||||
fontSize: props.config.fontSize
|
||||
};
|
||||
|
||||
// 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.quitAndInstall = this.quitAndInstall.bind(this);
|
||||
this.focusActive = this.focusActive.bind(this);
|
||||
this.closeBrowser = this.closeBrowser.bind(this);
|
||||
this.onHeaderMouseDown = this.onHeaderMouseDown.bind(this);
|
||||
this.closeTab = this.closeTab.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);
|
||||
|
||||
this.dismissUpdate = this.dismissUpdate.bind(this);
|
||||
this.onUpdateAvailable = this.onUpdateAvailable.bind(this);
|
||||
|
||||
document.body.style.backgroundColor = props.config.backgroundColor;
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (props.config.fontSize !== this.props.config.fontSize) {
|
||||
this.changeFontSize(props.config.fontSize);
|
||||
}
|
||||
|
||||
if (props.config.backgroundColor !== this.props.config.backgroundColor) {
|
||||
document.body.style.backgroundColor = props.config.backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { backgroundColor, borderColor, css } = this.props.config;
|
||||
return <div onClick={ this.focusActive }>
|
||||
<div style={{ borderColor }} className={ classes('main', { mac: this.state.mac }) }>
|
||||
<header style={{ backgroundColor }} onMouseDown={this.onHeaderMouseDown}>
|
||||
<Tabs
|
||||
active={this.state.active}
|
||||
activeMarkers={this.state.activeMarkers}
|
||||
borderColor={borderColor}
|
||||
data={this.state.sessions.map((uid) => {
|
||||
const title = this.state.titles[uid];
|
||||
return null != title ? title : 'Shell';
|
||||
})}
|
||||
onChange={this.onChange}
|
||||
onClose={this.closeTab}
|
||||
/>
|
||||
</header>
|
||||
|
||||
<div
|
||||
className='terms'
|
||||
style={{ padding: `${this.state.vpadding}px ${this.state.hpadding}px` }}
|
||||
ref='termWrapper'>{
|
||||
this.state.sessions.map((uid, i) => {
|
||||
const active = i === this.state.active;
|
||||
const { config } = this.props;
|
||||
return <div key={`d${uid}`} className={classes('term', { active })} style={{ width: '100%', height: '100%' }}>
|
||||
<Term
|
||||
key={uid}
|
||||
ref={`term-${uid}`}
|
||||
cols={this.state.cols}
|
||||
rows={this.state.rows}
|
||||
customCSS={config.termCSS}
|
||||
fontSize={this.state.fontSize}
|
||||
cursorColor={config.cursorColor}
|
||||
fontFamily={config.fontFamily}
|
||||
backgroundColor={config.backgroundColor}
|
||||
colors={config.colors}
|
||||
url={this.state.urls[uid]}
|
||||
onResize={this.onResize}
|
||||
onTitle={this.setTitle.bind(this, uid)}
|
||||
onData={this.write.bind(this, uid)}
|
||||
onURL={this.onURL.bind(this, uid)}
|
||||
onURLAbort={this.closeBrowser}
|
||||
/>
|
||||
</div>;
|
||||
})
|
||||
}</div>
|
||||
</div>
|
||||
<div className={classes('resize-indicator', { showing: this.state.resizeIndicatorShowing })}>
|
||||
{this.state.fontSizeIndicatorShowing && <div>{ this.state.fontSize }px</div>}
|
||||
<div>{ this.state.cols }x{ this.state.rows }</div>
|
||||
</div>
|
||||
<div className={classes('update-indicator', { showing: null !== this.state.updateVersion && !this.state.dismissedUpdate })}>
|
||||
Version <b>{ this.state.updateVersion }</b> ready.
|
||||
{this.state.updateNote ? ` ${this.state.updateNote}. ` : ' '}
|
||||
<a href='' onClick={this.quitAndInstall}>Restart</a>
|
||||
to apply <span className='close' onClick={this.dismissUpdate}>[x]</span>
|
||||
</div>
|
||||
<style dangerouslySetInnerHTML={{ __html: Array.from(css || '').join('\n') }} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
dismissUpdate () {
|
||||
this.setState({ dismissedUpdate: true });
|
||||
}
|
||||
|
||||
quitAndInstall (ev) {
|
||||
ev.preventDefault();
|
||||
this.rpc.emit('quit and install');
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
closeActiveTab () {
|
||||
this.closeTab(this.state.active);
|
||||
}
|
||||
|
||||
closeTab (id) {
|
||||
if (this.state.sessions.length) {
|
||||
const uid = this.state.sessions[id];
|
||||
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();
|
||||
|
||||
// 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.closeActiveTab.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);
|
||||
|
||||
this.rpc.on('update available', this.onUpdateAvailable);
|
||||
this.rpc.on('preferences', this.editPreferences.bind(this));
|
||||
}
|
||||
|
||||
clearCurrentTerm () {
|
||||
const uid = this.state.sessions[this.state.active];
|
||||
const term = this.refs[`term-${uid}`];
|
||||
term.clear();
|
||||
}
|
||||
|
||||
onUpdateAvailable ({ releaseName, releaseNotes = '' }) {
|
||||
this.setState({
|
||||
updateVersion: releaseName,
|
||||
updateNote: releaseNotes.split(/\n/)[0].trim()
|
||||
});
|
||||
}
|
||||
|
||||
editPreferences () {
|
||||
this.requestTab();
|
||||
const onsession = ({ uid: _uid }) => {
|
||||
const ondata = ({ uid }) => {
|
||||
if (uid !== _uid) return;
|
||||
this.rpc.removeListener('data', ondata);
|
||||
this.rpc.emit('data', { uid, data: '# Attempting to open ~/.hyperterm.js with your $EDITOR\n' +
|
||||
'# If this doesn\'t work, open it manually with your favorite editor!\n' +
|
||||
'$EDITOR ~/.hyperterm.js && exit\n' });
|
||||
};
|
||||
this.rpc.on('data', ondata);
|
||||
};
|
||||
this.rpc.once('new session', onsession);
|
||||
}
|
||||
|
||||
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, { relative = false } = {}) {
|
||||
this.setState({
|
||||
fontSize: relative ? this.state.fontSize + value : value,
|
||||
fontSizeIndicatorShowing: true
|
||||
});
|
||||
|
||||
clearTimeout(this.fontSizeIndicatorTimeout);
|
||||
this.fontSizeIndicatorTimeout = setTimeout(() => {
|
||||
this.setState({ fontSizeIndicatorShowing: false });
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
resetFontSize () {
|
||||
this.changeFontSize(this.props.config.fontSize);
|
||||
}
|
||||
|
||||
increaseFontSize () {
|
||||
this.changeFontSize(1, { relative: true });
|
||||
}
|
||||
|
||||
decreaseFontSize () {
|
||||
this.changeFontSize(-1, { relative: true });
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const ai2 = activeMarkers.indexOf(active);
|
||||
if (~ai2) {
|
||||
activeMarkers.splice(ai2, 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);
|
||||
|
||||
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);
|
||||
clearTimeout(this.fontSizeIndicatorTimeout);
|
||||
if (this.keys) this.keys.reset();
|
||||
delete this.clicks;
|
||||
clearTimeout(this.clickTimer);
|
||||
this.updateChecker.destroy();
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@
|
|||
<meta name="viewport" content="initial-scale=1.0" />
|
||||
<style>
|
||||
body {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
|
|
|||
14
app/index.js
14
app/index.js
|
|
@ -1,14 +0,0 @@
|
|||
import { render } from 'react-dom';
|
||||
import HyperTerm_ from './hyperterm';
|
||||
import React from 'react';
|
||||
import Config from './config';
|
||||
import decorate from './plugins';
|
||||
|
||||
// make the component reload with plugin changes
|
||||
const HyperTerm = decorate(HyperTerm_);
|
||||
|
||||
require('./css/hyperterm.css');
|
||||
require('./css/tabs.css');
|
||||
|
||||
const app = <Config><HyperTerm /></Config>;
|
||||
render(app, document.getElementById('mount'));
|
||||
15
app/lib/actions/config.js
Normal file
15
app/lib/actions/config.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { CONFIG_LOAD, CONFIG_RELOAD } from '../constants/config';
|
||||
|
||||
export function loadConfig (config) {
|
||||
return {
|
||||
type: CONFIG_LOAD,
|
||||
config
|
||||
};
|
||||
}
|
||||
|
||||
export function reloadConfig (config) {
|
||||
return {
|
||||
type: CONFIG_RELOAD,
|
||||
config
|
||||
};
|
||||
}
|
||||
26
app/lib/actions/header.js
Normal file
26
app/lib/actions/header.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { CLOSE_TAB, CHANGE_TAB } from '../constants/tabs';
|
||||
import { userExitSession, setActiveSession } from './sessions';
|
||||
|
||||
export function closeTab (uid) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: CLOSE_TAB,
|
||||
uid,
|
||||
effect () {
|
||||
dispatch(userExitSession(uid));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function changeTab (uid) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: CHANGE_TAB,
|
||||
uid,
|
||||
effect () {
|
||||
dispatch(setActiveSession(uid));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
7
app/lib/actions/index.js
Normal file
7
app/lib/actions/index.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { requestSession } from './sessions';
|
||||
|
||||
export function init () {
|
||||
return (dispatch) => {
|
||||
dispatch(requestSession());
|
||||
};
|
||||
}
|
||||
8
app/lib/actions/notifications.js
Normal file
8
app/lib/actions/notifications.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { NOTIFICATION_DISMISS } from '../constants/notifications';
|
||||
|
||||
export function dismissNotification (id) {
|
||||
return {
|
||||
type: NOTIFICATION_DISMISS,
|
||||
id
|
||||
};
|
||||
}
|
||||
182
app/lib/actions/sessions.js
Normal file
182
app/lib/actions/sessions.js
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import rpc from '../rpc';
|
||||
import getURL from '../utils/url-command';
|
||||
import { keys } from '../utils/object';
|
||||
import {
|
||||
SESSION_ADD,
|
||||
SESSION_RESIZE,
|
||||
SESSION_REQUEST,
|
||||
SESSION_ADD_DATA,
|
||||
SESSION_PTY_DATA,
|
||||
SESSION_PTY_EXIT,
|
||||
SESSION_USER_EXIT,
|
||||
SESSION_USER_EXIT_ACTIVE,
|
||||
SESSION_SET_ACTIVE,
|
||||
SESSION_CLEAR_ACTIVE,
|
||||
SESSION_USER_DATA,
|
||||
SESSION_URL_SET,
|
||||
SESSION_URL_UNSET,
|
||||
SESSION_SET_XTERM_TITLE,
|
||||
SESSION_SET_PROCESS_TITLE
|
||||
} from '../constants/sessions';
|
||||
|
||||
export function addSession (uid) {
|
||||
return {
|
||||
type: SESSION_ADD,
|
||||
uid
|
||||
};
|
||||
}
|
||||
|
||||
export function requestSession (uid) {
|
||||
return (dispatch, getState) => {
|
||||
const { ui } = getState();
|
||||
const cols = ui.cols;
|
||||
const rows = ui.rows;
|
||||
dispatch({
|
||||
type: SESSION_REQUEST,
|
||||
effect: () => {
|
||||
rpc.emit('new', { cols, rows });
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function addSessionData (uid, data) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: SESSION_ADD_DATA,
|
||||
data,
|
||||
effect () {
|
||||
const url = getURL(data);
|
||||
if (null != url) {
|
||||
dispatch({
|
||||
type: SESSION_URL_SET,
|
||||
uid,
|
||||
url
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: SESSION_PTY_DATA,
|
||||
uid,
|
||||
data
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function sessionExit (uid) {
|
||||
return (dispatch, getState) => {
|
||||
return dispatch({
|
||||
type: SESSION_PTY_EXIT,
|
||||
uid,
|
||||
effect () {
|
||||
// we reiterate the same logic as below
|
||||
// for SESSION_USER_EXIT since the exit
|
||||
// could happen pty side or optimistic
|
||||
const sessions = keys(getState().sessions.sessions);
|
||||
if (!sessions.length) {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// we want to distinguish an exit
|
||||
// that's UI initiated vs pty initiated
|
||||
export function userExitSession (uid) {
|
||||
return (dispatch, getState) => {
|
||||
return dispatch({
|
||||
type: SESSION_USER_EXIT,
|
||||
uid,
|
||||
effect () {
|
||||
rpc.emit('exit', { uid });
|
||||
const sessions = keys(getState().sessions.sessions);
|
||||
if (!sessions.length) {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function userExitActiveSession () {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: SESSION_USER_EXIT_ACTIVE,
|
||||
effect () {
|
||||
const uid = getState().sessions.activeUid;
|
||||
dispatch(userExitSession(uid));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function setActiveSession (uid) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const prevUid = state.sessions.activeUid;
|
||||
dispatch({
|
||||
type: SESSION_SET_ACTIVE,
|
||||
uid,
|
||||
effect () {
|
||||
// TODO: this goes away when we are able to poll
|
||||
// for the title ourseleves, instead of relying
|
||||
// on Session and focus/blur to subscribe
|
||||
if (prevUid) rpc.emit('blur', { uid: prevUid });
|
||||
rpc.emit('focus', { uid });
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function clearActiveSession () {
|
||||
return {
|
||||
type: SESSION_CLEAR_ACTIVE
|
||||
};
|
||||
}
|
||||
|
||||
export function setSessionProcessTitle (uid, title) {
|
||||
return {
|
||||
type: SESSION_SET_PROCESS_TITLE,
|
||||
uid,
|
||||
title
|
||||
};
|
||||
}
|
||||
|
||||
export function setSessionXtermTitle (uid, title) {
|
||||
return {
|
||||
type: SESSION_SET_XTERM_TITLE,
|
||||
uid,
|
||||
title
|
||||
};
|
||||
}
|
||||
|
||||
export function resizeSession (uid, cols, rows) {
|
||||
return {
|
||||
type: SESSION_RESIZE,
|
||||
cols,
|
||||
rows,
|
||||
effect () {
|
||||
rpc.emit('resize', { cols, rows });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function sendSessionData (uid, data) {
|
||||
return {
|
||||
type: SESSION_USER_DATA,
|
||||
data,
|
||||
effect () {
|
||||
rpc.emit('data', { uid, data });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function exitSessionBrowser (uid) {
|
||||
return {
|
||||
type: SESSION_URL_UNSET,
|
||||
uid
|
||||
};
|
||||
}
|
||||
143
app/lib/actions/ui.js
Normal file
143
app/lib/actions/ui.js
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { setActiveSession } from './sessions';
|
||||
import { keys } from '../utils/object';
|
||||
import { last } from '../utils/array';
|
||||
import rpc from '../rpc';
|
||||
import {
|
||||
requestSession,
|
||||
sendSessionData
|
||||
} from '../actions/sessions';
|
||||
import {
|
||||
UI_FONT_SIZE_SET,
|
||||
UI_FONT_SIZE_INCR,
|
||||
UI_FONT_SIZE_DECR,
|
||||
UI_FONT_SIZE_RESET,
|
||||
UI_MOVE_LEFT,
|
||||
UI_MOVE_RIGHT,
|
||||
UI_MOVE_TO,
|
||||
UI_SHOW_PREFERENCES
|
||||
} from '../constants/ui';
|
||||
|
||||
export function increaseFontSize () {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: UI_FONT_SIZE_INCR,
|
||||
effect () {
|
||||
const state = getState();
|
||||
const old = state.ui.fontSizeOverride || state.ui.fontSize;
|
||||
const size = old + 1;
|
||||
dispatch({
|
||||
type: UI_FONT_SIZE_SET,
|
||||
size
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function decreaseFontSize () {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: UI_FONT_SIZE_DECR,
|
||||
effect () {
|
||||
const state = getState();
|
||||
const old = state.ui.fontSizeOverride || state.ui.fontSize;
|
||||
const size = old + 1;
|
||||
dispatch({
|
||||
type: UI_FONT_SIZE_SET,
|
||||
size
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function resetFontSize () {
|
||||
return {
|
||||
type: UI_FONT_SIZE_RESET
|
||||
};
|
||||
}
|
||||
|
||||
export function moveLeft () {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: UI_MOVE_LEFT,
|
||||
effect () {
|
||||
const { sessions } = getState();
|
||||
const uid = sessions.activeUid;
|
||||
const sessionUids = keys(sessions.sessions);
|
||||
const index = sessionUids.indexOf(uid);
|
||||
const next = sessionUids[index - 1] || last(sessionUids);
|
||||
if (!next || uid === next) {
|
||||
console.log('ignoring left move action');
|
||||
} else {
|
||||
dispatch(setActiveSession(next));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function moveRight () {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: UI_MOVE_RIGHT,
|
||||
effect () {
|
||||
const { sessions } = getState();
|
||||
const uid = sessions.activeUid;
|
||||
const sessionUids = keys(sessions.sessions);
|
||||
const index = sessionUids.indexOf(uid);
|
||||
const next = sessionUids[index + 1] || sessionUids[0];
|
||||
if (!next || uid === next) {
|
||||
console.log('ignoring right move action');
|
||||
} else {
|
||||
dispatch(setActiveSession(next));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function moveTo (i) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: UI_MOVE_TO,
|
||||
index: i,
|
||||
effect () {
|
||||
const { sessions } = getState();
|
||||
const uid = sessions.activeUid;
|
||||
const sessionUids = keys(sessions.sessions);
|
||||
if (uid === sessionUids[i]) {
|
||||
console.log('ignoring same uid');
|
||||
} else if (null != sessionUids[i]) {
|
||||
dispatch(setActiveSession(sessionUids[i]));
|
||||
} else {
|
||||
console.log('ignoring inexistent index', i);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function showPreferences () {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({
|
||||
type: UI_SHOW_PREFERENCES,
|
||||
effect () {
|
||||
dispatch(requestSession());
|
||||
// TODO: replace this hack with an async action
|
||||
rpc.once('session add', ({ uid }) => {
|
||||
rpc.once('session data', () => {
|
||||
dispatch(sendSessionData(
|
||||
uid,
|
||||
['# Attempting to open ~/.hyperterm.js with your $EDITOR',
|
||||
'# If this doesn\'t work, open it manually with your favorite editor!',
|
||||
'$EDITOR ~/.hyperterm.js && exit',
|
||||
''
|
||||
].join('\n')
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
22
app/lib/actions/updater.js
Normal file
22
app/lib/actions/updater.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import {
|
||||
UPDATE_INSTALL,
|
||||
UPDATE_AVAILABLE
|
||||
} from '../constants/updater';
|
||||
import rpc from '../rpc';
|
||||
|
||||
export function installUpdate () {
|
||||
return {
|
||||
type: UPDATE_INSTALL,
|
||||
effect: () => {
|
||||
rpc.emit('quit and install');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function updateAvailable (version, notes) {
|
||||
return {
|
||||
type: UPDATE_AVAILABLE,
|
||||
version,
|
||||
notes
|
||||
};
|
||||
}
|
||||
70
app/lib/component.js
Normal file
70
app/lib/component.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet, css } from 'aphrodite';
|
||||
import { shouldComponentUpdate } from 'react-addons-pure-render-mixin';
|
||||
|
||||
export default class Component extends React.Component {
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
this.styles_ = this.createStyleSheet();
|
||||
this.cssHelper = this.cssHelper.bind(this);
|
||||
if (!this.shouldComponentUpdate) {
|
||||
this.shouldComponentUpdate = shouldComponentUpdate.bind(this);
|
||||
}
|
||||
}
|
||||
|
||||
createStyleSheet () {
|
||||
if (!this.styles) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const styles = this.styles();
|
||||
|
||||
if ('object' !== typeof styles) {
|
||||
throw new TypeError('Component `styles` returns a non-object');
|
||||
}
|
||||
|
||||
return StyleSheet.create(this.styles());
|
||||
}
|
||||
|
||||
// wrap aphrodite's css helper for two reasons:
|
||||
// - we can give the element an unaltered global classname
|
||||
// that can be used to introduce global css side effects
|
||||
// for example, through the configuration, web inspector
|
||||
// or user agente extensions
|
||||
// - the user doesn't need to keep track of both `css`
|
||||
// and `style`, and we make that whole ordear easier
|
||||
cssHelper (...args) {
|
||||
const classes = args
|
||||
.map((c) => {
|
||||
if (c) {
|
||||
// we compute the global name from the given
|
||||
// css class and we prepend the component name
|
||||
// it's important classes never get mangled by
|
||||
// uglifiers so that we can avoid collisions
|
||||
const component = this.constructor.name
|
||||
.toString()
|
||||
.toLowerCase();
|
||||
const globalName = `${component}_${c}`;
|
||||
return [globalName, css(this.styles_[c])];
|
||||
}
|
||||
})
|
||||
// skip nulls
|
||||
.filter((v) => !!v)
|
||||
// flatten
|
||||
.reduce((a, b) => a.concat(b));
|
||||
return classes.length ? classes.join(' ') : null;
|
||||
}
|
||||
|
||||
render () {
|
||||
// convert static objects from `babel-plugin-transform-jsx`
|
||||
// to `React.Element`.
|
||||
if (!this.template) {
|
||||
throw new TypeError("Component doesn't define `template`");
|
||||
}
|
||||
|
||||
// invoke the template creator passing our css helper
|
||||
return this.template(this.cssHelper);
|
||||
}
|
||||
|
||||
}
|
||||
92
app/lib/components/header.js
Normal file
92
app/lib/components/header.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import React from 'react';
|
||||
import Tabs_ from './tabs';
|
||||
import overrideStyles from '../utils/override-style';
|
||||
import Component from '../component';
|
||||
import { decorate, getTabsProps } from '../utils/plugins';
|
||||
|
||||
const Tabs = decorate(Tabs_, 'Tabs');
|
||||
|
||||
export default class Header extends Component {
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
this.onChangeIntent = this.onChangeIntent.bind(this);
|
||||
this.onHeaderMouseDown = this.onHeaderMouseDown.bind(this);
|
||||
}
|
||||
|
||||
onChangeIntent (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.props.onChangeTab(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 () {
|
||||
delete this.clicks;
|
||||
clearTimeout(this.clickTimer);
|
||||
}
|
||||
|
||||
template (css) {
|
||||
const { isMac, backgroundColor } = this.props;
|
||||
const props = getTabsProps(this.props, {
|
||||
tabs: this.props.tabs,
|
||||
borderColor: this.props.borderColor,
|
||||
onClose: this.props.onCloseTab,
|
||||
onChange: this.onChangeIntent
|
||||
});
|
||||
return <header
|
||||
ref={ overrideStyles({ backgroundColor }) }
|
||||
className={ css('header', isMac && 'headerRounded') }
|
||||
onMouseDown={ this.onHeaderMouseDown }>
|
||||
{ this.props.customChildrenBefore }
|
||||
<Tabs {...props} />
|
||||
{ this.props.customChildren }
|
||||
</header>;
|
||||
}
|
||||
|
||||
styles () {
|
||||
return {
|
||||
header: {
|
||||
position: 'fixed',
|
||||
top: '1px',
|
||||
left: '1px',
|
||||
right: '1px',
|
||||
background: '#000',
|
||||
zIndex: '100'
|
||||
},
|
||||
|
||||
headerRounded: {
|
||||
borderTopLeftRadius: '6px',
|
||||
borderTopRightRadius: '6px'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
116
app/lib/components/notification.js
Normal file
116
app/lib/components/notification.js
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import React from 'react';
|
||||
import Component from '../component';
|
||||
|
||||
export default class Notification extends Component {
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
this.state = {
|
||||
dismissing: false
|
||||
};
|
||||
this.dismiss = this.dismiss.bind(this);
|
||||
this.onElement = this.onElement.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (this.props.dismissAfter) {
|
||||
this.setDismissTimer();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (next) {
|
||||
// if we have a timer going and the notification text
|
||||
// changed we rest the timer
|
||||
if (next.text !== this.props.text) {
|
||||
if (this.props.dismissAfter) {
|
||||
this.resetDismissTimer();
|
||||
}
|
||||
if (this.state.dismissing) {
|
||||
this.setState({ dismissing: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dismiss () {
|
||||
this.setState({ dismissing: true });
|
||||
}
|
||||
|
||||
onElement (el) {
|
||||
if (el) {
|
||||
el.addEventListener('webkitTransitionEnd', () => {
|
||||
if (this.state.dismissing) {
|
||||
this.props.onDismiss();
|
||||
}
|
||||
});
|
||||
// aprhodie !important override :(
|
||||
const { backgroundColor } = this.props;
|
||||
if (backgroundColor) {
|
||||
el.style.setProperty(
|
||||
'background-color',
|
||||
backgroundColor,
|
||||
'important'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDismissTimer (after) {
|
||||
this.dismissTimer = setTimeout(() => {
|
||||
this.dismiss();
|
||||
}, this.props.dismissAfter);
|
||||
}
|
||||
|
||||
resetDismissTimer (after) {
|
||||
clearTimeout(this.dismissTimer);
|
||||
this.setDismissTimer();
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
clearTimeout(this.dismissTimer);
|
||||
}
|
||||
|
||||
template (css) {
|
||||
const { backgroundColor } = this.props;
|
||||
const opacity = this.state.dismissing ? 0 : 1;
|
||||
return <div
|
||||
style={{ opacity, backgroundColor }}
|
||||
ref={ this.onElement }
|
||||
className={ css('indicator') }>
|
||||
{ this.props.customChildrenBefore }
|
||||
{ this.props.children || this.props.text }
|
||||
{
|
||||
this.props.userDismissable
|
||||
? <a
|
||||
className={ css('dismissLink') }
|
||||
onClick={ this.dismiss }>[x]</a>
|
||||
: null
|
||||
}
|
||||
{ this.props.customChildren }
|
||||
</div>;
|
||||
}
|
||||
|
||||
styles () {
|
||||
return {
|
||||
indicator: {
|
||||
display: 'inline-block',
|
||||
cursor: 'default',
|
||||
WebkitUserSelect: 'none',
|
||||
background: 'rgba(255, 255, 255, .2)',
|
||||
padding: '6px 14px',
|
||||
marginLeft: '10px',
|
||||
transition: '150ms opacity ease',
|
||||
color: '#fff',
|
||||
font: '11px Menlo'
|
||||
},
|
||||
|
||||
dismissLink: {
|
||||
cursor: 'pointer',
|
||||
color: '#528D11',
|
||||
':hover': {
|
||||
color: '#2A5100'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
70
app/lib/components/notifications.js
Normal file
70
app/lib/components/notifications.js
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import React from 'react';
|
||||
import Component from '../component';
|
||||
import Notification_ from './notification';
|
||||
import { decorate } from '../utils/plugins';
|
||||
|
||||
const Notification = decorate(Notification_);
|
||||
|
||||
export default class Notifications extends Component {
|
||||
|
||||
template (css) {
|
||||
return <div className={ css('view') }>
|
||||
{ this.props.customChildrenBefore }
|
||||
{
|
||||
this.props.fontShowing &&
|
||||
<Notification
|
||||
key='font'
|
||||
backgroundColor='rgba(255, 255, 255, .2)'
|
||||
text={`${this.props.fontSize}px`}
|
||||
userDismissable={ false }
|
||||
onDismiss={ this.props.onDismissFont }
|
||||
dismissAfter={ 1000 } />
|
||||
}
|
||||
|
||||
{
|
||||
this.props.resizeShowing &&
|
||||
<Notification
|
||||
key='resize'
|
||||
backgroundColor='rgba(255, 255, 255, .2)'
|
||||
text={`${this.props.cols}x${this.props.rows}`}
|
||||
userDismissable={ false }
|
||||
onDismiss={ this.props.onDismissResize }
|
||||
dismissAfter={ 1000 } />
|
||||
}
|
||||
|
||||
{
|
||||
this.props.updateShowing &&
|
||||
<Notification
|
||||
key='update'
|
||||
backgroundColor='#7ED321'
|
||||
text={`Version ${this.props.updateVersion} available`}
|
||||
onDismiss={ this.props.onDismissUpdate }
|
||||
userDismissable={ true }>
|
||||
Version <b>{ this.props.updateVersion}</b> available.
|
||||
{ this.props.updateNote && ` ${this.props.updateNote.trim().replace(/\.$/, '')}.` }
|
||||
{ ' ' }
|
||||
<a style={{
|
||||
cursor: 'pointer',
|
||||
textDecoration: 'underline',
|
||||
fontWeight: 'bold' }}
|
||||
onClick={ this.props.onUpdateInstall }>
|
||||
Restart
|
||||
</a>
|
||||
{ ' ' }
|
||||
</Notification>
|
||||
}
|
||||
{ this.props.customChildren }
|
||||
</div>;
|
||||
}
|
||||
|
||||
styles () {
|
||||
return {
|
||||
view: {
|
||||
position: 'fixed',
|
||||
bottom: '20px',
|
||||
right: '20px'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
175
app/lib/components/tab.js
Normal file
175
app/lib/components/tab.js
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
import React from 'react';
|
||||
import Component from '../component';
|
||||
import overrideStyle from '../utils/override-style';
|
||||
|
||||
export default class Tab extends Component {
|
||||
constructor () {
|
||||
super();
|
||||
this.hover = this.hover.bind(this);
|
||||
this.blur = this.blur.bind(this);
|
||||
this.state = {
|
||||
hovered: false
|
||||
};
|
||||
}
|
||||
|
||||
hover () {
|
||||
this.setState({ hovered: true });
|
||||
}
|
||||
|
||||
blur () {
|
||||
this.setState({ hovered: false });
|
||||
}
|
||||
|
||||
template (css) {
|
||||
const { isActive, isFirst, isLast, borderColor, hasActivity } = this.props;
|
||||
const { hovered } = this.state;
|
||||
|
||||
return <li
|
||||
onMouseEnter={ this.hover }
|
||||
onMouseLeave={ this.blur }
|
||||
onClick={ this.props.onClick }
|
||||
ref={ overrideStyle({ borderColor }) }
|
||||
className={ css(
|
||||
'tab',
|
||||
isFirst && 'first',
|
||||
isActive && 'active',
|
||||
hasActivity && 'hasActivity'
|
||||
) }>
|
||||
{ this.props.customChildrenBefore }
|
||||
<span
|
||||
className={ css(
|
||||
'text',
|
||||
isLast && 'textLast',
|
||||
isActive && 'textActive'
|
||||
) }
|
||||
onClick={ this.props.isActive ? null : this.props.onSelect }>
|
||||
<span className={ css('textInner') }>
|
||||
{ this.props.text }
|
||||
</span>
|
||||
</span>
|
||||
<i
|
||||
className={ css(
|
||||
'icon',
|
||||
hovered && 'iconHovered'
|
||||
) }
|
||||
onClick={ this.props.onClose }>
|
||||
<svg className={ css('shape') }>
|
||||
<use xlinkHref='assets/icons.svg#close'></use>
|
||||
</svg>
|
||||
</i>
|
||||
{ this.props.customChildren }
|
||||
</li>;
|
||||
}
|
||||
|
||||
styles () {
|
||||
return {
|
||||
tab: {
|
||||
color: '#ccc',
|
||||
listStyleType: 'none',
|
||||
flexGrow: 1,
|
||||
position: 'relative',
|
||||
':hover': {
|
||||
color: '#ccc'
|
||||
}
|
||||
},
|
||||
|
||||
first: {
|
||||
marginLeft: '76px'
|
||||
},
|
||||
|
||||
active: {
|
||||
color: '#fff',
|
||||
':before': {
|
||||
position: 'absolute',
|
||||
content: '" "',
|
||||
borderBottom: '1px solid #000',
|
||||
display: 'block',
|
||||
left: '0px',
|
||||
right: '0px',
|
||||
bottom: '-1px'
|
||||
},
|
||||
':hover': {
|
||||
color: '#fff'
|
||||
}
|
||||
},
|
||||
|
||||
hasActivity: {
|
||||
color: '#50E3C2',
|
||||
':hover': {
|
||||
color: '#50E3C2'
|
||||
}
|
||||
},
|
||||
|
||||
text: {
|
||||
transition: 'color .2s ease',
|
||||
height: '34px',
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
borderLeft: '1px solid transparent',
|
||||
borderRight: '1px solid transparent',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
textInner: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
textLast: {
|
||||
borderRightWidth: 0
|
||||
},
|
||||
|
||||
textActive: {
|
||||
borderColor: 'inherit'
|
||||
},
|
||||
|
||||
icon: {
|
||||
transition: `opacity .2s ease, color .2s ease,
|
||||
transform .25s ease, background-color .1s ease`,
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
right: '7px',
|
||||
top: '10px',
|
||||
display: 'inline-block',
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
borderRadius: '100%',
|
||||
color: '#e9e9e9',
|
||||
opacity: 0,
|
||||
transform: 'scale(.95)',
|
||||
|
||||
':hover': {
|
||||
backgroundColor: 'rgba(255,255,255, .13)',
|
||||
color: '#fff'
|
||||
},
|
||||
|
||||
':active': {
|
||||
backgroundColor: 'rgba(255,255,255, .1)',
|
||||
color: '#909090'
|
||||
}
|
||||
},
|
||||
|
||||
iconHovered: {
|
||||
opacity: 1,
|
||||
transform: 'none',
|
||||
pointerEvents: 'all'
|
||||
},
|
||||
|
||||
shape: {
|
||||
position: 'absolute',
|
||||
left: '4px',
|
||||
top: '4px',
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
verticalAlign: 'middle',
|
||||
fill: 'currentColor'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
81
app/lib/components/tabs.js
Normal file
81
app/lib/components/tabs.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import Tab_ from './tab';
|
||||
import React from 'react';
|
||||
import Component from '../component';
|
||||
import overrideStyle from '../utils/override-style';
|
||||
import { decorate, getTabProps } from '../utils/plugins';
|
||||
|
||||
const Tab = decorate(Tab_, 'Tab');
|
||||
|
||||
export default class Tabs extends Component {
|
||||
|
||||
template (css) {
|
||||
const {
|
||||
tabs = [],
|
||||
borderColor,
|
||||
onChange,
|
||||
onClose
|
||||
} = this.props;
|
||||
|
||||
return <nav className={ css('nav') }>
|
||||
{
|
||||
tabs.length
|
||||
? 1 === tabs.length
|
||||
? <div className={ css('title') }>{ tabs[0].title }</div>
|
||||
: <ul
|
||||
ref={overrideStyle({ borderColor })}
|
||||
className={ css('list') }>
|
||||
{
|
||||
tabs.map((tab, i) => {
|
||||
const { uid, title, isActive, hasActivity } = tab;
|
||||
const props = getTabProps(tab, this.props, {
|
||||
text: '' === title ? 'Shell' : title,
|
||||
isFirst: 0 === i,
|
||||
isLast: tabs.length - 1 === i,
|
||||
borderColor: borderColor,
|
||||
isActive,
|
||||
hasActivity,
|
||||
onSelect: onChange.bind(null, uid),
|
||||
onClose: onClose.bind(null, uid)
|
||||
});
|
||||
return <Tab key={`tab-${uid}`} {...props} />;
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
: null
|
||||
}
|
||||
{ this.props.customChildren }
|
||||
</nav>;
|
||||
}
|
||||
|
||||
styles () {
|
||||
return {
|
||||
nav: {
|
||||
fontSize: '12px',
|
||||
fontFamily: `-apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans",
|
||||
"Droid Sans", "Helvetica Neue", sans-serif`,
|
||||
height: '34px',
|
||||
lineHeight: '34px',
|
||||
verticalAlign: 'middle',
|
||||
color: '#9B9B9B',
|
||||
cursor: 'default',
|
||||
WebkitUserSelect: 'none',
|
||||
WebkitAppRegion: 'drag'
|
||||
},
|
||||
|
||||
title: {
|
||||
textAlign: 'center',
|
||||
color: '#fff'
|
||||
},
|
||||
|
||||
list: {
|
||||
borderBottom: '1px solid #333',
|
||||
maxHeight: '34px',
|
||||
display: 'flex',
|
||||
flexFlow: 'row'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,17 +1,26 @@
|
|||
/*global URL:false,Blob:false*/
|
||||
import React, { Component } from 'react';
|
||||
import { hterm, lib as htermLib } from './hterm';
|
||||
import * as regex from './regex';
|
||||
/* global Blob,URL */
|
||||
import React from 'react';
|
||||
import hterm from '../hterm';
|
||||
import Component from '../component';
|
||||
|
||||
export default class Term extends Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = { scrollable: false };
|
||||
this.onWheel = this.onWheel.bind(this);
|
||||
this.onScrollEnter = this.onScrollEnter.bind(this);
|
||||
this.onScrollLeave = this.onScrollLeave.bind(this);
|
||||
props.ref_(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { props } = this;
|
||||
this.term = new hterm.Terminal();
|
||||
|
||||
// the first term that's created has unknown size
|
||||
// subsequent new tabs have size
|
||||
if (props.cols) {
|
||||
if (props.cols && props.rows) {
|
||||
this.term.realizeSize_(props.cols, props.rows);
|
||||
}
|
||||
|
||||
|
|
@ -19,22 +28,60 @@ export default class Term extends Component {
|
|||
this.term.prefs_.set('font-size', props.fontSize);
|
||||
this.term.prefs_.set('cursor-color', props.cursorColor);
|
||||
this.term.prefs_.set('enable-clipboard-notice', false);
|
||||
this.term.prefs_.set('foreground-color', props.foregroundColor);
|
||||
this.term.prefs_.set('background-color', props.backgroundColor);
|
||||
this.term.prefs_.set('color-palette-overrides', props.colors);
|
||||
|
||||
this.term.prefs_.set('user-css', this.getStylesheet(props.customCSS));
|
||||
this.term.prefs_.set('scrollbar-visible', false);
|
||||
|
||||
this.term.onTerminalReady = () => {
|
||||
const io = this.term.io.push();
|
||||
io.onVTKeystroke = io.sendString = (str) => {
|
||||
props.onData(str);
|
||||
};
|
||||
io.onVTKeystroke = io.sendString = props.onData;
|
||||
io.onTerminalResize = (cols, rows) => {
|
||||
props.onResize({ cols, rows });
|
||||
if (cols !== this.props.cols && rows !== this.props.rows) {
|
||||
props.onResize(cols, rows);
|
||||
}
|
||||
};
|
||||
};
|
||||
this.term.decorate(this.refs.term);
|
||||
this.term.installKeyboard();
|
||||
if (this.props.onTerminal) this.props.onTerminal(this.term);
|
||||
|
||||
const iframeWindow = this.getTermDocument().defaultView;
|
||||
iframeWindow.addEventListener('wheel', this.onWheel);
|
||||
}
|
||||
|
||||
onWheel () {
|
||||
this.term.prefs_.set('scrollbar-visible', true);
|
||||
clearTimeout(this.scrollbarsHideTimer);
|
||||
if (!this.scrollMouseEnter) {
|
||||
this.scrollbarsHideTimer = setTimeout(() => {
|
||||
this.term.prefs_.set('scrollbar-visible', false);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
onScrollEnter () {
|
||||
clearTimeout(this.scrollbarsHideTimer);
|
||||
this.term.prefs_.set('scrollbar-visible', true);
|
||||
this.scrollMouseEnter = true;
|
||||
}
|
||||
|
||||
onScrollLeave () {
|
||||
this.term.prefs_.set('scrollbar-visible', false);
|
||||
this.scrollMouseEnter = false;
|
||||
}
|
||||
|
||||
write (data) {
|
||||
this.term.io.print(data);
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.term.focus();
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.term.clearPreserveCursorRow();
|
||||
}
|
||||
|
||||
getTermDocument () {
|
||||
|
|
@ -46,38 +93,12 @@ export default class Term extends Component {
|
|||
.cursor-node[focus="false"] {
|
||||
border-width: 1px !important;
|
||||
}
|
||||
${Array.from(css || '').join('\n')}
|
||||
${css}
|
||||
`]);
|
||||
return URL.createObjectURL(blob, { type: 'text/css' });
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (this.props.fontSize !== nextProps.fontSize) {
|
||||
this.term.prefs_.set('font-size', nextProps.fontSize);
|
||||
}
|
||||
|
||||
if (this.props.backgroundColor !== nextProps.backgroundColor) {
|
||||
this.term.prefs_.set('background-color', nextProps.backgroundColor);
|
||||
}
|
||||
|
||||
if (this.props.fontFamily !== nextProps.fontFamily) {
|
||||
this.term.prefs_.set('font-family', nextProps.fontFamily);
|
||||
}
|
||||
|
||||
if (this.props.cursorColor !== nextProps.cursorColor) {
|
||||
this.term.prefs_.set('cursor-color', nextProps.cursorColor);
|
||||
}
|
||||
|
||||
if (this.props.colors.toString() !== nextProps.colors.toString()) {
|
||||
this.term.prefs_.set('color-palette-overrides', nextProps.colors);
|
||||
}
|
||||
|
||||
if (this.props.customCSS.toString() !== nextProps.customCSS.toString()) {
|
||||
this.term.prefs_.set('user-css', this.getStylesheet(nextProps.customCSS));
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate (nextProps) {
|
||||
if (this.props.url !== nextProps.url) {
|
||||
// when the url prop changes, we make sure
|
||||
// the terminal starts or stops ignoring
|
||||
|
|
@ -93,100 +114,52 @@ export default class Term extends Component {
|
|||
} else {
|
||||
this.term.io.pop();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
write (data) {
|
||||
let match = data.match(regex.bash);
|
||||
let url;
|
||||
|
||||
if (match) {
|
||||
url = match[5];
|
||||
} else {
|
||||
match = data.match(regex.zsh);
|
||||
if (match) {
|
||||
url = match[7];
|
||||
} else {
|
||||
match = data.match(regex.fish);
|
||||
if (match) {
|
||||
url = match[4];
|
||||
}
|
||||
}
|
||||
if (!this.props.cleared && nextProps.cleared) {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
if (url) {
|
||||
// extract the domain portion from the url
|
||||
const domain = url.split('/')[0];
|
||||
if (regex.domain.test(domain)) {
|
||||
this.props.onURL(toURL(url));
|
||||
return;
|
||||
}
|
||||
if (this.props.fontSize !== nextProps.fontSize) {
|
||||
this.term.prefs_.set('font-size', nextProps.fontSize);
|
||||
}
|
||||
|
||||
this.term.io.print(data);
|
||||
}
|
||||
if (this.props.foregroundColor !== nextProps.foregroundColor) {
|
||||
this.term.prefs_.set('foreground-color', nextProps.foregroundColor);
|
||||
}
|
||||
|
||||
focus () {
|
||||
this.term.focus();
|
||||
}
|
||||
if (this.props.backgroundColor !== nextProps.backgroundColor) {
|
||||
this.term.prefs_.set('background-color', nextProps.backgroundColor);
|
||||
}
|
||||
|
||||
clear () {
|
||||
const { term } = this;
|
||||
if (this.props.fontFamily !== nextProps.fontFamily) {
|
||||
this.term.prefs_.set('font-family', nextProps.fontFamily);
|
||||
}
|
||||
|
||||
// we re-implement `wipeContents` to preserve the line
|
||||
// and cursor position that the client is in.
|
||||
// otherwise the user ends up with a completely clear
|
||||
// screen which is really strange
|
||||
term.scrollbackRows_.length = 0;
|
||||
term.scrollPort_.resetCache();
|
||||
if (this.props.cursorColor !== nextProps.cursorColor) {
|
||||
this.term.prefs_.set('cursor-color', nextProps.cursorColor);
|
||||
}
|
||||
|
||||
[term.primaryScreen_, term.alternateScreen_].forEach(function (screen) {
|
||||
const bottom = screen.getHeight();
|
||||
if (bottom > 0) {
|
||||
term.renumberRows_(0, bottom);
|
||||
if (this.props.colors !== nextProps.colors) {
|
||||
this.term.prefs_.set('color-palette-overrides', nextProps.colors);
|
||||
}
|
||||
|
||||
const x = screen.cursorPosition.column;
|
||||
const y = screen.cursorPosition.row;
|
||||
|
||||
if (x === 0) {
|
||||
// Empty screen, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// here we move the row that the user was focused on
|
||||
// to the top of the screen
|
||||
term.moveRows_(y, 1, 0);
|
||||
|
||||
for (let i = 1; i < bottom; i++) {
|
||||
screen.setCursorPosition(i, 0);
|
||||
screen.clearCursorRow();
|
||||
}
|
||||
|
||||
// we restore the cursor position
|
||||
screen.setCursorPosition(0, x);
|
||||
}
|
||||
});
|
||||
|
||||
term.syncCursorPosition_();
|
||||
term.scrollPort_.invalidate();
|
||||
|
||||
// this will avoid a bug where the `wipeContents`
|
||||
// hterm API doens't send the scroll to the top
|
||||
this.term.scrollPort_.redraw_();
|
||||
if (this.props.customCSS !== nextProps.customCSS) {
|
||||
this.term.prefs_.set('user-css', this.getStylesheet(nextProps.customCSS));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
// there's no need to manually destroy
|
||||
// as all the events are attached to the iframe
|
||||
// which gets removed
|
||||
const iframeWindow = this.getTermDocument().defaultView;
|
||||
iframeWindow.addEventListener('wheel', this.onWheel);
|
||||
clearTimeout(this.scrollbarsHideTimer);
|
||||
this.props.ref_(null);
|
||||
}
|
||||
|
||||
render () {
|
||||
return <div style={{ width: '100%', height: '100%' }}>
|
||||
<div ref='term' style={{ position: 'relative', width: '100%', height: '100%' }} />
|
||||
template (css) {
|
||||
return <div className={ css('fit') }>
|
||||
{ this.props.customChildrenBefore }
|
||||
<div ref='term' className={ css('fit', 'term') } />
|
||||
{ this.props.url
|
||||
? <webview
|
||||
src={this.props.url}
|
||||
|
|
@ -201,19 +174,33 @@ export default class Term extends Component {
|
|||
}}></webview>
|
||||
: null
|
||||
}
|
||||
<div
|
||||
className={ css('scrollbarShim') }
|
||||
onMouseEnter={ this.onScrollEnter }
|
||||
onMouseLeave={ this.onScrollLeave } />
|
||||
{ this.props.customChildren }
|
||||
</div>;
|
||||
}
|
||||
|
||||
}
|
||||
styles () {
|
||||
return {
|
||||
fit: {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
|
||||
function toURL (domain) {
|
||||
if (/^https?:\/\//.test(domain)) {
|
||||
return domain;
|
||||
term: {
|
||||
position: 'relative'
|
||||
},
|
||||
|
||||
scrollbarShim: {
|
||||
position: 'fixed',
|
||||
right: 0,
|
||||
width: '50px',
|
||||
top: 0,
|
||||
bottom: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if ('//' === domain.substr(0, 2)) {
|
||||
return domain;
|
||||
}
|
||||
|
||||
return 'http://' + domain;
|
||||
}
|
||||
182
app/lib/components/terms.js
Normal file
182
app/lib/components/terms.js
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import React from 'react';
|
||||
import Term_ from './term';
|
||||
import Component from '../component';
|
||||
import { shouldComponentUpdate } from 'react-addons-pure-render-mixin';
|
||||
import { last } from '../utils/array';
|
||||
import { decorate, getTermProps } from '../utils/plugins';
|
||||
|
||||
const Term = decorate(Term_, 'Term');
|
||||
|
||||
export default class Terms extends Component {
|
||||
|
||||
constructor (props, context) {
|
||||
super(props, context);
|
||||
this.terms = {};
|
||||
this.bound = new WeakMap();
|
||||
this.onRef = this.onRef.bind(this);
|
||||
props.ref_(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps (next) {
|
||||
const { write } = next;
|
||||
if (write && this.props.write !== write) {
|
||||
this.getTermByUid(write.uid).write(write.data);
|
||||
}
|
||||
|
||||
// if we just rendered, we consider the first tab active
|
||||
// why is this decided here? because what session becomes
|
||||
// active is a *view* and *layout* concern. for example,
|
||||
// if a split is closed (and we had split), the next active
|
||||
// session after the close would be the one next to it
|
||||
// *in the view*, not necessarily the model datastructure
|
||||
if (next.sessions && next.sessions.length) {
|
||||
if (!this.props.activeSession && next.sessions.length) {
|
||||
this.props.onActive(next.sessions[0].uid);
|
||||
} else if (this.props.sessions.length !== next.sessions.length) {
|
||||
if (next.sessions.length > this.props.sessions.length) {
|
||||
// if we are adding, we focused on the new one
|
||||
this.props.onActive(last(next.sessions).uid);
|
||||
return;
|
||||
}
|
||||
|
||||
const newUids = uids(next.sessions);
|
||||
const curActive = this.props.activeSession;
|
||||
|
||||
// if we closed an item that wasn't focused, nothing changes
|
||||
if (~newUids.indexOf(curActive)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldIndex = uids(this.props.sessions).indexOf(curActive);
|
||||
if (newUids[oldIndex]) {
|
||||
this.props.onActive(newUids[oldIndex]);
|
||||
} else {
|
||||
this.props.onActive(last(next.sessions).uid);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.props.onActive(null);
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate (nextProps, nextState) {
|
||||
let nextProps_ = nextProps;
|
||||
if (this.props.write || nextProps.write) {
|
||||
// ignore `write` when performing the comparison
|
||||
nextProps_ = Object.assign({}, nextProps, { write: null });
|
||||
}
|
||||
return shouldComponentUpdate.call(this, nextProps_);
|
||||
}
|
||||
|
||||
onRef (uid, term) {
|
||||
if (term) {
|
||||
this.terms[uid] = term;
|
||||
} else {
|
||||
delete this.terms[uid];
|
||||
}
|
||||
}
|
||||
|
||||
getTermByUid (uid) {
|
||||
return this.terms[uid];
|
||||
}
|
||||
|
||||
getActiveTerm () {
|
||||
return this.getTermByUid(this.props.activeSession);
|
||||
}
|
||||
|
||||
bind (fn, thisObj, uid) {
|
||||
if (!this.bound.has(fn)) {
|
||||
this.bound.set(fn, {});
|
||||
}
|
||||
const map = this.bound.get(fn);
|
||||
if (!map[uid]) {
|
||||
map[uid] = fn.bind(thisObj, uid);
|
||||
}
|
||||
return map[uid];
|
||||
}
|
||||
|
||||
getTermProps (uid) {
|
||||
return getTermProps(uid, this.props);
|
||||
}
|
||||
|
||||
onTerminal (uid, term) {
|
||||
this.terms[uid] = term;
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.props.ref_(null);
|
||||
}
|
||||
|
||||
template (css) {
|
||||
return <div
|
||||
style={{ padding: this.props.padding }}
|
||||
className={ css('terms') }>
|
||||
{ this.props.customChildrenBefore }
|
||||
{
|
||||
this.props.sessions.map((session) => {
|
||||
const uid = session.uid;
|
||||
const isActive = uid === this.props.activeSession;
|
||||
const props = getTermProps(uid, this.props, {
|
||||
cols: this.props.cols,
|
||||
rows: this.props.rows,
|
||||
customCSS: this.props.customCSS,
|
||||
fontSize: this.props.fontSize,
|
||||
cursorColor: this.props.cursorColor,
|
||||
fontFamily: this.props.fontFamily,
|
||||
foregroundColor: this.props.foregroundColor,
|
||||
backgroundColor: this.props.backgroundColor,
|
||||
colors: this.props.colors,
|
||||
url: session.url,
|
||||
cleared: session.cleared,
|
||||
onResize: this.bind(this.props.onResize, null, uid),
|
||||
onTitle: this.bind(this.props.onTitle, null, uid),
|
||||
onData: this.bind(this.props.onData, null, uid),
|
||||
onURLAbort: this.bind(this.props.onURLAbort, null, uid)
|
||||
});
|
||||
return <div
|
||||
key={`d${uid}`}
|
||||
className={css('term', isActive && 'termActive')}>
|
||||
<Term
|
||||
key={uid}
|
||||
ref_={this.bind(this.onRef, this, uid)}
|
||||
{...props} />
|
||||
</div>;
|
||||
})
|
||||
}
|
||||
{ this.props.customChildren }
|
||||
</div>;
|
||||
}
|
||||
|
||||
styles () {
|
||||
return {
|
||||
terms: {
|
||||
position: 'absolute',
|
||||
marginTop: '34px',
|
||||
top: 0,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
color: '#fff'
|
||||
},
|
||||
|
||||
term: {
|
||||
display: 'none',
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
|
||||
termActive: {
|
||||
display: 'block'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// little memoized helper to compute a map of uids
|
||||
function uids (sessions) {
|
||||
if (!sessions._uids) {
|
||||
sessions._uids = sessions.map((s) => s.uid);
|
||||
}
|
||||
return sessions._uids;
|
||||
}
|
||||
2
app/lib/constants/config.js
Normal file
2
app/lib/constants/config.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export const CONFIG_LOAD = 'CONFIG_LOAD';
|
||||
export const CONFIG_RELOAD = 'CONFIG_RELOAD';
|
||||
1
app/lib/constants/index.js
Normal file
1
app/lib/constants/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const INIT = 'INIT';
|
||||
1
app/lib/constants/notifications.js
Normal file
1
app/lib/constants/notifications.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const NOTIFICATION_DISMISS = 'NOTIFICATION_DISMISS';
|
||||
15
app/lib/constants/sessions.js
Normal file
15
app/lib/constants/sessions.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export const SESSION_ADD = 'SESSION_ADD';
|
||||
export const SESSION_RESIZE = 'SESSION_RESIZE';
|
||||
export const SESSION_REQUEST = 'SESSION_REQUEST';
|
||||
export const SESSION_ADD_DATA = 'SESSION_ADD_DATA';
|
||||
export const SESSION_PTY_DATA = 'SESSION_PTY_DATA';
|
||||
export const SESSION_PTY_EXIT = 'SESSION_PTY_EXIT';
|
||||
export const SESSION_USER_EXIT = 'SESSION_USER_EXIT';
|
||||
export const SESSION_USER_EXIT_ACTIVE = 'SESSION_USER_EXIT_ACTIVE';
|
||||
export const SESSION_SET_ACTIVE = 'SESSION_SET_ACTIVE';
|
||||
export const SESSION_CLEAR_ACTIVE = 'SESSION_CLEAR_ACTIVE';
|
||||
export const SESSION_USER_DATA = 'SESSION_USER_DATA';
|
||||
export const SESSION_URL_SET = 'SESSION_URL_SET';
|
||||
export const SESSION_URL_UNSET = 'SESSION_URL_UNSET';
|
||||
export const SESSION_SET_XTERM_TITLE = 'SESSION_SET_XTERM_TITLE';
|
||||
export const SESSION_SET_PROCESS_TITLE = 'SESSION_SET_PROCESS_TITLE';
|
||||
2
app/lib/constants/tabs.js
Normal file
2
app/lib/constants/tabs.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export const CLOSE_TAB = 'CLOSE_TAB';
|
||||
export const CHANGE_TAB = 'CHANGE_TAB';
|
||||
8
app/lib/constants/ui.js
Normal file
8
app/lib/constants/ui.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export const UI_FONT_SIZE_SET = 'UI_FONT_SIZE_SET';
|
||||
export const UI_FONT_SIZE_INCR = 'UI_FONT_SIZE_INCR';
|
||||
export const UI_FONT_SIZE_DECR = 'UI_FONT_SIZE_DECR';
|
||||
export const UI_FONT_SIZE_RESET = 'UI_FONT_SIZE_RESET';
|
||||
export const UI_MOVE_LEFT = 'UI_MOVE_LEFT';
|
||||
export const UI_MOVE_RIGHT = 'UI_MOVE_RIGHT';
|
||||
export const UI_MOVE_TO = 'UI_MOVE_TO';
|
||||
export const UI_SHOW_PREFERENCES = 'UI_SHOW_PREFERENCES';
|
||||
2
app/lib/constants/updater.js
Normal file
2
app/lib/constants/updater.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export const UPDATE_INSTALL = 'UPDATE_INSTALL';
|
||||
export const UPDATE_AVAILABLE = 'UPDATE_AVAILABLE';
|
||||
48
app/lib/containers/header.js
Normal file
48
app/lib/containers/header.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import Header from '../components/header';
|
||||
import { closeTab, changeTab } from '../actions/header';
|
||||
import { values } from '../utils/object';
|
||||
import { createSelector } from 'reselect';
|
||||
import { connect } from '../utils/plugins';
|
||||
|
||||
const isMac = /Mac/.test(navigator.userAgent);
|
||||
|
||||
const getSessions = (sessions) => sessions.sessions;
|
||||
const getActiveUid = (sessions) => sessions.activeUid;
|
||||
const getActivityMarkers = (sessions, ui) => ui.activityMarkers;
|
||||
const getTabs = createSelector(
|
||||
[getSessions, getActiveUid, getActivityMarkers],
|
||||
(sessions, activeUid, activityMarkers) => values(sessions).map((s) => {
|
||||
return {
|
||||
uid: s.uid,
|
||||
title: s.title,
|
||||
isActive: s.uid === activeUid,
|
||||
hasActivity: activityMarkers[s.uid]
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const HeaderContainer = connect(
|
||||
(state) => {
|
||||
return {
|
||||
// active is an index
|
||||
isMac,
|
||||
tabs: getTabs(state.sessions, state.ui),
|
||||
activeMarkers: state.ui.activityMarkers,
|
||||
borderColor: state.ui.borderColor,
|
||||
backgroundColor: state.ui.backgroundColor
|
||||
};
|
||||
},
|
||||
(dispatch) => {
|
||||
return {
|
||||
onCloseTab: (i) => {
|
||||
dispatch(closeTab(i));
|
||||
},
|
||||
|
||||
onChangeTab: (i) => {
|
||||
dispatch(changeTab(i));
|
||||
}
|
||||
};
|
||||
}
|
||||
)(Header, 'Header');
|
||||
|
||||
export default HeaderContainer;
|
||||
137
app/lib/containers/hyperterm.js
Normal file
137
app/lib/containers/hyperterm.js
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import React from 'react';
|
||||
import HeaderContainer from './header';
|
||||
import TermsContainer from './terms';
|
||||
import NotificationsContainer from './notifications';
|
||||
import Component from '../component';
|
||||
import Mousetrap from 'mousetrap';
|
||||
import overrideStyles from '../utils/override-style';
|
||||
import { moveTo, moveLeft, moveRight } from '../actions/ui';
|
||||
import { connect } from '../utils/plugins';
|
||||
|
||||
const isMac = /Mac/.test(navigator.userAgent);
|
||||
|
||||
class HyperTerm extends Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.focusActive = this.focusActive.bind(this);
|
||||
document.body.style.backgroundColor = props.backgroundColor;
|
||||
this.onTermsRef = this.onTermsRef.bind(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps (next) {
|
||||
if (this.props.backgroundColor !== next.backgroundColor) {
|
||||
document.body.style.backgroundColor = next.backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
focusActive () {
|
||||
const term = this.terms.getActiveTerm();
|
||||
if (term) term.focus();
|
||||
}
|
||||
|
||||
attachKeyListeners () {
|
||||
const { moveTo, moveLeft, moveRight } = this.props;
|
||||
const term = this.terms.getActiveTerm();
|
||||
if (!term) return;
|
||||
const document = term.getTermDocument();
|
||||
const keys = new Mousetrap(document);
|
||||
keys.bind('command+1', moveTo.bind(this, 0));
|
||||
keys.bind('command+2', moveTo.bind(this, 1));
|
||||
keys.bind('command+3', moveTo.bind(this, 2));
|
||||
keys.bind('command+4', moveTo.bind(this, 3));
|
||||
keys.bind('command+5', moveTo.bind(this, 4));
|
||||
keys.bind('command+6', moveTo.bind(this, 5));
|
||||
keys.bind('command+7', moveTo.bind(this, 6));
|
||||
keys.bind('command+8', moveTo.bind(this, 7));
|
||||
keys.bind('command+9', moveTo.bind(this, 8));
|
||||
keys.bind('command+shift+left', moveLeft);
|
||||
keys.bind('command+shift+right', moveRight);
|
||||
keys.bind('command+shift+[', moveLeft);
|
||||
keys.bind('command+shift+]', moveRight);
|
||||
keys.bind('command+alt+left', moveLeft);
|
||||
keys.bind('command+alt+right', moveRight);
|
||||
this.keys = keys;
|
||||
}
|
||||
|
||||
onTermsRef (terms) {
|
||||
this.terms = terms;
|
||||
}
|
||||
|
||||
componentDidUpdate (prev) {
|
||||
if (prev.activeSession !== this.props.activeSession) {
|
||||
if (this.keys) this.keys.reset();
|
||||
this.focusActive();
|
||||
this.attachKeyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.keys) this.keys.reset();
|
||||
document.body.style.backgroundColor = 'inherit';
|
||||
}
|
||||
|
||||
template (css) {
|
||||
const { isMac, customCSS, borderColor } = this.props;
|
||||
return <div onClick={ this.focusActive }>
|
||||
<div
|
||||
ref={ overrideStyles({ borderColor }) }
|
||||
className={ css('main', isMac && 'mainRounded') }>
|
||||
<HeaderContainer />
|
||||
<TermsContainer ref_={this.onTermsRef} />
|
||||
</div>
|
||||
|
||||
<NotificationsContainer />
|
||||
<style dangerouslySetInnerHTML={{ __html: customCSS }} />
|
||||
{ this.props.customChildren }
|
||||
</div>;
|
||||
}
|
||||
|
||||
styles () {
|
||||
return {
|
||||
main: {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
// can be overridden by inline style above
|
||||
border: '1px solid #333'
|
||||
},
|
||||
|
||||
mainRounded: {
|
||||
borderRadius: '5px'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const HyperTermContainer = connect(
|
||||
(state) => {
|
||||
return {
|
||||
isMac,
|
||||
customCSS: state.ui.css,
|
||||
borderColor: state.ui.borderColor,
|
||||
activeSession: state.sessions.activeUid,
|
||||
backgroundColor: state.ui.backgroundColor
|
||||
};
|
||||
},
|
||||
(dispatch) => {
|
||||
return {
|
||||
moveTo: (i) => {
|
||||
dispatch(moveTo(i));
|
||||
},
|
||||
|
||||
moveLeft: () => {
|
||||
dispatch(moveLeft());
|
||||
},
|
||||
|
||||
moveRight: () => {
|
||||
dispatch(moveRight());
|
||||
}
|
||||
};
|
||||
},
|
||||
null,
|
||||
{ withRef: true }
|
||||
)(HyperTerm, 'HyperTerm');
|
||||
|
||||
export default HyperTermContainer;
|
||||
61
app/lib/containers/notifications.js
Normal file
61
app/lib/containers/notifications.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import Notifications from '../components/notifications';
|
||||
import { installUpdate } from '../actions/updater';
|
||||
import { connect } from '../utils/plugins';
|
||||
import { dismissNotification } from '../actions/notifications';
|
||||
|
||||
const NotificationsContainer = connect(
|
||||
(state) => {
|
||||
const { ui } = state;
|
||||
const { notifications } = ui;
|
||||
const state_ = {};
|
||||
|
||||
if (notifications.font) {
|
||||
const fontSize = ui.fontSize;
|
||||
|
||||
Object.assign(state_, {
|
||||
fontShowing: true,
|
||||
fontSize,
|
||||
fontText: `${fontSize}px`
|
||||
});
|
||||
}
|
||||
|
||||
if (notifications.resize) {
|
||||
const cols = ui.cols;
|
||||
const rows = ui.rows;
|
||||
|
||||
Object.assign(state_, {
|
||||
resizeShowing: true,
|
||||
cols,
|
||||
rows
|
||||
});
|
||||
}
|
||||
|
||||
if (notifications.updates) {
|
||||
Object.assign(state_, {
|
||||
updateShowing: true,
|
||||
updateVersion: ui.updateVersion,
|
||||
updateNote: ui.updateNotes.split('\n')[0]
|
||||
});
|
||||
}
|
||||
|
||||
return state_;
|
||||
},
|
||||
(dispatch) => {
|
||||
return {
|
||||
onDismissFont: () => {
|
||||
dispatch(dismissNotification('font'));
|
||||
},
|
||||
onDismissResize: () => {
|
||||
dispatch(dismissNotification('resize'));
|
||||
},
|
||||
onDismissUpdate: () => {
|
||||
dispatch(dismissNotification('updates'));
|
||||
},
|
||||
onUpdateInstall: () => {
|
||||
dispatch(installUpdate());
|
||||
}
|
||||
};
|
||||
}
|
||||
)(Notifications, 'Notifications');
|
||||
|
||||
export default NotificationsContainer;
|
||||
61
app/lib/containers/terms.js
Normal file
61
app/lib/containers/terms.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import Terms from '../components/terms';
|
||||
import { values } from '../utils/object';
|
||||
import { connect } from '../utils/plugins';
|
||||
import {
|
||||
resizeSession,
|
||||
sendSessionData,
|
||||
exitSessionBrowser,
|
||||
setSessionXtermTitle,
|
||||
setActiveSession
|
||||
} from '../actions/sessions';
|
||||
|
||||
const TermsContainer = connect(
|
||||
(state) => {
|
||||
const sessions = state.sessions.sessions;
|
||||
return {
|
||||
cols: state.ui.cols,
|
||||
rows: state.ui.rows,
|
||||
sessions: values(sessions),
|
||||
activeSession: state.sessions.activeUid,
|
||||
customCSS: state.ui.termCSS,
|
||||
write: state.sessions.write,
|
||||
fontSize: state.ui.fontSizeOverride
|
||||
? state.ui.fontSizeOverride
|
||||
: state.ui.fontSize,
|
||||
fontFamily: state.ui.fontFamily,
|
||||
padding: state.ui.padding,
|
||||
cursorColor: state.ui.cursorColor,
|
||||
borderColor: state.ui.borderColor,
|
||||
colors: state.ui.colors,
|
||||
foregroundColor: state.ui.foregroundColor,
|
||||
backgroundColor: state.ui.backgroundColor
|
||||
};
|
||||
},
|
||||
(dispatch) => {
|
||||
return {
|
||||
onData (uid, data) {
|
||||
dispatch(sendSessionData(uid, data));
|
||||
},
|
||||
|
||||
onTitle (uid, title) {
|
||||
dispatch(setSessionXtermTitle(uid, title));
|
||||
},
|
||||
|
||||
onResize (uid, cols, rows) {
|
||||
dispatch(resizeSession(uid, cols, rows));
|
||||
},
|
||||
|
||||
onURLAbort (uid) {
|
||||
dispatch(exitSessionBrowser(uid));
|
||||
},
|
||||
|
||||
onActive (uid) {
|
||||
dispatch(setActiveSession(uid));
|
||||
}
|
||||
};
|
||||
},
|
||||
null,
|
||||
{ withRef: true }
|
||||
)(Terms, 'Terms');
|
||||
|
||||
export default TermsContainer;
|
||||
|
|
@ -43,5 +43,48 @@ hterm.Keyboard.prototype.onKeyPress_ = function (e) {
|
|||
return oldKeyPress.call(this, e);
|
||||
};
|
||||
|
||||
export { hterm };
|
||||
// we re-implement `wipeContents` to preserve the line
|
||||
// and cursor position that the client is in.
|
||||
// otherwise the user ends up with a completely clear
|
||||
// screen which is really strange
|
||||
hterm.Terminal.prototype.clearPreserveCursorRow = function () {
|
||||
this.scrollbackRows_.length = 0;
|
||||
this.scrollPort_.resetCache();
|
||||
|
||||
[this.primaryScreen_, this.alternateScreen_].forEach((screen) => {
|
||||
const bottom = screen.getHeight();
|
||||
if (bottom > 0) {
|
||||
this.renumberRows_(0, bottom);
|
||||
|
||||
const x = screen.cursorPosition.column;
|
||||
const y = screen.cursorPosition.row;
|
||||
|
||||
if (x === 0) {
|
||||
// Empty screen, nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
// here we move the row that the user was focused on
|
||||
// to the top of the screen
|
||||
this.moveRows_(y, 1, 0);
|
||||
|
||||
for (let i = 1; i < bottom; i++) {
|
||||
screen.setCursorPosition(i, 0);
|
||||
screen.clearCursorRow();
|
||||
}
|
||||
|
||||
// we restore the cursor position
|
||||
screen.setCursorPosition(0, x);
|
||||
}
|
||||
});
|
||||
|
||||
this.syncCursorPosition_();
|
||||
this.scrollPort_.invalidate();
|
||||
|
||||
// this will avoid a bug where the `wipeContents`
|
||||
// hterm API doens't send the scroll to the top
|
||||
this.scrollPort_.redraw_();
|
||||
};
|
||||
|
||||
export default hterm;
|
||||
export { lib };
|
||||
113
app/lib/index.js
Normal file
113
app/lib/index.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import rpc from './rpc';
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import thunk from 'redux-thunk';
|
||||
import { Provider } from 'react-redux';
|
||||
import { init } from './actions/index';
|
||||
import effects from './utils/effects';
|
||||
import * as config from './utils/config';
|
||||
import rootReducer from './reducers/index';
|
||||
import * as plugins from './utils/plugins';
|
||||
import * as uiActions from './actions/ui';
|
||||
import forceUpdate from 'react-deep-force-update';
|
||||
import * as updaterActions from './actions/updater';
|
||||
import * as sessionActions from './actions/sessions';
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
import HyperTermContainer from './containers/hyperterm';
|
||||
import { loadConfig, reloadConfig } from './actions/config';
|
||||
|
||||
import createLogger from 'redux-logger';
|
||||
const logger = createLogger({ collapsed: true });
|
||||
|
||||
const store = createStore(
|
||||
rootReducer,
|
||||
applyMiddleware(
|
||||
thunk,
|
||||
logger,
|
||||
plugins.middleware,
|
||||
thunk,
|
||||
effects
|
||||
)
|
||||
);
|
||||
|
||||
window.__defineGetter__('state', () => store.getState());
|
||||
|
||||
// initialize config
|
||||
store.dispatch(loadConfig(config.getConfig()));
|
||||
config.subscribe(() => {
|
||||
store.dispatch(reloadConfig(config.getConfig()));
|
||||
});
|
||||
|
||||
// initialize communication with main electron process
|
||||
// and subscribe to all user intents for example from menues
|
||||
rpc.on('ready', () => {
|
||||
store.dispatch(init());
|
||||
});
|
||||
|
||||
rpc.on('session add', ({ uid }) => {
|
||||
store.dispatch(sessionActions.addSession(uid));
|
||||
});
|
||||
|
||||
rpc.on('session data', ({ uid, data }) => {
|
||||
store.dispatch(sessionActions.addSessionData(uid, data));
|
||||
});
|
||||
|
||||
rpc.on('session title', ({ uid, title }) => {
|
||||
store.dispatch(sessionActions.setSessionProcessTitle(uid, title));
|
||||
});
|
||||
|
||||
rpc.on('session exit', ({ uid }) => {
|
||||
store.dispatch(sessionActions.sessionExit(uid));
|
||||
});
|
||||
|
||||
rpc.on('session add req', () => {
|
||||
store.dispatch(sessionActions.requestSession());
|
||||
});
|
||||
|
||||
rpc.on('session close req', () => {
|
||||
store.dispatch(sessionActions.userExitActiveSession());
|
||||
});
|
||||
|
||||
rpc.on('session clear req', () => {
|
||||
store.dispatch(sessionActions.clearActiveSession());
|
||||
});
|
||||
|
||||
rpc.on('reset fontSize req', () => {
|
||||
store.dispatch(uiActions.resetFontSize());
|
||||
});
|
||||
|
||||
rpc.on('increase fontSize req', () => {
|
||||
store.dispatch(uiActions.increaseFontSize());
|
||||
});
|
||||
|
||||
rpc.on('decrease fontSize req', () => {
|
||||
store.dispatch(uiActions.resetFontSize());
|
||||
});
|
||||
|
||||
rpc.on('move left req', () => {
|
||||
store.dispatch(uiActions.moveLeft());
|
||||
});
|
||||
|
||||
rpc.on('move right req', () => {
|
||||
store.dispatch(uiActions.moveRight());
|
||||
});
|
||||
|
||||
rpc.on('preferences', () => {
|
||||
store.dispatch(uiActions.showPreferences());
|
||||
});
|
||||
|
||||
rpc.on('update available', ({ releaseName, releaseNotes }) => {
|
||||
store.dispatch(updaterActions.updateAvailable(releaseName, releaseNotes));
|
||||
});
|
||||
|
||||
const app = render(
|
||||
<Provider store={ store }>
|
||||
<HyperTermContainer />
|
||||
</Provider>,
|
||||
document.getElementById('mount')
|
||||
);
|
||||
|
||||
rpc.on('reload', () => {
|
||||
plugins.reload();
|
||||
forceUpdate(app);
|
||||
});
|
||||
8
app/lib/reducers/index.js
Normal file
8
app/lib/reducers/index.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { combineReducers } from 'redux';
|
||||
import ui from './ui';
|
||||
import sessions from './sessions';
|
||||
|
||||
export default combineReducers({
|
||||
ui,
|
||||
sessions
|
||||
});
|
||||
105
app/lib/reducers/sessions.js
Normal file
105
app/lib/reducers/sessions.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import Immutable from 'seamless-immutable';
|
||||
import { decorateSessionsReducer } from '../utils/plugins';
|
||||
import {
|
||||
SESSION_ADD,
|
||||
SESSION_PTY_EXIT,
|
||||
SESSION_USER_EXIT,
|
||||
SESSION_PTY_DATA,
|
||||
SESSION_SET_ACTIVE,
|
||||
SESSION_CLEAR_ACTIVE,
|
||||
SESSION_URL_SET,
|
||||
SESSION_URL_UNSET,
|
||||
SESSION_SET_XTERM_TITLE,
|
||||
SESSION_SET_PROCESS_TITLE
|
||||
} from '../constants/sessions';
|
||||
|
||||
const initialState = Immutable({
|
||||
sessions: {},
|
||||
write: null,
|
||||
activeUid: null
|
||||
});
|
||||
|
||||
function Session (obj) {
|
||||
return Immutable({
|
||||
uid: '',
|
||||
title: '',
|
||||
write: null,
|
||||
url: null,
|
||||
cleared: false
|
||||
}).merge(obj);
|
||||
}
|
||||
|
||||
function Write (obj) {
|
||||
return Immutable({
|
||||
uid: '',
|
||||
data: ''
|
||||
}).merge(obj);
|
||||
}
|
||||
|
||||
const reducer = (state = initialState, action) => {
|
||||
// prune the last write to the terminal
|
||||
if (state.write) {
|
||||
state = state.set('write', null);
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case SESSION_ADD:
|
||||
return state.setIn(['sessions', action.uid], Session({ uid: action.uid }));
|
||||
|
||||
case SESSION_URL_SET:
|
||||
return state.setIn(['sessions', action.uid, 'url'], action.url);
|
||||
|
||||
case SESSION_URL_UNSET:
|
||||
return state.setIn(['sessions', action.uid, 'url'], null);
|
||||
|
||||
case SESSION_SET_ACTIVE:
|
||||
return state.set('activeUid', action.uid);
|
||||
|
||||
case SESSION_CLEAR_ACTIVE:
|
||||
return state.merge({
|
||||
sessions: {
|
||||
[state.activeUid]: {
|
||||
cleared: true
|
||||
}
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
case SESSION_PTY_DATA:
|
||||
return state.merge({
|
||||
write: Write(action),
|
||||
sessions: {
|
||||
[action.uid]: {
|
||||
cleared: false
|
||||
}
|
||||
}
|
||||
}, { deep: true });
|
||||
|
||||
case SESSION_PTY_EXIT:
|
||||
if (state.sessions[action.uid]) {
|
||||
return deleteSession(state, action.uid);
|
||||
} else {
|
||||
console.log('ignore pty exit: session removed by user');
|
||||
return state;
|
||||
}
|
||||
|
||||
case SESSION_USER_EXIT:
|
||||
return deleteSession(state, action.uid);
|
||||
|
||||
case SESSION_SET_XTERM_TITLE:
|
||||
case SESSION_SET_PROCESS_TITLE:
|
||||
return state.setIn(['sessions', action.uid, 'title'], action.title);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default decorateSessionsReducer(reducer);
|
||||
|
||||
function deleteSession (state, uid) {
|
||||
return state.updateIn(['sessions'], (sessions) => {
|
||||
const sessions_ = sessions.asMutable();
|
||||
delete sessions_[uid];
|
||||
return sessions_;
|
||||
});
|
||||
}
|
||||
231
app/lib/reducers/ui.js
Normal file
231
app/lib/reducers/ui.js
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import Immutable from 'seamless-immutable';
|
||||
import { decorateUIReducer } from '../utils/plugins';
|
||||
import { CONFIG_LOAD, CONFIG_RELOAD } from '../constants/config';
|
||||
import { UI_FONT_SIZE_SET, UI_FONT_SIZE_RESET } from '../constants/ui';
|
||||
import { NOTIFICATION_DISMISS } from '../constants/notifications';
|
||||
import {
|
||||
SESSION_ADD,
|
||||
SESSION_RESIZE,
|
||||
SESSION_PTY_DATA,
|
||||
SESSION_PTY_EXIT,
|
||||
SESSION_SET_ACTIVE
|
||||
} from '../constants/sessions';
|
||||
import { UPDATE_AVAILABLE } from '../constants/updater';
|
||||
|
||||
// TODO: populate `config-default.js` from this :)
|
||||
const initial = Immutable({
|
||||
cols: null,
|
||||
rows: null,
|
||||
activeUid: null,
|
||||
cursorColor: '#F81CE5',
|
||||
borderColor: '#333',
|
||||
fontSize: 12,
|
||||
padding: '12px 14px',
|
||||
fontFamily: 'Menlo, "DejaVu Sans Mono", "Lucida Console", monospace',
|
||||
fontSizeOverride: null,
|
||||
css: '',
|
||||
termCSS: '',
|
||||
openAt: {},
|
||||
resizeAt: 0,
|
||||
colors: [
|
||||
'#000000',
|
||||
'#ff0000',
|
||||
'#33ff00',
|
||||
'#ffff00',
|
||||
'#0066ff',
|
||||
'#cc00ff',
|
||||
'#00ffff',
|
||||
'#d0d0d0',
|
||||
'#808080',
|
||||
'#ff0000',
|
||||
'#33ff00',
|
||||
'#ffff00',
|
||||
'#0066ff',
|
||||
'#cc00ff',
|
||||
'#00ffff',
|
||||
'#ffffff'
|
||||
],
|
||||
activityMarkers: {},
|
||||
notifications: {
|
||||
font: false,
|
||||
resize: false,
|
||||
updates: false
|
||||
},
|
||||
foregroundColor: '#fff',
|
||||
backgroundColor: '#000',
|
||||
updateVersion: null,
|
||||
updateNotes: null
|
||||
});
|
||||
|
||||
const reducer = (state = initial, action) => {
|
||||
let state_ = state;
|
||||
|
||||
switch (action.type) {
|
||||
case CONFIG_LOAD:
|
||||
case CONFIG_RELOAD:
|
||||
const { config } = action;
|
||||
state_ = state
|
||||
// we unset the user font size override if the
|
||||
// font size changed from the config
|
||||
.merge((() => {
|
||||
const ret = {};
|
||||
|
||||
if (state.fontSizeOverride && config.fontSize !== state.fontSize) {
|
||||
ret.fontSizeOverride = null;
|
||||
}
|
||||
|
||||
if (null != config.fontSize) {
|
||||
ret.fontSize = config.fontSize;
|
||||
}
|
||||
|
||||
if (null != config.fontFamily) {
|
||||
ret.fontFamily = config.fontFamily;
|
||||
}
|
||||
|
||||
if (null != config.cursorColor) {
|
||||
ret.cursorColor = config.cursorColor;
|
||||
}
|
||||
|
||||
if (null != config.borderColor) {
|
||||
ret.borderColor = config.borderColor;
|
||||
}
|
||||
|
||||
if (null != config.padding) {
|
||||
ret.padding = config.padding;
|
||||
}
|
||||
|
||||
if (null != config.foregroundColor) {
|
||||
ret.foregroundColor = config.foregroundColor;
|
||||
}
|
||||
|
||||
if (null != config.backgroundColor) {
|
||||
ret.backgroundColor = config.backgroundColor;
|
||||
}
|
||||
|
||||
if (null != config.css) {
|
||||
ret.css = config.css;
|
||||
}
|
||||
|
||||
if (null != config.termCSS) {
|
||||
ret.termCSS = config.termCSS;
|
||||
}
|
||||
|
||||
if (null != config.colors) {
|
||||
if (state.colors.toString() !== config.colors.toString()) {
|
||||
ret.colors = config.colors;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
})());
|
||||
break;
|
||||
|
||||
case SESSION_ADD:
|
||||
state_ = state.merge({
|
||||
openAt: {
|
||||
[action.uid]: Date.now()
|
||||
}
|
||||
}, { deep: true });
|
||||
break;
|
||||
|
||||
case SESSION_RESIZE:
|
||||
state_ = state.merge({
|
||||
rows: action.rows,
|
||||
cols: action.cols,
|
||||
resizeAt: Date.now()
|
||||
});
|
||||
break;
|
||||
|
||||
case SESSION_PTY_EXIT:
|
||||
state_ = state
|
||||
.updateIn(['openAt'], (times) => {
|
||||
const times_ = times.asMutable();
|
||||
delete times_[action.uid];
|
||||
return times_;
|
||||
})
|
||||
.updateIn(['activityMarkers'], (markers) => {
|
||||
const markers_ = markers.asMutable();
|
||||
delete markers_[action.uid];
|
||||
return markers_;
|
||||
});
|
||||
break;
|
||||
|
||||
case SESSION_SET_ACTIVE:
|
||||
state_ = state.merge({
|
||||
activeUid: action.uid,
|
||||
activityMarkers: {
|
||||
[action.uid]: false
|
||||
}
|
||||
}, { deep: true });
|
||||
break;
|
||||
|
||||
case SESSION_PTY_DATA:
|
||||
// ignore activity markers for current tab
|
||||
if (action.uid === state.activeUid) break;
|
||||
|
||||
// current time for comparisons
|
||||
let now = Date.now();
|
||||
|
||||
// if first data events after open, ignore
|
||||
if (now - state.openAt[action.uid] < 1000) break;
|
||||
|
||||
// we ignore activity markers that are within
|
||||
// proximity of a resize event, since we
|
||||
// expect to get data packets from the resize
|
||||
// of the ptys as a result
|
||||
if (!state.resizeAt || now - state.resizeAt > 1000) {
|
||||
state_ = state.merge({
|
||||
activityMarkers: {
|
||||
[action.uid]: true
|
||||
}
|
||||
}, { deep: true });
|
||||
}
|
||||
break;
|
||||
|
||||
case UI_FONT_SIZE_SET:
|
||||
state_ = state.set('fontSizeOverride', state.value);
|
||||
break;
|
||||
|
||||
case UI_FONT_SIZE_RESET:
|
||||
state_ = state.set('fontSizeOverride', null);
|
||||
break;
|
||||
|
||||
case NOTIFICATION_DISMISS:
|
||||
state_ = state.merge({
|
||||
notifications: {
|
||||
[action.id]: false
|
||||
}
|
||||
}, { deep: true });
|
||||
break;
|
||||
|
||||
case UPDATE_AVAILABLE:
|
||||
state_ = state.merge({
|
||||
updateVersion: action.version,
|
||||
updateNotes: action.notes || ''
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// we check that if any of the font size values changed
|
||||
// we show a notification
|
||||
if (CONFIG_LOAD !== action.type) {
|
||||
if (state_.fontSize !== state.fontSize ||
|
||||
state_.fontSizeOverride !== state.fontSizeOverride) {
|
||||
state_ = state_.merge({ notifications: { font: true } }, { deep: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (null != state.cols && null != state.rows &&
|
||||
(state.rows !== state_.rows ||
|
||||
state.cols !== state_.cols)) {
|
||||
state_ = state_.merge({ notifications: { resize: true } }, { deep: true });
|
||||
}
|
||||
|
||||
if (state.updateVersion !== state_.updateVersion) {
|
||||
state_ = state_.merge({ notifications: { updates: true } }, { deep: true });
|
||||
}
|
||||
|
||||
return state_;
|
||||
};
|
||||
|
||||
export default decorateUIReducer(reducer);
|
||||
3
app/lib/rpc.js
Normal file
3
app/lib/rpc.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import RPC from './utils/rpc';
|
||||
const rpc = new RPC();
|
||||
export default rpc;
|
||||
3
app/lib/utils/array.js
Normal file
3
app/lib/utils/array.js
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export function last (arr) {
|
||||
return arr[arr.length - 1];
|
||||
}
|
||||
13
app/lib/utils/config.js
Normal file
13
app/lib/utils/config.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { ipcRenderer, remote } from 'electron';
|
||||
const config = remote.require('./config');
|
||||
|
||||
export function getConfig () {
|
||||
return config.getConfig();
|
||||
}
|
||||
|
||||
export function subscribe (fn) {
|
||||
ipcRenderer.on('config change', fn);
|
||||
return () => {
|
||||
ipcRenderer.removeListener('config change', fn);
|
||||
};
|
||||
}
|
||||
15
app/lib/utils/effects.js
vendored
Normal file
15
app/lib/utils/effects.js
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// simple redux middleware that executes
|
||||
// the `effect` field if provided in an action
|
||||
// since this is preceded by the `plugins`
|
||||
// middleware, it allows authors to interrumpt,
|
||||
// defer or add to existing side effects at will
|
||||
// as the result of an action being triggered
|
||||
|
||||
export default (store) => (next) => (action) => {
|
||||
const ret = next(action);
|
||||
if (action.effect) {
|
||||
action.effect();
|
||||
delete action.effect;
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
/* global Notification */
|
||||
/* eslint no-new:0 */
|
||||
export default function notify (title, body) {
|
||||
console.log(`[Notification] ${title}: ${body}`);
|
||||
new Notification(title, { body });
|
||||
}
|
||||
17
app/lib/utils/object.js
Normal file
17
app/lib/utils/object.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import vals from 'object-values';
|
||||
|
||||
const valsCache = new WeakMap();
|
||||
export function values (imm) {
|
||||
if (!valsCache.has(imm)) {
|
||||
valsCache.set(imm, vals(imm));
|
||||
}
|
||||
return valsCache.get(imm);
|
||||
}
|
||||
|
||||
const keysCache = new WeakMap();
|
||||
export function keys (imm) {
|
||||
if (!keysCache.has(imm)) {
|
||||
keysCache.set(imm, Object.keys(imm));
|
||||
}
|
||||
return keysCache.get(imm);
|
||||
}
|
||||
31
app/lib/utils/override-style.js
Normal file
31
app/lib/utils/override-style.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// hack to configure important style attributes
|
||||
// to work around this madness:
|
||||
// https://github.com/Khan/aphrodite/pull/41
|
||||
|
||||
export default function overrideStyle (obj) {
|
||||
if (obj) {
|
||||
return function (ref) {
|
||||
if (ref) {
|
||||
for (const key in obj) {
|
||||
if (null != obj[key]) {
|
||||
const val = 'number' === typeof obj[key]
|
||||
? `${obj[key]}px`
|
||||
: String(obj[key]);
|
||||
const prop = toProp(key);
|
||||
ref.style.setProperty(prop, val, 'important');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// converts camelCase to camel-case
|
||||
function toProp (key) {
|
||||
return key.replace(
|
||||
/[a-z]([A-Z])/,
|
||||
(a, b) => a.substr(0, a.length - 1) + '-' + b.toLowerCase()
|
||||
);
|
||||
}
|
||||
382
app/lib/utils/plugins.js
Normal file
382
app/lib/utils/plugins.js
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
import { ipcRenderer, remote } from 'electron';
|
||||
import { connect as reduxConnect } from 'react-redux';
|
||||
|
||||
// we expose these two deps to component decorators
|
||||
import React from 'react';
|
||||
import Notification from '../components/notification';
|
||||
import notify from './notify';
|
||||
|
||||
// remote interface to `../plugins`
|
||||
let plugins = remote.require('./plugins');
|
||||
|
||||
// `require`d modules
|
||||
let modules;
|
||||
|
||||
// cache of decorated components
|
||||
let decorated = {};
|
||||
|
||||
// varios caches extracted of the plugin methods
|
||||
let connectors;
|
||||
let middlewares;
|
||||
let uiReducers;
|
||||
let sessionsReducers;
|
||||
let tabPropsDecorators;
|
||||
let tabsPropsDecorators;
|
||||
let termPropsDecorators;
|
||||
|
||||
// the fs locations where usr plugins are stored
|
||||
const { path, localPath } = plugins.getBasePaths();
|
||||
|
||||
const clearModulesCache = () => {
|
||||
// clear require cache
|
||||
for (const entry in window.require.cache) {
|
||||
if (entry.indexOf(path) === 0 || entry.indexOf(localPath) === 0) {
|
||||
// `require` is webpacks', `window.require`, electron's
|
||||
delete window.require.cache[entry];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getPluginName = (path) => window.require('path').basename(path);
|
||||
|
||||
const loadModules = () => {
|
||||
console.log('(re)loading renderer plugins');
|
||||
const paths = plugins.getPaths();
|
||||
|
||||
// initialize cache that we populate with extension methods
|
||||
connectors = {
|
||||
Terms: { state: [], dispatch: [] },
|
||||
Header: { state: [], dispatch: [] },
|
||||
HyperTerm: { state: [], dispatch: [] },
|
||||
Notifications: { state: [], dispatch: [] }
|
||||
};
|
||||
uiReducers = [];
|
||||
middlewares = [];
|
||||
sessionsReducers = [];
|
||||
tabPropsDecorators = [];
|
||||
tabsPropsDecorators = [];
|
||||
termPropsDecorators = [];
|
||||
|
||||
modules = paths.plugins.concat(paths.localPlugins)
|
||||
.map((path) => {
|
||||
let mod;
|
||||
const pluginName = getPluginName(path);
|
||||
|
||||
// window.require allows us to ensure this doens't get
|
||||
// in the way of our build
|
||||
try {
|
||||
mod = window.require(path);
|
||||
} catch (err) {
|
||||
console.error(err.stack);
|
||||
notify('Plugin load error', `"${pluginName}" failed to load in the renderer process. Check Developer Tools for details.`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const i in mod) {
|
||||
mod[i]._pluginName = pluginName;
|
||||
}
|
||||
|
||||
if (mod.middleware) {
|
||||
middlewares.push(mod.middleware);
|
||||
}
|
||||
|
||||
if (mod.reduceUI) {
|
||||
uiReducers.push(mod.reduceUI);
|
||||
}
|
||||
|
||||
if (mod.reduceSessions) {
|
||||
sessionsReducers.push(mod.reduceSessions);
|
||||
}
|
||||
|
||||
if (mod.mapTermsState) {
|
||||
connectors.Terms.state.push(mod.mapTermsState);
|
||||
}
|
||||
|
||||
if (mod.mapTermsDispatch) {
|
||||
connectors.Terms.dispatch.push(mod.mapTermsState);
|
||||
}
|
||||
|
||||
if (mod.mapHeaderState) {
|
||||
connectors.Header.state.push(mod.mapHeaderState);
|
||||
}
|
||||
|
||||
if (mod.mapHeaderDispatch) {
|
||||
connectors.Header.dispatch.push(mod.mapHeaderDispatch);
|
||||
}
|
||||
|
||||
if (mod.mapHyperTermState) {
|
||||
connectors.HyperTerm.state.push(mod.mapHyperTermState);
|
||||
}
|
||||
|
||||
if (mod.mapHyperTermDispatch) {
|
||||
connectors.HyperTerm.dispatch.push(mod.mapHyperTermDispatch);
|
||||
}
|
||||
|
||||
if (mod.mapNotificationsState) {
|
||||
connectors.Notifications.state.push(mod.mapNotificationsState);
|
||||
}
|
||||
|
||||
if (mod.mapNotificationsDispatch) {
|
||||
connectors.Notifications.dispatch.push(mod.mapNotificationsDispatch);
|
||||
}
|
||||
|
||||
if (mod.getTermProps) {
|
||||
termPropsDecorators.push(mod.getTermProps);
|
||||
}
|
||||
|
||||
if (mod.getTabProps) {
|
||||
tabPropsDecorators.push(mod.getTabProps);
|
||||
}
|
||||
|
||||
if (mod.getTabsProps) {
|
||||
tabsPropsDecorators.push(mod.getTabsProps);
|
||||
}
|
||||
|
||||
return mod;
|
||||
})
|
||||
.filter((mod) => !!mod);
|
||||
};
|
||||
|
||||
// load modules for initial decoration
|
||||
loadModules();
|
||||
|
||||
export function reload () {
|
||||
clearModulesCache();
|
||||
loadModules();
|
||||
// trigger re-decoration when components
|
||||
// get re-rendered
|
||||
decorated = {};
|
||||
}
|
||||
|
||||
// we want to refresh our modules cache every time
|
||||
// plugins reload.
|
||||
// the re-painting happens by the top-level `Config` component
|
||||
// that reacts to configuration changes and plugin changes
|
||||
ipcRenderer.on('plugins change', function (ev) {
|
||||
notify(
|
||||
'Plugins Updated',
|
||||
'Restart or choose "Plugins" > "Reload Now"'
|
||||
);
|
||||
});
|
||||
|
||||
export function getTermProps (uid, parentProps, props) {
|
||||
let props_;
|
||||
|
||||
termPropsDecorators.forEach((fn) => {
|
||||
let ret_;
|
||||
|
||||
if (!props_) props_ = Object.assign({}, props);
|
||||
|
||||
try {
|
||||
ret_ = fn(uid, parentProps, props_);
|
||||
} catch (err) {
|
||||
console.error(err.stack);
|
||||
notify('Plugin error', `${fn._pluginName}: Error occurred in \`getTermProps\`. Check Developer Tools for details.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ret_ || 'object' !== typeof ret_) {
|
||||
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`getTermProps\` (object expected).`);
|
||||
return;
|
||||
}
|
||||
|
||||
props = ret_;
|
||||
});
|
||||
|
||||
return props_ || props;
|
||||
}
|
||||
|
||||
export function getTabsProps (parentProps, props) {
|
||||
let props_;
|
||||
|
||||
tabsPropsDecorators.forEach((fn) => {
|
||||
let ret_;
|
||||
|
||||
if (!props_) props_ = Object.assign({}, props);
|
||||
|
||||
try {
|
||||
ret_ = fn(parentProps, props_);
|
||||
} catch (err) {
|
||||
console.error(err.stack);
|
||||
notify('Plugin error', `${fn._pluginName}: Error occurred in \`getTabsProps\`. Check Developer Tools for details.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ret_ || 'object' !== typeof ret_) {
|
||||
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`getTabsProps\` (object expected).`);
|
||||
return;
|
||||
}
|
||||
|
||||
props_ = ret_;
|
||||
});
|
||||
|
||||
return props_ || props;
|
||||
}
|
||||
|
||||
export function getTabProps (tab, parentProps, props) {
|
||||
let props_;
|
||||
|
||||
tabPropsDecorators.forEach((fn) => {
|
||||
let ret_;
|
||||
|
||||
if (!props_) props_ = Object.assign({}, props);
|
||||
|
||||
try {
|
||||
ret_ = fn(tab, parentProps, props_);
|
||||
} catch (err) {
|
||||
console.error(err.stack);
|
||||
notify('Plugin error', `${fn._pluginName}: Error occurred in \`getTabProps\`. Check Developer Tools for details.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ret_ || 'object' !== typeof ret_) {
|
||||
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`getTabProps\` (object expected).`);
|
||||
return;
|
||||
}
|
||||
|
||||
props_ = ret_;
|
||||
});
|
||||
|
||||
return props_ || props;
|
||||
}
|
||||
|
||||
// connects + decorates a class
|
||||
// plugins can override mapToState, dispatchToProps
|
||||
// and the class gets deorated (proxied)
|
||||
export function connect (stateFn, dispatchFn, c, d = {}) {
|
||||
return function (Class, name) {
|
||||
return reduxConnect(
|
||||
function (state) {
|
||||
let ret = stateFn(state);
|
||||
connectors[name].state.forEach((fn) => {
|
||||
let ret_;
|
||||
|
||||
try {
|
||||
ret_ = fn(state, ret);
|
||||
} catch (err) {
|
||||
console.error(err.stack);
|
||||
notify('Plugin error', `${fn._pluginName}: Error occurred in \`map${name}State\`. Check Developer Tools for details.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ret_ || 'object' !== typeof ret_) {
|
||||
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`map${name}State\` (object expected).`);
|
||||
return;
|
||||
}
|
||||
|
||||
ret = ret_;
|
||||
});
|
||||
return ret;
|
||||
},
|
||||
dispatchFn,
|
||||
c,
|
||||
d
|
||||
)(decorate(Class, name));
|
||||
};
|
||||
}
|
||||
|
||||
export function decorateUIReducer (fn) {
|
||||
return (state, action) => {
|
||||
let state_ = fn(state, action);
|
||||
|
||||
uiReducers.forEach((pluginReducer) => {
|
||||
let state__;
|
||||
|
||||
try {
|
||||
state__ = pluginReducer(state_, action);
|
||||
} catch (err) {
|
||||
console.error(err.stack);
|
||||
notify('Plugin error', `${fn._pluginName}: Error occurred in \`reduceUI\`. Check Developer Tools for details.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state__ || 'object' !== typeof state__) {
|
||||
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`reduceUI\`.`);
|
||||
return;
|
||||
}
|
||||
|
||||
state_ = state__;
|
||||
});
|
||||
|
||||
return state_;
|
||||
};
|
||||
}
|
||||
|
||||
export function decorateSessionsReducer (fn) {
|
||||
return (state, action) => {
|
||||
let state_ = fn(state, action);
|
||||
|
||||
sessionsReducers.forEach((pluginReducer) => {
|
||||
let state__;
|
||||
|
||||
try {
|
||||
state__ = pluginReducer(state_, action);
|
||||
} catch (err) {
|
||||
console.error(err.stack);
|
||||
notify('Plugin error', `${fn._pluginName}: Error occurred in \`reduceSessions\`. Check Developer Tools for details.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state__ || 'object' !== typeof state__) {
|
||||
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`reduceSessions\`.`);
|
||||
return;
|
||||
}
|
||||
|
||||
state_ = state__;
|
||||
});
|
||||
|
||||
return state_;
|
||||
};
|
||||
}
|
||||
|
||||
// redux middleware generator
|
||||
export const middleware = (store) => (next) => (action) => {
|
||||
const nextMiddleware = remaining => action => remaining.length
|
||||
? remaining[0](store)(nextMiddleware(remaining.slice(1)))(action)
|
||||
: next(action);
|
||||
nextMiddleware(middlewares)(action);
|
||||
};
|
||||
|
||||
function getDecorated (parent, name) {
|
||||
if (!decorated[name]) {
|
||||
let class_ = parent;
|
||||
|
||||
modules.forEach((mod) => {
|
||||
const method = 'decorate' + name;
|
||||
const fn = mod[method];
|
||||
|
||||
if (fn) {
|
||||
let class__;
|
||||
|
||||
try {
|
||||
class__ = fn(class_, { React, Notification, notify });
|
||||
} catch (err) {
|
||||
console.error(err.stack);
|
||||
notify('Plugin error', `${fn._pluginName}: Error occurred in \`${method}\`. Check Developer Tools for details`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!class__ || 'function' !== typeof class__.prototype.render) {
|
||||
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`${method}\`. No \`render\` method found. Please return a \`React.Component\`.`);
|
||||
return;
|
||||
}
|
||||
|
||||
class_ = class__;
|
||||
}
|
||||
});
|
||||
|
||||
decorated[name] = class_;
|
||||
}
|
||||
|
||||
return decorated[name];
|
||||
}
|
||||
|
||||
// for each component, we return the `react-proxy`d component
|
||||
export function decorate (Component, name) {
|
||||
return class extends React.Component {
|
||||
render () {
|
||||
const Sub = getDecorated(Component, name);
|
||||
return <Sub {...this.props} />;
|
||||
}
|
||||
};
|
||||
}
|
||||
40
app/lib/utils/url-command.js
Normal file
40
app/lib/utils/url-command.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import * as regex from './url-regex';
|
||||
|
||||
export default function isUrlCommand (data) {
|
||||
let match = data.match(regex.bash);
|
||||
let url;
|
||||
|
||||
if (match) {
|
||||
url = match[5];
|
||||
} else {
|
||||
match = data.match(regex.zsh);
|
||||
if (match) {
|
||||
url = match[7];
|
||||
} else {
|
||||
match = data.match(regex.fish);
|
||||
if (match) {
|
||||
url = match[4];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (url) {
|
||||
// extract the domain portion from the url
|
||||
const domain = url.split('/')[0];
|
||||
if (regex.domain.test(domain)) {
|
||||
return toURL(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toURL (domain) {
|
||||
if (/^https?:\/\//.test(domain)) {
|
||||
return domain;
|
||||
}
|
||||
|
||||
if ('//' === domain.substr(0, 2)) {
|
||||
return domain;
|
||||
}
|
||||
|
||||
return 'http://' + domain;
|
||||
}
|
||||
|
|
@ -1,36 +1,40 @@
|
|||
{
|
||||
"name": "hyperterm-web",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"description": "Web app that runs in the renderer process",
|
||||
"license": "MIT",
|
||||
"repository": "zeit/hyperterm",
|
||||
"dependencies": {
|
||||
"classnames": "2.2.5",
|
||||
"aphrodite": "0.4.1",
|
||||
"hterm-umdjs": "1.1.2",
|
||||
"json-loader": "0.5.4",
|
||||
"mousetrap": "1.6.0",
|
||||
"ms": "0.7.1",
|
||||
"object-values": "1.0.0",
|
||||
"react": "15.2.1",
|
||||
"react-addons-shallow-compare": "15.2.1",
|
||||
"react-addons-pure-render-mixin": "15.2.1",
|
||||
"react-deep-force-update": "2.0.1",
|
||||
"react-dom": "15.2.1",
|
||||
"react-proxy": "1.1.8",
|
||||
"semver-compare": "1.0.0"
|
||||
"react-redux": "4.4.5",
|
||||
"redux": "3.5.2",
|
||||
"redux-logger": "2.6.1",
|
||||
"redux-thunk": "2.1.0",
|
||||
"reselect": "2.5.3",
|
||||
"seamless-immutable": "6.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "6.10.1",
|
||||
"babel-core": "6.10.4",
|
||||
"babel-eslint": "6.1.1",
|
||||
"babel-loader": "6.2.4",
|
||||
"babel-plugin-transform-es2015-classes": "6.9.0",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "6.10.3",
|
||||
"babel-plugin-transform-react-jsx": "6.8.0",
|
||||
"babel-preset-es2015": "6.9.0",
|
||||
"css-loader": "0.23.1",
|
||||
"babel-preset-es2015-native-modules": "6.6.0",
|
||||
"babel-preset-react": "6.11.1",
|
||||
"eslint": "3.0.1",
|
||||
"eslint-config-standard": "5.3.1",
|
||||
"eslint-plugin-promise": "1.3.2",
|
||||
"eslint-plugin-react": "5.2.2",
|
||||
"eslint-plugin-standard": "1.3.2",
|
||||
"style-loader": "0.13.1",
|
||||
"webpack": "1.13.1"
|
||||
"webpack": "2.1.0-beta.15"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "standard",
|
||||
|
|
@ -62,10 +66,9 @@
|
|||
}
|
||||
},
|
||||
"babel": {
|
||||
"plugins": [
|
||||
"transform-es2015-classes",
|
||||
"transform-es2015-modules-commonjs",
|
||||
"transform-react-jsx"
|
||||
"presets": [
|
||||
"es2015-native-modules",
|
||||
"react"
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
import { ipcRenderer, remote } from 'electron';
|
||||
import notify from './notify';
|
||||
import { createProxy } from 'react-proxy';
|
||||
|
||||
// remote interface to `../plugins`
|
||||
let plugins = remote.require('./plugins');
|
||||
|
||||
// `require`d modules
|
||||
let modules;
|
||||
|
||||
// the fs locations where usr plugins are stored
|
||||
const { path, localPath } = plugins.getBasePaths();
|
||||
|
||||
// where we store the decorated components
|
||||
let proxies = {};
|
||||
|
||||
const clearCache = () => {
|
||||
// clear require cache
|
||||
for (const entry in window.require.cache) {
|
||||
if (entry.indexOf(path) === 0 || entry.indexOf(localPath) === 0) {
|
||||
// `require` is webpacks', `window.require`, electron's
|
||||
delete window.require.cache[entry];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadModules = () => {
|
||||
console.log('(re)loading renderer plugins');
|
||||
const paths = plugins.getPaths();
|
||||
modules = paths.plugins.concat(paths.localPlugins).map((path) => {
|
||||
// window.require allows us to ensure this doens't get
|
||||
// in the way of our build
|
||||
try {
|
||||
return window.require(path);
|
||||
} catch (err) {
|
||||
const name = remote.require('path').basename(path);
|
||||
console.error(err.stack);
|
||||
notify('Plugin load error', `"${name}" failed to load in the renderer process. Check Developer Tools for details.`);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const updateProxy = (name) => {
|
||||
const [Component, proxy] = proxies[name];
|
||||
let decorated = Component;
|
||||
modules.forEach((mod) => {
|
||||
const decorator = mod[`decorate${name}`];
|
||||
if (decorator) {
|
||||
decorated = decorator(Component);
|
||||
}
|
||||
});
|
||||
if (decorated !== Component) {
|
||||
proxy.update(decorated);
|
||||
}
|
||||
};
|
||||
|
||||
const updateProxies = () => {
|
||||
for (const name in proxies) {
|
||||
updateProxy(name);
|
||||
}
|
||||
};
|
||||
|
||||
// load modules for initial decoration
|
||||
loadModules();
|
||||
|
||||
// we want to refresh our modules cache every time
|
||||
// plugins reload.
|
||||
// the re-painting happens by the top-level `Config` component
|
||||
// that reacts to configuration changes and plugin changes
|
||||
ipcRenderer.on('plugins change', () => {
|
||||
clearCache();
|
||||
loadModules();
|
||||
updateProxies();
|
||||
});
|
||||
|
||||
// for each component, we return the `react-proxy`d component
|
||||
export default function decorate (Component, props = null) {
|
||||
const name = Component.name;
|
||||
|
||||
if (!proxies[name]) {
|
||||
const proxy = createProxy(Component);
|
||||
proxies[name] = [Component, proxy];
|
||||
updateProxy(name);
|
||||
}
|
||||
|
||||
const [, proxy] = proxies[name];
|
||||
return proxy.get();
|
||||
}
|
||||
52
app/tabs.js
52
app/tabs.js
|
|
@ -1,52 +0,0 @@
|
|||
import React from 'react';
|
||||
import classes from 'classnames';
|
||||
|
||||
export default class Tabs extends React.Component {
|
||||
|
||||
render () {
|
||||
const {
|
||||
data = [],
|
||||
borderColor,
|
||||
active,
|
||||
activeMarkers = [],
|
||||
onChange,
|
||||
onClose
|
||||
} = this.props;
|
||||
|
||||
return <nav style={{ WebkitAppRegion: 'drag' }}>{
|
||||
data.length
|
||||
? 1 === data.length
|
||||
? <div className='single'>{ data[0] }</div>
|
||||
: <ul style={{ borderColor }} className='tabs'>
|
||||
{
|
||||
data.map((tab, i) => {
|
||||
const isActive = i === active;
|
||||
const hasActivity = ~activeMarkers.indexOf(i);
|
||||
return <li
|
||||
key={`tab-${i}`}
|
||||
className={classes({ is_active: isActive, has_activity: hasActivity })}>
|
||||
<span
|
||||
style={{ borderColor: isActive ? borderColor : null }}
|
||||
onClick={ onChange ? onClick.bind(null, i, onChange, active) : null }>
|
||||
{ tab }
|
||||
</span>
|
||||
<i onClick={ onClose ? onClose.bind(null, i) : null }>
|
||||
<svg className='icon'>
|
||||
<use xlinkHref='assets/icons.svg#close'></use>
|
||||
</svg>
|
||||
</i>
|
||||
</li>;
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
: null
|
||||
}</nav>;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function onClick (i, onChange, active) {
|
||||
if (i !== active) {
|
||||
onChange(i);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ const isProd = nodeEnv === 'production';
|
|||
|
||||
module.exports = {
|
||||
devtool: isProd ? 'hidden-source-map' : 'cheap-eval-source-map',
|
||||
entry: './index.js',
|
||||
entry: './lib/index.js',
|
||||
output: {
|
||||
path: path.join(__dirname, './dist'),
|
||||
filename: 'bundle.js'
|
||||
|
|
@ -23,16 +23,24 @@ module.exports = {
|
|||
{
|
||||
test: /\.json/,
|
||||
loader: 'json-loader'
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
loader: 'style-loader!css-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new webpack.optimize.OccurrenceOrderPlugin(),
|
||||
new webpack.ExternalsPlugin('commonjs', ['electron']),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
warnings: false
|
||||
},
|
||||
output: {
|
||||
comments: false
|
||||
},
|
||||
sourceMap: false
|
||||
}),
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
minimize: true,
|
||||
debug: false
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': {
|
||||
'NODE_ENV': JSON.stringify(nodeEnv)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
module.exports = {
|
||||
config: {
|
||||
// default font size for all tabs
|
||||
// default font size in pixels for all tabs
|
||||
fontSize: 12,
|
||||
|
||||
// font family with optional fallbacks
|
||||
|
|
@ -9,17 +9,23 @@ module.exports = {
|
|||
// terminal cursor background color (hex)
|
||||
cursorColor: '#F81CE5',
|
||||
|
||||
// terminal background color (hex)
|
||||
backgroundColor: '#000000',
|
||||
// color of the text
|
||||
foregroundColor: '#fff',
|
||||
|
||||
// terminal background color
|
||||
backgroundColor: '#000',
|
||||
|
||||
// border color (window, tabs)
|
||||
borderColor: '#333',
|
||||
|
||||
// custom css to embed in the main window
|
||||
css: [''],
|
||||
css: '',
|
||||
|
||||
// custom css to embed inside each terminal
|
||||
termCSS: [''],
|
||||
// custom padding (css format, i.e.: `top right bottom left`)
|
||||
termCSS: '',
|
||||
|
||||
// custom padding
|
||||
padding: '12px 14px',
|
||||
|
||||
// some color overrides. see http://bit.ly/29k1iU2 for
|
||||
// the full list
|
||||
|
|
|
|||
40
index.js
40
index.js
|
|
@ -6,6 +6,7 @@ const genUid = require('uid2');
|
|||
const { resolve } = require('path');
|
||||
const isDev = require('electron-is-dev');
|
||||
const AutoUpdater = require('./auto-updater');
|
||||
const toHex = require('convert-css-color-name-to-hex');
|
||||
|
||||
// set up config
|
||||
const config = require('./config');
|
||||
|
|
@ -44,7 +45,7 @@ app.on('ready', () => {
|
|||
height: 380,
|
||||
titleBarStyle: 'hidden-inset',
|
||||
title: 'HyperTerm',
|
||||
backgroundColor: config.getConfig().backgroundColor || '#000',
|
||||
backgroundColor: toHex(config.getConfig().backgroundColor || '#000'),
|
||||
transparent: true,
|
||||
// we only want to show when the prompt
|
||||
// is ready for user input
|
||||
|
|
@ -75,29 +76,42 @@ app.on('ready', () => {
|
|||
rpc.on('new', ({ rows = 40, cols = 100 }) => {
|
||||
initSession({ rows, cols }, (uid, session) => {
|
||||
sessions.set(uid, session);
|
||||
rpc.emit('new session', { uid });
|
||||
rpc.emit('session add', { uid });
|
||||
|
||||
session.on('data', (data) => {
|
||||
rpc.emit('data', { uid, data });
|
||||
rpc.emit('session data', { uid, data });
|
||||
});
|
||||
|
||||
session.on('title', (title) => {
|
||||
rpc.emit('title', { uid, title });
|
||||
rpc.emit('session title', { uid, title });
|
||||
});
|
||||
|
||||
session.on('exit', () => {
|
||||
rpc.emit('exit', { uid });
|
||||
rpc.emit('session exit', { uid });
|
||||
sessions.delete(uid);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// TODO: this goes away when we are able to poll
|
||||
// for the title ourseleves, instead of relying
|
||||
// on Session and focus/blur to subscribe
|
||||
rpc.on('focus', ({ uid }) => {
|
||||
sessions.get(uid).focus();
|
||||
const session = sessions.get(uid);
|
||||
if (session) {
|
||||
session.focus();
|
||||
} else {
|
||||
console.log('session not found by', uid);
|
||||
}
|
||||
});
|
||||
|
||||
rpc.on('blur', ({ uid }) => {
|
||||
sessions.get(uid).blur();
|
||||
const session = sessions.get(uid);
|
||||
if (session) {
|
||||
session.blur();
|
||||
} else {
|
||||
console.log('session not found by', uid);
|
||||
}
|
||||
});
|
||||
|
||||
rpc.on('exit', ({ uid }) => {
|
||||
|
|
@ -154,9 +168,15 @@ app.on('ready', () => {
|
|||
// load plugins
|
||||
load();
|
||||
|
||||
const pluginsUnsubscribe = plugins.subscribe(() => {
|
||||
load();
|
||||
win.webContents.send('plugins change');
|
||||
const pluginsUnsubscribe = plugins.subscribe((err, { force }) => {
|
||||
if (!err) {
|
||||
load();
|
||||
if (force) {
|
||||
win.webContents.send('plugins reload');
|
||||
} else {
|
||||
win.webContents.send('plugins change');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// the window can be closed by the browser process itself
|
||||
|
|
|
|||
41
menu.js
41
menu.js
|
|
@ -66,7 +66,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
|
|||
accelerator: 'CmdOrCtrl+T',
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.rpc.emit('new tab');
|
||||
focusedWindow.rpc.emit('session add req');
|
||||
} else {
|
||||
createWindow();
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
|
|||
accelerator: 'CmdOrCtrl+W',
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.rpc.emit('close tab');
|
||||
focusedWindow.rpc.emit('session close req');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
|
|||
accelerator: 'CmdOrCtrl+K',
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.rpc.emit('clear');
|
||||
focusedWindow.rpc.emit('session clear req');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -108,7 +108,18 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
|
|||
label: 'Reload',
|
||||
accelerator: 'CmdOrCtrl+R',
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) focusedWindow.reload();
|
||||
if (focusedWindow) {
|
||||
focusedWindow.rpc.emit('reload');
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Full Reload',
|
||||
accelerator: 'CmdOrCtrl+Shift+R',
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.reload();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
@ -128,7 +139,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
|
|||
accelerator: 'CmdOrCtrl+0',
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.rpc.emit('reset font size');
|
||||
focusedWindow.rpc.emit('reset fontSize req');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -137,7 +148,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
|
|||
accelerator: 'CmdOrCtrl+plus',
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.rpc.emit('increase font size');
|
||||
focusedWindow.rpc.emit('increase fontSize req');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -146,24 +157,12 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
|
|||
accelerator: 'CmdOrCtrl+-',
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.rpc.emit('decrease font size');
|
||||
focusedWindow.rpc.emit('decrease fontSize req');
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Tools',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Update plugins',
|
||||
accelerator: 'CmdOrCtrl+U',
|
||||
click (item, focusedWindow) {
|
||||
updatePlugins();
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'Window',
|
||||
submenu: [
|
||||
|
|
@ -181,7 +180,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
|
|||
accelerator: 'CmdOrCtrl+Left',
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.rpc.emit('move left');
|
||||
focusedWindow.rpc.emit('move left req');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -190,7 +189,7 @@ module.exports = function createMenu ({ createWindow, updatePlugins }) {
|
|||
accelerator: 'CmdOrCtrl+Right',
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.rpc.emit('move right');
|
||||
focusedWindow.rpc.emit('move right req');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ app.on('ready', () => {
|
|||
});
|
||||
|
||||
function notify (title, body) {
|
||||
console.log(`[Notification] ${title}: ${body}`);
|
||||
if (win) {
|
||||
win.webContents.send('notification', { title, body });
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
"description": "HTML/JS/CSS Terminal",
|
||||
"dependencies": {
|
||||
"child_pty": "3.0.1",
|
||||
"convert-css-color-name-to-hex": "0.1.1",
|
||||
"default-shell": "1.0.1",
|
||||
"electron-config": "0.2.0",
|
||||
"electron-is-dev": "0.1.1",
|
||||
|
|
|
|||
17
plugins.js
17
plugins.js
|
|
@ -75,16 +75,16 @@ function updatePlugins ({ force = false } = {}) {
|
|||
// cache modules
|
||||
modules = requirePlugins();
|
||||
|
||||
// notify watchers
|
||||
watchers.forEach((fn) => fn(err));
|
||||
|
||||
const loaded = modules.length;
|
||||
const total = paths.plugins.length + paths.localPlugins.length;
|
||||
const pluginVersions = JSON.stringify(getPluginVersions());
|
||||
if (force || (cache.get('plugin-versions') !== pluginVersions && loaded === total)) {
|
||||
notify('HyperTerm plugins updated!');
|
||||
}
|
||||
const changed = cache.get('plugin-versions') !== pluginVersions && loaded === total;
|
||||
cache.set('plugin-versions', pluginVersions);
|
||||
|
||||
// notify watchers
|
||||
if (force || changed) {
|
||||
watchers.forEach((fn) => fn(err, { force }));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -221,9 +221,12 @@ function requirePlugins () {
|
|||
mod = require(path);
|
||||
|
||||
if (!mod || (!mod.onApp && !mod.onWindow && !mod.onUnload &&
|
||||
!mod.middleware &&
|
||||
!mod.decorateConfig && !mod.decorateMenu &&
|
||||
!mod.decorateTerm && !mod.decorateHyperTerm &&
|
||||
!mod.decorateTabs && !mod.decorateConfig)) {
|
||||
!mod.decorateTab && !mod.decorateNotification &&
|
||||
!mod.decorateNotifications && !mod.decorateTabs &&
|
||||
!mod.decorateConfig)) {
|
||||
notify('Plugin error!', `Plugin "${basename(path)}" does not expose any ` +
|
||||
'HyperTerm extension API methods');
|
||||
return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue