Fixed merge conflicts

This commit is contained in:
Leo Lamprecht 2018-04-16 07:17:17 -07:00
parent 4d7ce43755
commit 4ce99863c4
133 changed files with 74506 additions and 53064 deletions

View file

@ -8,8 +8,10 @@ export function loadConfig(config) {
}
export function reloadConfig(config) {
const now = Date.now();
return {
type: CONFIG_RELOAD,
config
config,
now
};
}

View file

@ -1,5 +1,11 @@
import {CLOSE_TAB, CHANGE_TAB} from '../constants/tabs';
import {UI_WINDOW_MAXIMIZE, UI_WINDOW_UNMAXIMIZE, UI_OPEN_HAMBURGER_MENU, UI_WINDOW_MINIMIZE, UI_WINDOW_CLOSE} from '../constants/ui';
import {
UI_WINDOW_MAXIMIZE,
UI_WINDOW_UNMAXIMIZE,
UI_OPEN_HAMBURGER_MENU,
UI_WINDOW_MINIMIZE,
UI_WINDOW_CLOSE
} from '../constants/ui';
import rpc from '../rpc';
import {userExitTermGroup, setActiveGroup} from './term-groups';

View file

@ -1,7 +1,4 @@
import {
NOTIFICATION_MESSAGE,
NOTIFICATION_DISMISS
} from '../constants/notifications';
import {NOTIFICATION_MESSAGE, NOTIFICATION_DISMISS} from '../constants/notifications';
export function dismissNotification(id) {
return {

View file

@ -38,11 +38,11 @@ export function addSession({uid, shell, pid, cols, rows, splitDirection}) {
export function requestSession() {
return (dispatch, getState) => {
const {ui} = getState();
const {cols, rows, cwd} = ui;
dispatch({
type: SESSION_REQUEST,
effect: () => {
const {ui} = getState();
const {cols, rows, cwd} = ui;
rpc.emit('new', {cols, rows, cwd});
}
});
@ -50,7 +50,7 @@ export function requestSession() {
}
export function addSessionData(uid, data) {
return function (dispatch, getState) {
return (dispatch, getState) => {
dispatch({
type: SESSION_ADD_DATA,
data,
@ -147,7 +147,7 @@ export function resizeSession(uid, cols, rows) {
}
export function sendSessionData(uid, data, escaped) {
return function (dispatch, getState) {
return (dispatch, getState) => {
dispatch({
type: SESSION_USER_DATA,
data,

View file

@ -13,10 +13,10 @@ import {setActiveSession, ptyExitSession, userExitSession} from './sessions';
function requestSplit(direction) {
return () => (dispatch, getState) => {
const {ui} = getState();
dispatch({
type: SESSION_REQUEST,
effect: () => {
const {ui} = getState();
rpc.emit('new', {
splitDirection: direction,
cwd: ui.cwd
@ -39,11 +39,11 @@ export function resizeTermGroup(uid, sizes) {
export function requestTermGroup() {
return (dispatch, getState) => {
const {ui} = getState();
const {cols, rows, cwd} = ui;
dispatch({
type: TERM_GROUP_REQUEST,
effect: () => {
const {ui} = getState();
const {cols, rows, cwd} = ui;
rpc.emit('new', {
isNewGroup: true,
cols,

View file

@ -5,11 +5,7 @@ import getRootGroups from '../selectors';
import findBySession from '../utils/term-groups';
import notify from '../utils/notify';
import rpc from '../rpc';
import {
requestSession,
sendSessionData,
setActiveSession
} from '../actions/sessions';
import {requestSession, sendSessionData, setActiveSession} from '../actions/sessions';
import {
UI_FONT_SIZE_SET,
UI_FONT_SIZE_INCR,
@ -23,13 +19,33 @@ import {
UI_MOVE_PREV_PANE,
UI_WINDOW_GEOMETRY_CHANGED,
UI_WINDOW_MOVE,
UI_OPEN_FILE
UI_OPEN_FILE,
UI_OPEN_SSH_URL,
UI_CONTEXTMENU_OPEN,
UI_COMMAND_EXEC
} from '../constants/ui';
import {setActiveGroup} from './term-groups';
import parseUrl from 'parse-url';
const {stat} = window.require('fs');
export function openContextMenu(uid, selection) {
return (dispatch, getState) => {
dispatch({
type: UI_CONTEXTMENU_OPEN,
uid,
effect() {
const state = getState();
const show = !state.ui.quickEdit;
if (show) {
rpc.emit('open context menu', selection);
}
}
});
};
}
export function increaseFontSize() {
return (dispatch, getState) => {
dispatch({
@ -74,9 +90,7 @@ export function setFontSmoothing() {
return dispatch => {
setTimeout(() => {
const devicePixelRatio = window.devicePixelRatio;
const fontSmoothing = devicePixelRatio < 2 ?
'subpixel-antialiased' :
'antialiased';
const fontSmoothing = devicePixelRatio < 2 ? 'subpixel-antialiased' : 'antialiased';
dispatch({
type: UI_FONT_SMOOTHING_SET,
@ -100,11 +114,7 @@ const findChildSessions = (termGroups, uid) => {
return [uid];
}
return group
.children
.reduce((total, childUid) => total.concat(
findChildSessions(termGroups, childUid)
), []);
return group.children.reduce((total, childUid) => total.concat(findChildSessions(termGroups, childUid)), []);
};
// Get the index of the next or previous group,
@ -126,6 +136,7 @@ function moveToNeighborPane(type) {
const {uid} = findBySession(termGroups, sessions.activeUid);
const childGroups = findChildSessions(termGroups.termGroups, termGroups.activeRootGroup);
if (childGroups.length === 1) {
//eslint-disable-next-line no-console
console.log('ignoring move for single group');
} else {
const index = getNeighborIndex(childGroups, uid, type);
@ -156,6 +167,7 @@ export function moveLeft() {
const index = groupUids.indexOf(uid);
const next = groupUids[index - 1] || last(groupUids);
if (!next || uid === next) {
//eslint-disable-next-line no-console
console.log('ignoring left move action');
} else {
dispatch(setActiveGroup(next));
@ -176,6 +188,7 @@ export function moveRight() {
const index = groupUids.indexOf(uid);
const next = groupUids[index + 1] || groupUids[0];
if (!next || uid === next) {
//eslint-disable-next-line no-console
console.log('ignoring right move action');
} else {
dispatch(setActiveGroup(next));
@ -187,6 +200,14 @@ export function moveRight() {
export function moveTo(i) {
return (dispatch, getState) => {
if (i === 'last') {
// Finding last tab index
const {termGroups} = getState().termGroups;
i =
Object.keys(termGroups)
.map(uid => termGroups[uid])
.filter(({parentUid}) => !parentUid).length - 1;
}
dispatch({
type: UI_MOVE_TO,
index: i,
@ -195,10 +216,12 @@ export function moveTo(i) {
const groupUids = getGroupUids(state);
const uid = state.termGroups.activeRootGroup;
if (uid === groupUids[i]) {
//eslint-disable-next-line no-console
console.log('ignoring same uid');
} else if (groupUids[i]) {
dispatch(setActiveGroup(groupUids[i]));
} else {
//eslint-disable-next-line no-console
console.log('ignoring inexistent index', i);
}
}
@ -235,6 +258,7 @@ export function openFile(path) {
effect() {
stat(path, (err, stats) => {
if (err) {
//eslint-disable-next-line no-console
console.error(err.stack);
notify('Unable to open path', `"${path}" doesn't exist.`);
} else {
@ -256,3 +280,42 @@ export function openFile(path) {
});
};
}
export function openSSH(url) {
return dispatch => {
dispatch({
type: UI_OPEN_SSH_URL,
effect() {
let parsedUrl = parseUrl(url, true);
let command = parsedUrl.protocol + ' ' + (parsedUrl.user || '') + '@' + parsedUrl.resource;
if (parsedUrl.port) command += ' -p ' + parsedUrl.port;
command += '\n';
rpc.once('session add', ({uid}) => {
rpc.once('session data', () => {
dispatch(sendSessionData(uid, command));
});
});
dispatch(requestSession());
}
});
};
}
export function execCommand(command, fn, e) {
return dispatch =>
dispatch({
type: UI_COMMAND_EXEC,
command,
effect() {
if (fn) {
fn(e);
} else {
rpc.emit('command', command);
}
}
});
}

View file

@ -1,7 +1,4 @@
import {
UPDATE_INSTALL,
UPDATE_AVAILABLE
} from '../constants/updater';
import {UPDATE_INSTALL, UPDATE_AVAILABLE} from '../constants/updater';
import rpc from '../rpc';
export function installUpdate() {
@ -13,10 +10,12 @@ export function installUpdate() {
};
}
export function updateAvailable(version, notes) {
export function updateAvailable(version, notes, releaseUrl, canInstall) {
return {
type: UPDATE_AVAILABLE,
version,
notes
notes,
releaseUrl,
canInstall
};
}

View file

@ -1,23 +1,46 @@
const commands = {};
import {remote} from 'electron';
class CommandRegistry {
register(cmds) {
if (cmds) {
for (const command in cmds) {
if (command) {
commands[command] = cmds[command];
}
}
}
const {getDecoratedKeymaps} = remote.require('./plugins');
let commands = {};
export const getRegisteredKeys = () => {
const keymaps = getDecoratedKeymaps();
return Object.keys(keymaps).reduce((result, actionName) => {
const commandKeys = keymaps[actionName];
commandKeys.forEach(shortcut => {
result[shortcut] = actionName;
});
return result;
}, {});
};
export const registerCommandHandlers = cmds => {
if (!cmds) {
return;
}
getCommand(cmd) {
return commands[cmd] !== undefined;
}
commands = Object.assign(commands, cmds);
};
exec(cmd, e) {
commands[cmd](e);
}
}
export const getCommandHandler = command => {
return commands[command];
};
export default new CommandRegistry();
// Some commands are directly excuted by Electron menuItem role.
// They should not be prevented to reach Electron.
const roleCommands = [
'window:close',
'editor:undo',
'editor:redo',
'editor:cut',
'editor:copy',
'editor:paste',
'editor:selectAll',
'window:minimize',
'window:zoom',
'window:toggleFullScreen'
];
export const shouldPreventDefault = command => !roleCommands.includes(command);

View file

@ -1,68 +0,0 @@
import React from 'react';
import {StyleSheet, css} from 'aphrodite-simple';
export default class Component extends React.PureComponent {
constructor() {
super();
this.styles_ = this.createStyleSheet();
this.cssHelper = this.cssHelper.bind(this);
}
createStyleSheet() {
if (!this.styles) {
return {};
}
const styles = this.styles();
if (typeof styles !== 'object') {
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 agent extensions
// - the user doesn't need to keep track of both `css`
// and `style`, and we make that whole ordeal 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])];
}
return null;
})
// skip nulls
.filter(v => Boolean(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);
}
}

View file

@ -1,14 +1,12 @@
import React from 'react';
import Component from '../component';
import {decorate, getTabsProps} from '../utils/plugins';
import Tabs_ from './tabs';
const Tabs = decorate(Tabs_, 'Tabs');
export default class Header extends Component {
export default class Header extends React.PureComponent {
constructor() {
super();
this.onChangeIntent = this.onChangeIntent.bind(this);
@ -22,8 +20,7 @@ export default class Header extends Component {
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) {
if (window.screenX !== this.headerMouseDownWindowX || window.screenY !== this.headerMouseDownWindowY) {
return;
}
@ -74,11 +71,11 @@ export default class Header extends Component {
const {showHamburgerMenu, showWindowControls} = this.props;
const defaults = {
hambMenu: process.platform === 'win32', // show by default on windows
winCtrls: !this.props.isMac // show by default on windows and linux
hambMenu: !this.props.isMac, // show by default on windows and linux
winCtrls: !this.props.isMac // show by default on Windows and Linux
};
// don't allow the user to change defaults on MacOS
// don't allow the user to change defaults on macOS
if (this.props.isMac) {
return defaults;
}
@ -89,7 +86,7 @@ export default class Header extends Component {
};
}
template(css) {
render() {
const {isMac} = this.props;
const props = getTabsProps(this.props, {
tabs: this.props.tabs,
@ -105,148 +102,160 @@ export default class Header extends Component {
}
const {hambMenu, winCtrls} = this.getWindowHeaderConfig();
const left = winCtrls === 'left';
const maxButtonHref = this.props.maximized ?
'./renderer/assets/icons.svg#restore-window' :
'./renderer/assets/icons.svg#maximize-window';
const maxButtonHref = this.props.maximized
? './renderer/assets/icons.svg#restore-window'
: './renderer/assets/icons.svg#maximize-window';
return (<header
className={css('header', isMac && 'headerRounded')}
onMouseDown={this.handleHeaderMouseDown}
onDoubleClick={this.handleMaximizeClick}
return (
<header
className={`header_header ${isMac && 'header_headerRounded'}`}
onMouseDown={this.handleHeaderMouseDown}
onDoubleClick={this.handleMaximizeClick}
>
{
!isMac &&
<div
className={css('windowHeader', props.tabs.length > 1 && 'windowHeaderWithBorder')}
style={{borderColor}}
{!isMac && (
<div
className={`header_windowHeader ${props.tabs.length > 1 ? 'header_windowHeaderWithBorder' : ''}`}
style={{borderColor}}
>
{
hambMenu &&
<svg
className={css('shape', (left && 'hamburgerMenuRight') || 'hamburgerMenuLeft')}
onClick={this.handleHamburgerMenuClick}
{hambMenu && (
<svg
className={`header_shape ${left ? 'header_hamburgerMenuRight' : 'header_hamburgerMenuLeft'}`}
onClick={this.handleHamburgerMenuClick}
>
<use xlinkHref="./renderer/assets/icons.svg#hamburger-menu"/>
</svg>
<use xlinkHref="./renderer/assets/icons.svg#hamburger-menu" />
</svg>
)}
<span className="header_appTitle">{title}</span>
{winCtrls && (
<div className={`header_windowControls ${left ? 'header_windowControlsLeft' : ''}`}>
<svg
className={`header_shape ${left ? 'header_minimizeWindowLeft' : ''}`}
onClick={this.handleMinimizeClick}
>
<use xlinkHref="./renderer/assets/icons.svg#minimize-window" />
</svg>
<svg
className={`header_shape ${left ? 'header_maximizeWindowLeft' : ''}`}
onClick={this.handleMaximizeClick}
>
<use xlinkHref={maxButtonHref} />
</svg>
<svg
className={`header_shape header_closeWindow ${left ? 'header_closeWindowLeft' : ''}`}
onClick={this.handleCloseClick}
>
<use xlinkHref="./renderer/assets/icons.svg#close-window" />
</svg>
</div>
)}
</div>
)}
{this.props.customChildrenBefore}
<Tabs {...props} />
{this.props.customChildren}
<style jsx>{`
.header_header {
position: fixed;
top: 1px;
left: 1px;
right: 1px;
z-index: 100;
}
<span className={css('appTitle')}>{title}</span>
{
winCtrls &&
<div className={css('windowControls', left && 'windowControlsLeft')}>
<svg
className={css('shape', left && 'minimizeWindowLeft')}
onClick={this.handleMinimizeClick}
>
<use xlinkHref="./renderer/assets/icons.svg#minimize-window"/>
</svg>
<svg
className={css('shape', left && 'maximizeWindowLeft')}
onClick={this.handleMaximizeClick}
>
<use xlinkHref={maxButtonHref}/>
</svg>
<svg
className={css('shape', 'closeWindow', left && 'closeWindowLeft')}
onClick={this.handleCloseClick}
>
<use xlinkHref="./renderer/assets/icons.svg#close-window"/>
</svg>
</div>
.header_headerRounded {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
</div>
}
{ this.props.customChildrenBefore }
<Tabs {...props}/>
{ this.props.customChildren }
</header>);
.header_windowHeader {
height: 34px;
width: 100%;
position: fixed;
top: 1px;
left: 1px;
right: 1px;
-webkit-app-region: drag;
-webkit-user-select: none;
display: flex;
justify-content: center;
align-items: center;
}
.header_windowHeaderWithBorder {
border-color: #ccc;
border-bottom-style: solid;
border-bottom-width: 1px;
}
.header_appTitle {
font-size: 12px;
}
.header_shape {
width: 40px;
height: 34px;
padding: 12px 15px 12px 15px;
-webkit-app-region: no-drag;
color: #fff;
opacity: 0.5;
shape-rendering: crispEdges;
}
.header_shape:hover {
opacity: 1;
}
.header_shape:active {
opacity: 0.3;
}
.header_hamburgerMenuLeft {
position: fixed;
top: 0;
left: 0;
}
.header_hamburgerMenuRight {
position: fixed;
top: 0;
right: 0;
}
.header_windowControls {
display: flex;
width: 120px;
height: 34px;
justify-content: space-between;
position: fixed;
right: 0;
}
.header_windowControlsLeft {
left: 0px;
}
.header_closeWindowLeft {
order: 1;
}
.header_minimizeWindowLeft {
order: 2;
}
.header_maximizeWindowLeft {
order: 3;
}
.header_closeWindow:hover {
color: #fe354e;
}
.header_closeWindow:active {
color: #fe354e;
}
`}</style>
</header>
);
}
styles() {
return {
header: {
position: 'fixed',
top: '1px',
left: '1px',
right: '1px',
zIndex: '100'
},
headerRounded: {
borderTopLeftRadius: '4px',
borderTopRightRadius: '4px'
},
windowHeader: {
height: '34px',
width: '100%',
position: 'fixed',
top: '1px',
left: '1px',
right: '1px',
WebkitAppRegion: 'drag',
WebkitUserSelect: 'none',
display: 'flex',
justifyContent: 'center',
alignItems: 'center'
},
windowHeaderWithBorder: {
borderColor: '#ccc',
borderBottomStyle: 'solid',
borderBottomWidth: '1px'
},
appTitle: {
fontSize: '12px'
},
shape: {
width: '40px',
height: '34px',
padding: '12px 15px 12px 15px',
WebkitAppRegion: 'no-drag',
color: '#FFFFFF',
opacity: 0.5,
shapeRendering: 'crispEdges',
':hover': {
opacity: 1
},
':active': {
opacity: 0.3
}
},
hamburgerMenuLeft: {
position: 'fixed',
top: '0',
left: '0'
},
hamburgerMenuRight: {
position: 'fixed',
top: '0',
right: '0'
},
windowControls: {
display: 'flex',
width: '120px',
height: '34px',
justifyContent: 'space-between',
position: 'fixed',
right: '0'
},
windowControlsLeft: {left: '0px'},
closeWindowLeft: {order: 1},
minimizeWindowLeft: {order: 2},
maximizeWindowLeft: {order: 3},
closeWindow: {':hover': {color: '#FE354E'}, ':active': {color: '#FE354E'}}
};
}
}

View file

@ -1,8 +1,6 @@
import React from 'react';
import Component from '../component';
export default class Notification extends Component {
export default class Notification extends React.PureComponent {
constructor() {
super();
this.state = {
@ -44,11 +42,7 @@ export default class Notification extends Component {
});
const {backgroundColor} = this.props;
if (backgroundColor) {
el.style.setProperty(
'background-color',
backgroundColor,
'important'
);
el.style.setProperty('background-color', backgroundColor, 'important');
}
}
}
@ -68,58 +62,53 @@ export default class Notification extends Component {
clearTimeout(this.dismissTimer);
}
template(css) {
render() {
const {backgroundColor} = this.props;
const opacity = this.state.dismissing ? 0 : 1;
return (<div
ref={this.onElement}
style={{opacity, backgroundColor}}
className={css('indicator')}
>
{ this.props.customChildrenBefore }
{ this.props.children || this.props.text }
{
this.props.userDismissable ?
return (
<div ref={this.onElement} style={{opacity, backgroundColor}} className="notification_indicator">
{this.props.customChildrenBefore}
{this.props.children || this.props.text}
{this.props.userDismissable ? (
<a
className={css('dismissLink')}
className="notification_dismissLink"
onClick={this.handleDismiss}
style={{color: this.props.userDismissColor}}
>[x]</a> :
null
}
{ this.props.customChildren }
</div>);
>
[x]
</a>
) : null}
{this.props.customChildren}
<style jsx>{`
.notification_indicator {
display: inline-block;
cursor: default;
-webkit-user-select: none;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
padding: 8px 14px 9px;
margin-left: 10px;
transition: 150ms opacity ease;
color: #fff;
font-size: 11px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
}
.notification_dismissLink {
position: relative;
left: 4px;
cursor: pointer;
color: #528d11;
}
.notification_dismissLink:hover,
.notification_dismissLink:focus {
color: #2a5100;
}
`}</style>
</div>
);
}
styles() {
return {
indicator: {
display: 'inline-block',
cursor: 'default',
WebkitUserSelect: 'none',
background: 'rgba(255, 255, 255, .2)',
borderRadius: '2px',
padding: '8px 14px 9px',
marginLeft: '10px',
transition: '150ms opacity ease',
color: '#fff',
fontSize: '11px',
fontFamily: `-apple-system, BlinkMacSystemFont,
"Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif`
},
dismissLink: {
position: 'relative',
left: '4px',
cursor: 'pointer',
color: '#528D11',
':hover': {
color: '#2A5100'
}
}
};
}
}

View file

@ -1,19 +1,17 @@
import React from 'react';
import Component from '../component';
import {decorate} from '../utils/plugins';
import Notification_ from './notification';
const Notification = decorate(Notification_);
const Notification = decorate(Notification_, 'Notification');
export default class Notifications extends Component {
template(css) {
return (<div className={css('view')}>
{ this.props.customChildrenBefore }
{
this.props.fontShowing &&
export default class Notifications extends React.PureComponent {
render() {
return (
<div className="notifications_view">
{this.props.customChildrenBefore}
{this.props.fontShowing && (
<Notification
key="font"
backgroundColor="rgba(255, 255, 255, .2)"
@ -21,11 +19,10 @@ export default class Notifications extends Component {
userDismissable={false}
onDismiss={this.props.onDismissFont}
dismissAfter={1000}
/>
}
/>
)}
{
this.props.resizeShowing &&
{this.props.resizeShowing && (
<Notification
key="resize"
backgroundColor="rgba(255, 255, 255, .2)"
@ -33,11 +30,10 @@ export default class Notifications extends Component {
userDismissable={false}
onDismiss={this.props.onDismissResize}
dismissAfter={1000}
/>
}
/>
)}
{
this.props.messageShowing &&
{this.props.messageShowing && (
<Notification
key="message"
backgroundColor="#FE354E"
@ -45,71 +41,87 @@ export default class Notifications extends Component {
onDismiss={this.props.onDismissMessage}
userDismissable={this.props.messageDismissable}
userDismissColor="#AA2D3C"
>{
this.props.messageURL ? [
this.props.messageText,
' (',
<a
key="link"
style={{color: '#fff'}}
onClick={ev => {
window.require('electron').shell.openExternal(ev.target.href);
ev.preventDefault();
}}
href={this.props.messageURL}
>more</a>,
')'
] : null
}
>
{this.props.messageURL
? [
this.props.messageText,
' (',
<a
key="link"
style={{color: '#fff'}}
onClick={ev => {
window.require('electron').shell.openExternal(ev.target.href);
ev.preventDefault();
}}
href={this.props.messageURL}
>
more
</a>,
')'
]
: null}
</Notification>
}
)}
{
this.props.updateShowing &&
{this.props.updateShowing && (
<Notification
key="update"
backgroundColor="#7ED321"
text={`Version ${this.props.updateVersion} ready`}
onDismiss={this.props.onDismissUpdate}
userDismissable
>
>
Version <b>{this.props.updateVersion}</b> ready.
{this.props.updateNote && ` ${this.props.updateNote.trim().replace(/\.$/, '')}`}
{' '}
(<a
{this.props.updateNote && ` ${this.props.updateNote.trim().replace(/\.$/, '')}`} (<a
style={{color: '#fff'}}
onClick={ev => {
window.require('electron').shell.openExternal(ev.target.href);
ev.preventDefault();
}}
href={`https://github.com/zeit/hyper/releases/tag/${this.props.updateVersion}`}
>notes</a>).
{' '}
<a
style={{
cursor: 'pointer',
textDecoration: 'underline',
fontWeight: 'bold'
}}
onClick={this.props.onUpdateInstall}
>
notes
</a>).{' '}
{this.props.updateCanInstall ? (
<a
style={{
cursor: 'pointer',
textDecoration: 'underline',
fontWeight: 'bold'
}}
onClick={this.props.onUpdateInstall}
>
Restart
</a>.
{ ' ' }
</a>
) : (
<a
style={{
color: '#fff',
cursor: 'pointer',
textDecoration: 'underline',
fontWeight: 'bold'
}}
onClick={ev => {
window.require('electron').shell.openExternal(ev.target.href);
ev.preventDefault();
}}
href={this.props.updateReleaseUrl}
>
Download
</a>
)}.{' '}
</Notification>
}
{ this.props.customChildren }
</div>);
}
)}
{this.props.customChildren}
styles() {
return {
view: {
position: 'fixed',
bottom: '20px',
right: '20px'
}
};
<style jsx>{`
.notifications_view {
position: fixed;
bottom: 20px;
right: 20px;
}
`}</style>
</div>
);
}
}

View file

@ -1,12 +1,12 @@
/* eslint-disable quote-props */
import React from 'react';
import Component from '../component';
export default class SplitPane extends Component {
import _ from 'lodash';
export default class SplitPane extends React.PureComponent {
constructor(props) {
super(props);
this.handleDragStart = this.handleDragStart.bind(this);
this.handleAutoResize = this.handleAutoResize.bind(this);
this.onDrag = this.onDrag.bind(this);
this.onDragEnd = this.onDragEnd.bind(this);
this.state = {dragging: false};
@ -19,6 +19,28 @@ export default class SplitPane extends Component {
}
}
setupPanes(ev) {
this.panes = Array.from(ev.target.parentNode.childNodes);
this.paneIndex = this.panes.indexOf(ev.target);
this.paneIndex -= Math.ceil(this.paneIndex / 2);
}
handleAutoResize(ev) {
ev.preventDefault();
this.setupPanes(ev);
const sizes_ = this.getSizes();
sizes_[this.paneIndex] = 0;
sizes_[this.paneIndex + 1] = 0;
const availableWidth = 1 - _.sum(sizes_);
sizes_[this.paneIndex] = availableWidth / 2;
sizes_[this.paneIndex + 1] = availableWidth / 2;
this.props.onResize(sizes_);
}
handleDragStart(ev) {
ev.preventDefault();
this.setState({dragging: true});
@ -38,14 +60,12 @@ export default class SplitPane extends Component {
this.dragTarget = ev.target;
this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2];
this.panes = Array.from(ev.target.parentNode.childNodes);
this.panesSize = ev.target.parentNode.getBoundingClientRect()[this.d1];
this.paneIndex = this.panes.indexOf(ev.target);
this.paneIndex -= Math.ceil(this.paneIndex / 2);
this.setupPanes(ev);
}
onDrag(ev) {
let {sizes} = this.props;
getSizes() {
const {sizes} = this.props;
let sizes_;
if (sizes) {
@ -54,9 +74,13 @@ export default class SplitPane extends Component {
const total = this.props.children.length;
const count = new Array(total).fill(1 / total);
sizes = count;
sizes_ = count;
}
return sizes_;
}
onDrag(ev) {
const sizes_ = this.getSizes();
const i = this.paneIndex;
const pos = ev[this.d3];
@ -79,9 +103,10 @@ export default class SplitPane extends Component {
}
}
template(css) {
render() {
const children = this.props.children;
const {direction, borderColor} = this.props;
const sizeProperty = direction === 'horizontal' ? 'height' : 'width';
let {sizes} = this.props;
if (!sizes) {
// workaround for the fact that if we don't specify
@ -89,101 +114,96 @@ export default class SplitPane extends Component {
// right height for the horizontal panes
sizes = new Array(children.length).fill(1 / children.length);
}
return (<div className={css('panes', `panes_${direction}`)}>
{
React.Children.map(children, (child, i) => {
return (
<div className={`splitpane_panes splitpane_panes_${direction}`}>
{React.Children.map(children, (child, i) => {
const style = {
flexBasis: (sizes[i] * 100) + '%',
// flexBasis doesn't work for the first horizontal pane, height need to be specified
[sizeProperty]: sizes[i] * 100 + '%',
flexBasis: sizes[i] * 100 + '%',
flexGrow: 0
};
return [
<div
key="pane"
className={css('pane')}
style={style}
>
{ child }
<div key="pane" className="splitpane_pane" style={style}>
{child}
</div>,
i < children.length - 1 ?
i < children.length - 1 ? (
<div
key="divider"
onMouseDown={this.handleDragStart}
onDoubleClick={this.handleAutoResize}
style={{backgroundColor: borderColor}}
className={css('divider', `divider_${direction}`)}
/> :
null
className={`splitpane_divider splitpane_divider_${direction}`}
/>
) : null
];
})
}
<div
style={{display: this.state.dragging ? 'block' : 'none'}}
className={css('shim')}
/>
</div>);
}
})}
<div style={{display: this.state.dragging ? 'block' : 'none'}} className="splitpane_shim" />
styles() {
return {
panes: {
display: 'flex',
flex: 1,
outline: 'none',
position: 'relative',
width: '100%',
height: '100%'
},
<style jsx>{`
.splitpane_panes {
display: flex;
flex: 1;
outline: none;
position: relative;
width: 100%;
height: 100%;
}
'panes_vertical': {
flexDirection: 'row'
},
.splitpane_panes_vertical {
flex-direction: row;
}
'panes_horizontal': {
flexDirection: 'column'
},
.splitpane_panes_horizontal {
flex-direction: column;
}
pane: {
flex: 1,
outline: 'none',
position: 'relative'
},
.splitpane_pane {
flex: 1;
outline: none;
position: relative;
}
divider: {
boxSizing: 'border-box',
zIndex: '1',
backgroundClip: 'padding-box',
flexShrink: 0
},
.splitpane_divider {
box-sizing: border-box;
z-index: 1;
background-clip: padding-box;
flex-shrink: 0;
}
'divider_vertical': {
borderLeft: '5px solid rgba(255, 255, 255, 0)',
borderRight: '5px solid rgba(255, 255, 255, 0)',
width: '11px',
margin: '0 -5px',
cursor: 'col-resize'
},
.splitpane_divider_vertical {
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
width: 11px;
margin: 0 -5px;
cursor: col-resize;
}
'divider_horizontal': {
height: '11px',
margin: '-5px 0',
borderTop: '5px solid rgba(255, 255, 255, 0)',
borderBottom: '5px solid rgba(255, 255, 255, 0)',
cursor: 'row-resize',
width: '100%'
},
.splitpane_divider_horizontal {
height: 11px;
margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
// this shim is used to make sure mousemove events
// trigger in all the draggable area of the screen
//
// this is not the case due to hterm's <iframe>
shim: {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'transparent'
}
};
/*
this shim is used to make sure mousemove events
trigger in all the draggable area of the screen
this is not the case due to hterm's <iframe>
*/
.splitpane_shim {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent;
}
`}</style>
</div>
);
}
componentWillUnmount() {
@ -192,5 +212,4 @@ export default class SplitPane extends Component {
this.onDragEnd();
}
}
}

View file

@ -0,0 +1,144 @@
import React from 'react';
export default class StyleSheet extends React.PureComponent {
render() {
const {backgroundColor, fontFamily, foregroundColor, borderColor} = this.props;
return (
<style jsx global>{`
.xterm {
font-family: ${fontFamily};
font-feature-settings: 'liga' 0;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 10;
}
.xterm .xterm-helper-textarea {
/*
* HACK: to fix IE's blinking cursor
* Move textarea out of the screen to the far left, so that the cursor is not visible.
*/
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -10;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: ${backgroundColor};
color: ${foregroundColor};
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: ${backgroundColor};
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm .xterm-scroll-area {
visibility: hidden;
}
.xterm .xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
left: -9999em;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.xterm:not(.enable-mouse-events) {
cursor: text;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 100;
color: transparent;
}
.xterm .xterm-accessibility-tree:focus [id^='xterm-active-item-'] {
outline: 1px solid #f80;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-thumb {
-webkit-border-radius: 10px;
border-radius: 10px;
background: ${borderColor};
}
::-webkit-scrollbar-thumb:window-inactive {
background: ${borderColor};
}
`}</style>
);
}
}

View file

@ -1,7 +1,6 @@
import React from 'react';
import Component from '../component';
export default class Tab extends Component {
export default class Tab extends React.PureComponent {
constructor() {
super();
@ -14,10 +13,6 @@ export default class Tab extends Component {
};
}
shouldComponentUpdate() {
return true;
}
handleHover() {
this.setState({
hovered: true
@ -41,160 +36,145 @@ export default class Tab extends Component {
}
}
template(css) {
render() {
const {isActive, isFirst, isLast, borderColor, hasActivity} = this.props;
const {hovered} = this.state;
return (<li
onMouseEnter={this.handleHover}
onMouseLeave={this.handleBlur}
onClick={this.props.onClick}
style={{borderColor}}
className={css(
'tab',
isFirst && 'first',
isActive && 'active',
isFirst && isActive && 'firstActive',
hasActivity && 'hasActivity'
)}
>
{ this.props.customChildrenBefore }
<span
className={css(
'text',
isLast && 'textLast',
isActive && 'textActive'
)}
onClick={this.handleClick}
return (
<React.Fragment>
<li
onMouseEnter={this.handleHover}
onMouseLeave={this.handleBlur}
onClick={this.props.onClick}
style={{borderColor}}
className={`tab_tab ${isFirst ? 'tab_first' : ''} ${isActive ? 'tab_active' : ''} ${
isFirst && isActive ? 'tab_firstActive' : ''
} ${hasActivity ? 'tab_hasActivity' : ''}`}
>
<span
title={this.props.text}
className={css('textInner')}
{this.props.customChildrenBefore}
<span
className={`tab_text ${isLast ? 'tab_textLast' : ''} ${isActive ? 'tab_textActive' : ''}`}
onClick={this.handleClick}
>
{ this.props.text }
</span>
</span>
<i
className={css(
'icon',
hovered && 'iconHovered'
)}
onClick={this.props.onClose}
>
<svg className={css('shape')}>
<use xlinkHref="./renderer/assets/icons.svg#close-tab"/>
</svg>
</i>
{ this.props.customChildren }
</li>);
<span title={this.props.text} className="tab_textInner">
{this.props.text}
</span>
</span>
<i className={`tab_icon ${hovered ? 'tab_iconHovered' : ''}`} onClick={this.props.onClose}>
<svg className="tab_shape">
<use xlinkHref="./renderer/assets/icons.svg#close-tab" />
</svg>
</i>
{this.props.customChildren}
</li>
<style jsx>{`
.tab_tab {
color: #ccc;
border-color: #ccc;
border-bottom-width: 1px;
border-bottom-style: solid;
border-left-width: 1px;
border-left-style: solid;
list-style-type: none;
flex-grow: 1;
position: relative;
}
.tab_tab:hover {
color: #ccc;
}
.tab_first {
border-left-width: 0;
padding-left: 1px;
}
.tab_firstActive {
border-left-width: 1px;
padding-left: 0;
}
.tab_active {
color: #fff;
border-bottom-width: 0;
}
.tab_active:hover {
color: #fff;
}
.tab_hasActivity {
color: #50e3c2;
}
.tab_hasActivity:hover {
color: #50e3c2;
}
.tab_text {
transition: color 0.2s ease;
height: 34px;
display: block;
width: 100%;
position: relative;
overflow: hidden;
}
.tab_textInner {
position: absolute;
left: 24px;
right: 24px;
top: 0;
bottom: 0;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.tab_icon {
transition: opacity 0.2s ease, color 0.2s ease, transform 0.25s ease, background-color 0.1s ease;
pointer-events: none;
position: absolute;
right: 7px;
top: 10px;
display: inline-block;
width: 14px;
height: 14px;
border-radius: 100%;
color: #e9e9e9;
opacity: 0;
transform: scale(0.95);
}
.tab_icon:hover {
background-color: rgba(255, 255, 255, 0.13);
color: #fff;
}
.tab_icon:active {
background-color: rgba(255, 255, 255, 0.1);
color: #909090;
}
.tab_iconHovered {
opacity: 1;
transform: none;
pointer-events: all;
}
.tab_shape {
position: absolute;
left: 4px;
top: 4px;
width: 6px;
height: 6px;
vertical-align: middle;
fill: currentColor;
shape-rendering: crispEdges;
}
`}</style>
</React.Fragment>
);
}
styles() {
return {
tab: {
color: '#ccc',
borderColor: '#ccc',
borderBottomWidth: 1,
borderBottomStyle: 'solid',
borderLeftWidth: 1,
borderLeftStyle: 'solid',
listStyleType: 'none',
flexGrow: 1,
position: 'relative',
':hover': {
color: '#ccc'
}
},
first: {
borderLeftWidth: 0,
paddingLeft: 1
},
firstActive: {
borderLeftWidth: 1,
paddingLeft: 0
},
active: {
color: '#fff',
borderBottomWidth: 0,
':hover': {
color: '#fff'
}
},
hasActivity: {
color: '#50E3C2',
':hover': {
color: '#50E3C2'
}
},
text: {
transition: 'color .2s ease',
height: '34px',
display: 'block',
width: '100%',
position: 'relative',
overflow: 'hidden'
},
textInner: {
position: 'absolute',
left: '24px',
right: '24px',
top: 0,
bottom: 0,
textAlign: 'center',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden'
},
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',
shapeRendering: 'crispEdges'
}
};
}
}

View file

@ -1,6 +1,5 @@
import React from 'react';
import Component from '../component';
import {decorate, getTabProps} from '../utils/plugins';
import Tab_ from './tab';
@ -8,106 +7,84 @@ import Tab_ from './tab';
const Tab = decorate(Tab_, 'Tab');
const isMac = /Mac/.test(navigator.userAgent);
export default class Tabs extends Component {
template(css) {
const {
tabs = [],
borderColor,
onChange,
onClose
} = this.props;
export default class Tabs extends React.PureComponent {
render() {
const {tabs = [], borderColor, onChange, onClose} = this.props;
const hide = !isMac && tabs.length === 1;
return (<nav className={css('nav', hide && 'hiddenNav')}>
{ this.props.customChildrenBefore }
{
tabs.length === 1 && isMac ?
<div className={css('title')}>{tabs[0].title}</div> :
null
}
{
tabs.length > 1 ?
[
<ul
key="list"
className={css('list')}
>
{
tabs.map((tab, i) => {
const {uid, title, isActive, hasActivity} = tab;
const props = getTabProps(tab, this.props, {
text: title === '' ? 'Shell' : title,
isFirst: i === 0,
isLast: tabs.length - 1 === i,
borderColor,
isActive,
hasActivity,
onSelect: onChange.bind(null, uid),
onClose: onClose.bind(null, uid)
});
return <Tab key={`tab-${uid}`} {...props}/>;
})
}
</ul>,
isMac && <div
key="shim"
style={{borderColor}}
className={css('borderShim')}
/>
] :
null
}
{ this.props.customChildren }
</nav>);
return (
<nav className={`tabs_nav ${hide ? 'tabs_hiddenNav' : ''}`}>
{this.props.customChildrenBefore}
{tabs.length === 1 && isMac ? <div className="tabs_title">{tabs[0].title}</div> : null}
{tabs.length > 1
? [
<ul key="list" className="tabs_list">
{tabs.map((tab, i) => {
const {uid, title, isActive, hasActivity} = tab;
const props = getTabProps(tab, this.props, {
text: title === '' ? 'Shell' : title,
isFirst: i === 0,
isLast: tabs.length - 1 === i,
borderColor,
isActive,
hasActivity,
onSelect: onChange.bind(null, uid),
onClose: onClose.bind(null, uid)
});
return <Tab key={`tab-${uid}`} {...props} />;
})}
</ul>,
isMac && <div key="shim" style={{borderColor}} className="tabs_borderShim" />
]
: null}
{this.props.customChildren}
<style jsx>{`
.tabs_nav {
font-size: 12px;
height: 34px;
line-height: 34px;
vertical-align: middle;
color: #9b9b9b;
cursor: default;
position: relative;
-webkit-user-select: none;
-webkit-app-region: ${isMac ? 'drag' : ''};
top: ${isMac ? '0px' : '34px'};
}
.tabs_hiddenNav {
display: none;
}
.tabs_title {
text-align: center;
color: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding-left: 76px;
padding-right: 76px;
}
.tabs_list {
max-height: 34px;
display: flex;
flex-flow: row;
margin-left: ${isMac ? '76px' : '0'};
}
.tabs_borderShim {
position: absolute;
width: 76px;
bottom: 0;
border-color: #ccc;
border-bottom-style: solid;
border-bottom-width: 1px;
}
`}</style>
</nav>
);
}
styles() {
return {
nav: {
fontSize: '12px',
height: '34px',
lineHeight: '34px',
verticalAlign: 'middle',
color: '#9B9B9B',
cursor: 'default',
position: 'relative',
WebkitUserSelect: 'none',
WebkitAppRegion: isMac ? 'drag' : '',
top: isMac ? '0px' : '34px'
},
hiddenNav: {
display: 'none'
},
title: {
textAlign: 'center',
color: '#fff',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
paddingLeft: 76,
paddingRight: 76
},
list: {
maxHeight: '34px',
display: 'flex',
flexFlow: 'row',
marginLeft: isMac ? 76 : 0
},
borderShim: {
position: 'absolute',
width: '76px',
bottom: 0,
borderColor: '#ccc',
borderBottomStyle: 'solid',
borderBottomWidth: '1px'
}
};
}
}

View file

@ -1,6 +1,5 @@
import React from 'react';
import {connect} from 'react-redux';
import Component from '../component';
import {decorate, getTermProps, getTermGroupProps} from '../utils/plugins';
import {resizeTermGroup} from '../actions/term-groups';
import Term_ from './term';
@ -9,11 +8,13 @@ import SplitPane_ from './split-pane';
const Term = decorate(Term_, 'Term');
const SplitPane = decorate(SplitPane_, 'SplitPane');
class TermGroup_ extends Component {
class TermGroup_ extends React.PureComponent {
constructor(props, context) {
super(props, context);
this.bound = new WeakMap();
this.termRefs = {};
this.sizeChanged = false;
this.onTermRef = this.onTermRef.bind(this);
}
bind(fn, thisObj, uid) {
@ -34,14 +35,21 @@ class TermGroup_ extends Component {
}
const direction = this.props.termGroup.direction.toLowerCase();
return (<SplitPane
direction={direction}
sizes={this.props.termGroup.sizes}
onResize={this.props.onTermGroupResize}
borderColor={this.props.borderColor}
return (
<SplitPane
direction={direction}
sizes={this.props.termGroup.sizes}
onResize={this.props.onTermGroupResize}
borderColor={this.props.borderColor}
>
{ groups }
</SplitPane>);
{groups}
</SplitPane>
);
}
onTermRef(uid, term) {
this.term = term;
this.props.ref_(uid, term);
}
renderTerm(uid) {
@ -50,19 +58,21 @@ class TermGroup_ extends Component {
const props = getTermProps(uid, this.props, {
isTermActive: uid === this.props.activeSession,
term: termRef ? termRef.term : null,
customCSS: this.props.customCSS,
fontSize: this.props.fontSize,
cursorColor: this.props.cursorColor,
cursorShape: this.props.cursorShape,
backgroundColor: this.props.backgroundColor,
foregroundColor: this.props.foregroundColor,
colors: this.props.colors,
cursorBlink: this.props.cursorBlink,
cursorShape: this.props.cursorShape,
cursorColor: this.props.cursorColor,
cursorAccentColor: this.props.cursorAccentColor,
fontSize: this.props.fontSize,
fontFamily: this.props.fontFamily,
uiFontFamily: this.props.uiFontFamily,
fontSmoothing: this.props.fontSmoothing,
foregroundColor: this.props.foregroundColor,
backgroundColor: this.props.backgroundColor,
fontWeight: this.props.fontWeight,
fontWeightBold: this.props.fontWeightBold,
modifierKeys: this.props.modifierKeys,
padding: this.props.padding,
colors: this.props.colors,
url: session.url,
cleared: session.cleared,
cols: session.cols,
@ -75,7 +85,9 @@ class TermGroup_ extends Component {
onTitle: this.bind(this.props.onTitle, null, uid),
onData: this.bind(this.props.onData, null, uid),
onURLAbort: this.bind(this.props.onURLAbort, null, uid),
onContextMenu: this.bind(this.props.onContextMenu, null, uid),
borderColor: this.props.borderColor,
selectionColor: this.props.selectionColor,
quickEdit: this.props.quickEdit,
uid
});
@ -83,28 +95,36 @@ class TermGroup_ extends Component {
// This will create a new ref_ function for every render,
// which is inefficient. Should maybe do something similar
// to this.bind.
return (<Term
key={uid}
ref_={term => this.props.ref_(uid, term)}
{...props}
/>);
return <Term ref_={this.onTermRef} key={uid} {...props} />;
}
template() {
componentWillReceiveProps(nextProps) {
if (this.props.termGroup.sizes != nextProps.termGroup.sizes || nextProps.sizeChanged) {
this.term && this.term.fitResize();
// Indicate to children that their size has changed even if their ratio hasn't
this.sizeChanged = true;
} else {
this.sizeChanged = false;
}
}
render() {
const {childGroups, termGroup} = this.props;
if (termGroup.sessionUid) {
return this.renderTerm(termGroup.sessionUid);
}
const groups = childGroups.map(child => {
const props = getTermGroupProps(child.uid, this.props.parentProps, Object.assign({}, this.props, {
termGroup: child
}));
const props = getTermGroupProps(
child.uid,
this.props.parentProps,
Object.assign({}, this.props, {
termGroup: child,
sizeChanged: this.sizeChanged
})
);
return (<DecoratedTermGroup
key={child.uid}
{...props}
/>);
return <DecoratedTermGroup key={child.uid} {...props} />;
});
return this.renderSplit(groups);
@ -113,9 +133,7 @@ class TermGroup_ extends Component {
const TermGroup = connect(
(state, ownProps) => ({
childGroups: ownProps.termGroup.children.map(uid =>
state.termGroups.termGroups[uid]
)
childGroups: ownProps.termGroup.children.map(uid => state.termGroups.termGroups[uid])
}),
(dispatch, ownProps) => ({
onTermGroupResize(splitSizes) {

View file

@ -1,437 +1,335 @@
/* global Blob,URL,requestAnimationFrame */
import React from 'react';
import Color from 'color';
import uuid from 'uuid';
import hterm from '../hterm';
import Component from '../component';
import getColorList from '../utils/colors';
import {Terminal} from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit';
import * as webLinks from 'xterm/lib/addons/webLinks/webLinks';
import * as winptyCompat from 'xterm/lib/addons/winptyCompat/winptyCompat';
import {clipboard} from 'electron';
import * as Color from 'color';
import terms from '../terms';
import notify from '../utils/notify';
import processClipboard from '../utils/paste';
export default class Term extends Component {
Terminal.applyAddon(fit);
Terminal.applyAddon(webLinks);
Terminal.applyAddon(winptyCompat);
// map old hterm constants to xterm.js
const CURSOR_STYLES = {
BEAM: 'bar',
UNDERLINE: 'underline',
BLOCK: 'block'
};
const getTermOptions = props => {
// Set a background color only if it is opaque
const needTransparency = Color(props.backgroundColor).alpha() < 1;
const backgroundColor = needTransparency ? 'transparent' : props.backgroundColor;
return {
macOptionIsMeta: props.modifierKeys.altIsMeta,
cursorStyle: CURSOR_STYLES[props.cursorShape],
cursorBlink: props.cursorBlink,
fontFamily: props.fontFamily,
fontSize: props.fontSize,
fontWeight: props.fontWeight,
fontWeightBold: props.fontWeightBold,
allowTransparency: needTransparency,
experimentalCharAtlas: 'dynamic',
theme: {
foreground: props.foregroundColor,
background: backgroundColor,
cursor: props.cursorColor,
cursorAccent: props.cursorAccentColor,
selection: props.selectionColor,
black: props.colors.black,
red: props.colors.red,
green: props.colors.green,
yellow: props.colors.yellow,
blue: props.colors.blue,
magenta: props.colors.magenta,
cyan: props.colors.cyan,
white: props.colors.white,
brightBlack: props.colors.lightBlack,
brightRed: props.colors.lightRed,
brightGreen: props.colors.lightGreen,
brightYellow: props.colors.lightYellow,
brightBlue: props.colors.lightBlue,
brightMagenta: props.colors.lightMagenta,
brightCyan: props.colors.lightCyan,
brightWhite: props.colors.lightWhite
}
};
};
export default class Term extends React.PureComponent {
constructor(props) {
super(props);
this.handleWheel = this.handleWheel.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleScrollEnter = this.handleScrollEnter.bind(this);
this.handleScrollLeave = this.handleScrollLeave.bind(this);
this.onHyperCaret = this.onHyperCaret.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleFocus = this.handleFocus.bind(this);
props.ref_(this);
props.ref_(props.uid, this);
this.termRef = null;
this.termWrapperRef = null;
this.termRect = null;
this.onOpen = this.onOpen.bind(this);
this.onWindowResize = this.onWindowResize.bind(this);
this.onWindowPaste = this.onWindowPaste.bind(this);
this.onTermRef = this.onTermRef.bind(this);
this.onTermWrapperRef = this.onTermWrapperRef.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.termOptions = {};
}
componentDidMount() {
const {props} = this;
this.term = props.term || new hterm.Terminal(uuid.v4());
this.term.onHyperCaret(this.hyperCaret);
// the first term that's created has unknown size
// subsequent new tabs have size
if (props.cols && props.rows) {
this.term.realizeSize_(props.cols, props.rows);
this.termOptions = getTermOptions(props);
this.term = props.term || new Terminal(this.termOptions);
this.term.attachCustomKeyEventHandler(this.keyboardHandler);
this.term.open(this.termRef);
this.term.webLinksInit();
this.term.winptyCompatInit();
if (props.term) {
//We need to set options again after reattaching an existing term
Object.keys(this.termOptions).forEach(option => this.term.setOption(option, this.termOptions[option]));
}
if (this.props.isTermActive) {
this.term.focus();
}
const prefs = this.term.getPrefs();
this.onOpen(this.termOptions);
prefs.set('font-family', props.fontFamily);
prefs.set('font-size', props.fontSize);
prefs.set('font-smoothing', props.fontSmoothing);
prefs.set('cursor-color', this.validateColor(props.cursorColor, 'rgba(255,255,255,0.5)'));
prefs.set('cursor-blink', props.cursorBlink);
prefs.set('enable-clipboard-notice', false);
prefs.set('foreground-color', props.foregroundColor);
// hterm.ScrollPort.prototype.setBackgroundColor is overriden
// to make hterm's background transparent. we still need to set
// background-color for proper text rendering
prefs.set('background-color', props.backgroundColor);
prefs.set('color-palette-overrides', getColorList(props.colors));
prefs.set('user-css', this.getStylesheet(props.customCSS));
prefs.set('scrollbar-visible', false);
prefs.set('receive-encoding', 'raw');
prefs.set('send-encoding', 'raw');
prefs.set('alt-sends-what', 'browser-key');
if (props.bell === 'SOUND') {
prefs.set('audible-bell-sound', this.props.bellSoundURL);
} else {
prefs.set('audible-bell-sound', '');
if (props.onTitle) {
this.term.on('title', props.onTitle);
}
if (props.copyOnSelect) {
prefs.set('copy-on-select', true);
} else {
prefs.set('copy-on-select', false);
if (props.onActive) {
this.term.on('focus', props.onActive);
}
this.term.onTerminalReady = () => {
const io = this.term.io.push();
io.onVTKeystroke = props.onData;
io.sendString = props.onData;
io.onTerminalResize = (cols, rows) => {
if (cols !== this.props.cols || rows !== this.props.rows) {
props.onResize(cols, rows);
}
};
this.term.modifierKeys = props.modifierKeys;
// this.term.CursorNode_ is available at this point.
this.term.setCursorShape(props.cursorShape);
// required to be set for CursorBlink to work
this.term.setCursorVisible(true);
// emit onTitle event when hterm instance
// wants to set the title of its tab
this.term.setWindowTitle = props.onTitle;
this.term.focusHyperCaret();
};
this.term.decorate(this.termRef);
this.term.installKeyboard();
if (this.props.onTerminal) {
this.props.onTerminal(this.term);
if (props.onData) {
this.term.on('data', props.onData);
}
const iframeWindow = this.getTermDocument().defaultView;
iframeWindow.addEventListener('wheel', this.handleWheel);
if (props.onResize) {
this.term.on('resize', ({cols, rows}) => {
props.onResize(cols, rows);
});
}
if (props.onCursorMove) {
this.term.on('cursormove', () => {
const cursorFrame = {
x: this.term.buffer.x * this.term.renderer.dimensions.actualCellWidth,
y: this.term.buffer.y * this.term.renderer.dimensions.actualCellHeight,
width: this.term.renderer.dimensions.actualCellWidth,
height: this.term.renderer.dimensions.actualCellHeight,
col: this.term.buffer.y,
row: this.term.buffer.x
};
props.onCursorMove(cursorFrame);
});
}
window.addEventListener('resize', this.onWindowResize, {
passive: true
});
window.addEventListener('paste', this.onWindowPaste, {
capture: true
});
this.getScreenNode().addEventListener('mouseup', this.handleMouseUp);
this.getScreenNode().addEventListener('mousedown', this.handleMouseDown);
terms[this.props.uid] = this;
}
handleWheel(e) {
if (this.props.onWheel) {
this.props.onWheel(e);
}
const prefs = this.term.getPrefs();
prefs.set('scrollbar-visible', true);
clearTimeout(this.scrollbarsHideTimer);
if (!this.scrollMouseEnter) {
this.scrollbarsHideTimer = setTimeout(() => {
prefs.set('scrollbar-visible', false);
}, 1000);
onOpen(termOptions) {
// we need to delay one frame so that styles
// get applied and we can make an accurate measurement
// of the container width and height
requestAnimationFrame(() => {
// at this point it would make sense for character
// measurement to have taken place but it seems that
// xterm.js might be doing this asynchronously, so
// we force it instead
// eslint-disable-next-line no-debugger
//debugger;
this.term.charMeasure.measure(termOptions);
this.fitResize();
});
}
getTermDocument() {
// eslint-disable-next-line no-console
console.warn(
'The underlying terminal engine of Hyper no longer ' +
'uses iframes with individual `document` objects for each ' +
'terminal instance. This method call is retained for ' +
"backwards compatibility reasons. It's ok to attach directly" +
'to the `document` object of the main `window`.'
);
return document;
}
onWindowResize() {
this.fitResize();
}
// intercepting paste event for any necessary processing of
// clipboard data, if result is falsy, paste event continues
onWindowPaste(e) {
if (!this.props.isTermActive) return;
const processed = processClipboard();
if (processed) {
e.preventDefault();
e.stopPropagation();
this.term.send(processed);
}
}
handleScrollEnter() {
clearTimeout(this.scrollbarsHideTimer);
const prefs = this.term.getPrefs();
prefs.set('scrollbar-visible', true);
this.scrollMouseEnter = true;
}
handleScrollLeave() {
const prefs = this.term.getPrefs();
prefs.set('scrollbar-visible', false);
this.scrollMouseEnter = false;
}
handleMouseUp() {
this.props.onActive();
// this makes sure that we focus the hyper caret only
// if a click on the term does not result in a selection
// otherwise, if we focus without such check, it'd be
// impossible to select a piece of text
if (this.term.document_.getSelection().type !== 'Range') {
this.term.focusHyperCaret();
onMouseUp(e) {
if (this.props.quickEdit && e.button === 2) {
if (this.term.hasSelection()) {
clipboard.writeText(this.term.getSelection());
this.term.clearSelection();
} else {
document.execCommand('paste');
}
} else if (this.props.copyOnSelect && this.term.hasSelection()) {
clipboard.writeText(this.term.getSelection());
}
}
handleFocus() {
// This will in turn result in `this.focus()` being
// called, which is unecessary.
// Should investigate if it matters.
this.props.onActive();
}
handleKeyDown(e) {
if (e.ctrlKey && e.key === 'c') {
this.props.onURLAbort();
}
}
onHyperCaret(caret) {
this.hyperCaret = caret;
}
write(data) {
// sometimes the preference set above for
// `receive-encoding` is not known by the vt
// before we type to write (since the preference
// manager is asynchronous), so we force it to
// avoid buffering
// this fixes a race condition where sometimes
// opening new sessions results in broken
// output due to the term attempting to decode
// as `utf-8` instead of `raw`
if (this.term.vt.characterEncoding !== 'raw') {
this.term.vt.characterEncoding = 'raw';
}
this.term.io.writeUTF8(data);
this.term.write(data);
}
focus() {
this.term.focusHyperCaret();
this.term.focus();
}
clear() {
this.term.wipeContents();
this.term.onVTKeystroke('\f');
this.term.clear();
}
moveWordLeft() {
this.term.onVTKeystroke('\x1bb');
reset() {
this.term.reset();
}
moveWordRight() {
this.term.onVTKeystroke('\x1bf');
}
deleteWordLeft() {
this.term.onVTKeystroke('\x1b\x7f');
}
deleteWordRight() {
this.term.onVTKeystroke('\x1bd');
}
deleteLine() {
this.term.onVTKeystroke('\x1bw');
}
moveToStart() {
this.term.onVTKeystroke('\x01');
}
moveToEnd() {
this.term.onVTKeystroke('\x05');
resize(cols, rows) {
this.term.resize(cols, rows);
}
selectAll() {
this.term.selectAll();
}
getScreenNode() {
return this.term.scrollPort_.getScreenNode();
}
getTermDocument() {
return this.term.document_;
}
getStylesheet(css) {
const hyperCaret = `
.hyper-caret {
outline: none;
display: inline-block;
color: transparent;
text-shadow: 0 0 0 black;
font-family: ${this.props.fontFamily};
font-size: ${this.props.fontSize}px;
}
`;
const scrollBarCss = `
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-thumb {
-webkit-border-radius: 10px;
border-radius: 10px;
background: ${this.props.borderColor};
}
::-webkit-scrollbar-thumb:window-inactive {
background: ${this.props.borderColor};
}
`;
const selectCss = `
::selection {
background: ${Color(this.props.cursorColor).alpha(0.4).rgb().toString()};
}
`;
return URL.createObjectURL(new Blob([`
.cursor-node[focus="false"] {
border-width: 1px !important;
}
x-row {
line-height: 1em;
}
${hyperCaret}
${scrollBarCss}
${selectCss}
${css}
`], {type: 'text/css'}));
}
validateColor(color, alternative = 'rgb(255,255,255)') {
try {
return Color(color).rgb().toString();
} catch (err) {
notify(`color "${color}" is invalid`);
fitResize() {
if (!this.termWrapperRef) {
return;
}
return alternative;
this.term.fit();
}
handleMouseDown(ev) {
// we prevent losing focus when clicking the boundary
// wrappers of the main terminal element
if (ev.target === this.termWrapperRef ||
ev.target === this.termRef) {
ev.preventDefault();
}
if (this.props.quickEdit) {
this.term.onMouseDown_(ev);
}
keyboardHandler(e) {
// Has Mousetrap flagged this event as a command?
return !e.catched;
}
componentWillReceiveProps(nextProps) {
if (this.props.url !== nextProps.url) {
// when the url prop changes, we make sure
// the terminal starts or stops ignoring
// key input so that it doesn't conflict
// with the <webview>
if (nextProps.url) {
this.term.io.push();
window.addEventListener('keydown', this.handleKeyDown);
} else {
window.removeEventListener('keydown', this.handleKeyDown);
this.term.io.pop();
}
}
if (!this.props.cleared && nextProps.cleared) {
this.clear();
}
const nextTermOptions = getTermOptions(nextProps);
const prefs = this.term.getPrefs();
// Update only options that have changed.
Object.keys(nextTermOptions)
.filter(option => option !== 'theme' && nextTermOptions[option] !== this.termOptions[option])
.forEach(option => this.term.setOption(option, nextTermOptions[option]));
if (this.props.fontSize !== nextProps.fontSize) {
prefs.set('font-size', nextProps.fontSize);
this.hyperCaret.style.fontSize = nextProps.fontSize + 'px';
// Do we need to update theme?
const shouldUpdateTheme =
!this.termOptions.theme ||
Object.keys(nextTermOptions.theme).some(
option => nextTermOptions.theme[option] !== this.termOptions.theme[option]
);
if (shouldUpdateTheme) {
this.term.setOption('theme', nextTermOptions.theme);
}
if (this.props.foregroundColor !== nextProps.foregroundColor) {
prefs.set('foreground-color', nextProps.foregroundColor);
this.termOptions = nextTermOptions;
if (!this.props.isTermActive && nextProps.isTermActive) {
requestAnimationFrame(() => {
this.term.charMeasure.measure(this.termOptions);
this.fitResize();
});
}
if (this.props.fontFamily !== nextProps.fontFamily) {
prefs.set('font-family', nextProps.fontFamily);
this.hyperCaret.style.fontFamily = nextProps.fontFamily;
if (this.props.fontSize !== nextProps.fontSize || this.props.fontFamily !== nextProps.fontFamily) {
// invalidate xterm cache about how wide each
// character is
this.term.charMeasure.measure(this.termOptions);
// resize to fit the container
this.fitResize();
}
if (this.props.fontSmoothing !== nextProps.fontSmoothing) {
prefs.set('font-smoothing', nextProps.fontSmoothing);
if (nextProps.rows !== this.props.rows || nextProps.cols !== this.props.cols) {
this.resize(nextProps.cols, nextProps.rows);
}
}
if (this.props.cursorColor !== nextProps.cursorColor) {
prefs.set('cursor-color', this.validateColor(nextProps.cursorColor, 'rgba(255,255,255,0.5)'));
}
onTermWrapperRef(component) {
this.termWrapperRef = component;
}
if (this.props.cursorShape !== nextProps.cursorShape) {
this.term.setCursorShape(nextProps.cursorShape);
}
if (this.props.cursorBlink !== nextProps.cursorBlink) {
prefs.set('cursor-blink', nextProps.cursorBlink);
}
if (this.props.colors !== nextProps.colors) {
prefs.set('color-palette-overrides', getColorList(nextProps.colors));
}
if (this.props.customCSS !== nextProps.customCSS) {
prefs.set('user-css', this.getStylesheet(nextProps.customCSS));
}
if (this.props.bell === 'SOUND') {
prefs.set('audible-bell-sound', this.props.bellSoundURL);
} else {
prefs.set('audible-bell-sound', '');
}
if (this.props.copyOnSelect) {
prefs.set('copy-on-select', true);
} else {
prefs.set('copy-on-select', false);
}
onTermRef(component) {
this.termRef = component;
}
componentWillUnmount() {
terms[this.props.uid] = this;
// turn blinking off to prevent leaking a timeout when disposing terminal
const prefs = this.term.getPrefs();
prefs.set('cursor-blink', false);
clearTimeout(this.scrollbarsHideTimer);
this.props.ref_(null);
terms[this.props.uid] = null;
this.props.ref_(this.props.uid, null);
// to clean up the terminal, we remove the listeners
// instead of invoking `destroy`, since it will make the
// term insta un-attachable in the future (which we need
// to do in case of splitting, see `componentDidMount`
['title', 'focus', 'data', 'resize', 'cursormove'].forEach(type => this.term.removeAllListeners(type));
window.removeEventListener('resize', this.onWindowResize, {
passive: true
});
window.removeEventListener('paste', this.onWindowPaste, {
capture: true
});
}
template(css) {
return (<div
ref={component => {
this.termWrapperRef = component;
}}
className={css('fit', this.props.isTermActive && 'active')}
onMouseDown={this.handleMouseDown}
style={{padding: this.props.padding}}
>
{ this.props.customChildrenBefore }
render() {
return (
<div
ref={component => {
this.termRef = component;
}}
className={css('fit', 'term')}
/>
{ this.props.url ?
<webview
key="hyper-webview"
src={this.props.url}
onFocus={this.handleFocus}
style={{
background: '#fff',
position: 'absolute',
top: 0,
left: 0,
display: 'inline-flex',
width: '100%',
height: '100%'
}}
/> :
<div // eslint-disable-line react/jsx-indent
key="scrollbar"
className={css('scrollbarShim')}
onMouseEnter={this.handleScrollEnter}
onMouseLeave={this.handleScrollLeave}
/>
}
<div key="hyper-caret" ref={this.onHyperCaret} contentEditable className="hyper-caret"/>
{ this.props.customChildren }
</div>);
}
className={`term_fit ${this.props.isTermActive ? 'term_active' : ''}`}
style={{padding: this.props.padding}}
onMouseUp={this.onMouseUp}
>
{this.props.customChildrenBefore}
<div ref={this.onTermWrapperRef} className="term_fit term_wrapper">
<div ref={this.onTermRef} className="term_fit term_term" />
</div>
{this.props.customChildren}
styles() {
return {
fit: {
display: 'block',
width: '100%',
height: '100%'
},
<style jsx>{`
.term_fit {
display: block;
width: 100%;
height: 100%;
}
term: {
position: 'relative'
},
scrollbarShim: {
position: 'fixed',
right: 0,
width: '50px',
top: 0,
bottom: 0,
pointerEvents: 'none'
}
};
.term_wrapper {
/* TODO: decide whether to keep this or not based on understanding what xterm-selection is for */
overflow: hidden;
}
`}</style>
</div>
);
}
}

View file

@ -1,20 +1,21 @@
import React from 'react';
import Component from '../component';
import {decorate, getTermGroupProps} from '../utils/plugins';
import CommandRegistry from '../command-registry';
import {registerCommandHandlers} from '../command-registry';
import TermGroup_ from './term-group';
import StyleSheet_ from './style-sheet';
const TermGroup = decorate(TermGroup_, 'TermGroup');
const StyleSheet = decorate(StyleSheet_, 'StyleSheet');
const isMac = /Mac/.test(navigator.userAgent);
export default class Terms extends Component {
export default class Terms extends React.Component {
constructor(props, context) {
super(props, context);
this.terms = {};
this.bound = new WeakMap();
this.onRef = this.onRef.bind(this);
this.registerCommands = CommandRegistry.register;
this.registerCommands = registerCommandHandlers;
props.ref_(this);
}
@ -62,18 +63,24 @@ export default class Terms extends Component {
this.terms[uid] = term;
}
componentDidMount() {
window.addEventListener('contextmenu', () => {
const selection = window.getSelection().toString();
const {props: {uid}} = this.getActiveTerm();
this.props.onContextMenu(uid, selection);
});
}
componentWillUnmount() {
this.props.ref_(null);
}
template(css) {
render() {
const shift = !isMac && this.props.termGroups.length > 1;
return (<div
className={css('terms', shift && 'termsShifted')}
>
{ this.props.customChildrenBefore }
{
this.props.termGroups.map(termGroup => {
return (
<div className={`terms_terms ${shift ? 'terms_termsShifted' : ''}`}>
{this.props.customChildrenBefore}
{this.props.termGroups.map(termGroup => {
const {uid} = termGroup;
const isActive = uid === this.props.activeRootGroup;
const props = getTermGroupProps(uid, this.props, {
@ -81,19 +88,20 @@ export default class Terms extends Component {
terms: this.terms,
activeSession: this.props.activeSession,
sessions: this.props.sessions,
customCSS: this.props.customCSS,
fontSize: this.props.fontSize,
backgroundColor: this.props.backgroundColor,
foregroundColor: this.props.foregroundColor,
borderColor: this.props.borderColor,
cursorColor: this.props.cursorColor,
selectionColor: this.props.selectionColor,
colors: this.props.colors,
cursorShape: this.props.cursorShape,
cursorBlink: this.props.cursorBlink,
cursorColor: this.props.cursorColor,
fontSize: this.props.fontSize,
fontFamily: this.props.fontFamily,
uiFontFamily: this.props.uiFontFamily,
fontSmoothing: this.props.fontSmoothing,
foregroundColor: this.props.foregroundColor,
backgroundColor: this.props.backgroundColor,
fontWeight: this.props.fontWeight,
fontWeightBold: this.props.fontWeightBold,
padding: this.props.padding,
colors: this.props.colors,
bell: this.props.bell,
bellSoundURL: this.props.bellSoundURL,
copyOnSelect: this.props.copyOnSelect,
@ -103,54 +111,56 @@ export default class Terms extends Component {
onTitle: this.props.onTitle,
onData: this.props.onData,
onURLAbort: this.props.onURLAbort,
onContextMenu: this.props.onContextMenu,
quickEdit: this.props.quickEdit,
parentProps: this.props
});
return (
<div
key={`d${uid}`}
className={css('termGroup', isActive && 'termGroupActive')}
>
<TermGroup
key={uid}
ref_={this.onRef}
{...props}
/>
<div key={`d${uid}`} className={`terms_termGroup ${isActive ? 'terms_termGroupActive' : ''}`}>
<TermGroup key={uid} ref_={this.onRef} {...props} />
</div>
);
})
}
{ this.props.customChildren }
</div>);
}
})}
{this.props.customChildren}
<StyleSheet
backgroundColor={this.props.backgroundColor}
customCSS={this.props.customCSS}
fontFamily={this.props.fontFamily}
foregroundColor={this.props.foregroundColor}
borderColor={this.props.borderColor}
/>
styles() {
return {
terms: {
position: 'absolute',
marginTop: '34px',
top: 0,
right: 0,
left: 0,
bottom: 0,
color: '#fff',
transition: isMac ? '' : 'margin-top 0.3s ease'
},
<style jsx>{`
.terms_terms {
position: absolute;
margin-top: 34px;
top: 0;
right: 0;
left: 0;
bottom: 0;
color: #fff;
transition: ${isMac ? 'none' : 'margin-top 0.3s ease'};
}
termsShifted: {
marginTop: '68px'
},
.terms_termsShifted {
margin-top: 68px;
}
termGroup: {
display: 'none',
width: '100%',
height: '100%'
},
.terms_termGroup {
display: block;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: -9999em; /* Offscreen to pause xterm rendering, thanks to IntersectionObserver */
}
termGroupActive: {
display: 'block'
}
};
.terms_termGroupActive {
left: 0;
}
`}</style>
</div>
);
}
}

View file

@ -14,6 +14,9 @@ export const UI_WINDOW_MAXIMIZE = 'UI_WINDOW_MAXIMIZE';
export const UI_WINDOW_UNMAXIMIZE = 'UI_WINDOW_UNMAXIMIZE';
export const UI_WINDOW_GEOMETRY_CHANGED = 'UI_WINDOW_GEOMETRY_CHANGED';
export const UI_OPEN_FILE = 'UI_OPEN_FILE';
export const UI_OPEN_SSH_URL = 'UI_OPEN_SSH_URL';
export const UI_OPEN_HAMBURGER_MENU = 'UI_OPEN_HAMBURGER_MENU';
export const UI_WINDOW_MINIMIZE = 'UI_WINDOW_MINIMIZE';
export const UI_WINDOW_CLOSE = 'UI_WINDOW_CLOSE';
export const UI_CONTEXTMENU_OPEN = 'UI_CONTEXTMENU_OPEN';
export const UI_COMMAND_EXEC = 'UI_COMMAND_EXEC';

View file

@ -14,16 +14,17 @@ const getActiveSessions = ({termGroups}) => termGroups.activeSessions;
const getActivityMarkers = ({ui}) => ui.activityMarkers;
const getTabs = createSelector(
[getSessions, getRootGroups, getActiveSessions, getActiveRootGroup, getActivityMarkers],
(sessions, rootGroups, activeSessions, activeRootGroup, activityMarkers) => rootGroups.map(t => {
const activeSessionUid = activeSessions[t.uid];
const session = sessions[activeSessionUid];
return {
uid: t.uid,
title: session.title,
isActive: t.uid === activeRootGroup,
hasActivity: activityMarkers[session.uid]
};
})
(sessions, rootGroups, activeSessions, activeRootGroup, activityMarkers) =>
rootGroups.map(t => {
const activeSessionUid = activeSessions[t.uid];
const session = sessions[activeSessionUid];
return {
uid: t.uid,
title: session.title,
isActive: t.uid === activeRootGroup,
hasActivity: activityMarkers[session.uid]
};
})
);
const HeaderContainer = connect(

View file

@ -1,11 +1,12 @@
/* eslint-disable react/no-danger */
import Mousetrap from 'mousetrap';
import React from 'react';
import Mousetrap from 'mousetrap';
import Component from '../component';
import {connect} from '../utils/plugins';
import * as uiActions from '../actions/ui';
import {getRegisteredKeys, getCommandHandler, shouldPreventDefault} from '../command-registry';
import stylis from 'stylis';
import HeaderContainer from './header';
import TermsContainer from './terms';
@ -13,11 +14,16 @@ import NotificationsContainer from './notifications';
const isMac = /Mac/.test(navigator.userAgent);
class Hyper extends Component {
class Hyper extends React.PureComponent {
constructor(props) {
super(props);
this.handleFocusActive = this.handleFocusActive.bind(this);
this.handleSelectAll = this.handleSelectAll.bind(this);
this.onTermsRef = this.onTermsRef.bind(this);
this.mousetrap = null;
this.state = {
lastConfigUpdate: 0
};
}
componentWillReceiveProps(next) {
@ -26,6 +32,11 @@ class Hyper extends Component {
// starts working again
document.body.style.backgroundColor = next.backgroundColor;
}
const {lastConfigUpdate} = next;
if (lastConfigUpdate && lastConfigUpdate !== this.state.lastConfigUpdate) {
this.setState({lastConfigUpdate});
this.attachKeyListeners();
}
}
handleFocusActive() {
@ -35,44 +46,43 @@ class Hyper extends Component {
}
}
attachKeyListeners() {
const {moveTo, moveLeft, moveRight} = this.props;
handleSelectAll() {
const term = this.terms.getActiveTerm();
if (!term) {
return;
if (term) {
term.selectAll();
}
const lastIndex = this.terms.getLastTermIndex();
const document = term.getTermDocument();
const keys = new Mousetrap(document);
keys.bind('mod+1', moveTo.bind(this, 0));
keys.bind('mod+2', moveTo.bind(this, 1));
keys.bind('mod+3', moveTo.bind(this, 2));
keys.bind('mod+4', moveTo.bind(this, 3));
keys.bind('mod+5', moveTo.bind(this, 4));
keys.bind('mod+6', moveTo.bind(this, 5));
keys.bind('mod+7', moveTo.bind(this, 6));
keys.bind('mod+8', moveTo.bind(this, 7));
keys.bind('mod+9', moveTo.bind(this, lastIndex));
}
keys.bind('mod+shift+left', moveLeft);
keys.bind('mod+shift+right', moveRight);
keys.bind('mod+shift+[', moveLeft);
keys.bind('mod+shift+]', moveRight);
keys.bind('mod+alt+left', moveLeft);
keys.bind('mod+alt+right', moveRight);
keys.bind('ctrl+shift+tab', moveLeft);
keys.bind('ctrl+tab', moveRight);
attachKeyListeners() {
if (!this.mousetrap) {
this.mousetrap = new Mousetrap(window, true);
this.mousetrap.stopCallback = () => {
// All events should be intercepted even if focus is in an input/textarea
return false;
};
} else {
this.mousetrap.reset();
}
const bound = method => term[method].bind(term);
keys.bind('alt+left', bound('moveWordLeft'));
keys.bind('alt+right', bound('moveWordRight'));
keys.bind('alt+backspace', bound('deleteWordLeft'));
keys.bind('alt+del', bound('deleteWordRight'));
keys.bind('mod+backspace', bound('deleteLine'));
keys.bind('mod+left', bound('moveToStart'));
keys.bind('mod+right', bound('moveToEnd'));
keys.bind('mod+a', bound('selectAll'));
this.keys = keys;
const keys = getRegisteredKeys();
Object.keys(keys).forEach(commandKeys => {
this.mousetrap.bind(
commandKeys,
e => {
const command = keys[commandKeys];
// We should tell to xterm that it should ignore this event.
e.catched = true;
this.props.execCommand(command, getCommandHandler(command), e);
shouldPreventDefault(command) && e.preventDefault();
},
'keydown'
);
});
}
componentDidMount() {
this.attachKeyListeners();
window.rpc.on('term selectAll', this.handleSelectAll);
}
onTermsRef(terms) {
@ -81,61 +91,58 @@ class Hyper extends Component {
componentDidUpdate(prev) {
if (prev.activeSession !== this.props.activeSession) {
if (this.keys) {
this.keys.reset();
}
this.handleFocusActive();
this.attachKeyListeners();
}
}
componentWillUnmount() {
if (this.keys) {
this.keys.reset();
}
document.body.style.backgroundColor = 'inherit';
}
template(css) {
const {isMac, customCSS, uiFontFamily, borderColor, maximized} = this.props;
const borderWidth = isMac ? '' :
`${maximized ? '0' : '1'}px`;
render() {
const {isMac: isMac_, customCSS, uiFontFamily, borderColor, maximized} = this.props;
const borderWidth = isMac_ ? '' : `${maximized ? '0' : '1'}px`;
return (
<div>
<div id="hyper">
<div
style={{fontFamily: uiFontFamily, borderColor, borderWidth}}
className={css('main', isMac && 'mainRounded')}
className={`hyper_main ${isMac_ && 'hyper_mainRounded'}`}
>
<HeaderContainer/>
<TermsContainer ref_={this.onTermsRef}/>
{ this.props.customInnerChildren }
<HeaderContainer />
<TermsContainer ref_={this.onTermsRef} />
{this.props.customInnerChildren}
</div>
<NotificationsContainer/>
<style dangerouslySetInnerHTML={{__html: customCSS}}/>
{ this.props.customChildren }
<NotificationsContainer />
{this.props.customChildren}
<style jsx>
{`
.hyper_main {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
border: 1px solid #333;
}
.hyper_mainRounded {
border-radius: 5px;
}
`}
</style>
{/*
Add custom CSS to Hyper.
We add a scope to the customCSS so that it can get around the weighting applied by styled-jsx
*/}
<style dangerouslySetInnerHTML={{__html: stylis('#hyper', customCSS, {prefix: false})}} />
</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 HyperContainer = connect(
@ -147,21 +154,14 @@ const HyperContainer = connect(
borderColor: state.ui.borderColor,
activeSession: state.sessions.activeUid,
backgroundColor: state.ui.backgroundColor,
maximized: state.ui.maximized
maximized: state.ui.maximized,
lastConfigUpdate: state.ui._lastUpdate
};
},
dispatch => {
return {
moveTo: i => {
dispatch(uiActions.moveTo(i));
},
moveLeft: () => {
dispatch(uiActions.moveLeft());
},
moveRight: () => {
dispatch(uiActions.moveRight());
execCommand: (command, fn, e) => {
dispatch(uiActions.execCommand(command, fn, e));
}
};
},

View file

@ -34,7 +34,9 @@ const NotificationsContainer = connect(
Object.assign(state_, {
updateShowing: true,
updateVersion: ui.updateVersion,
updateNote: ui.updateNotes.split('\n')[0]
updateNote: ui.updateNotes.split('\n')[0],
updateReleaseUrl: ui.updateReleaseUrl,
updateCanInstall: ui.updateCanInstall
});
} else if (notifications.message) {
Object.assign(state_, {

View file

@ -7,6 +7,7 @@ import {
setSessionXtermTitle,
setActiveSession
} from '../actions/sessions';
import {openContextMenu} from '../actions/ui';
import getRootGroups from '../selectors';
const TermsContainer = connect(
@ -21,17 +22,19 @@ const TermsContainer = connect(
activeSession: state.sessions.activeUid,
customCSS: state.ui.termCSS,
write: state.sessions.write,
fontSize: state.ui.fontSizeOverride ?
state.ui.fontSizeOverride :
state.ui.fontSize,
fontSize: state.ui.fontSizeOverride ? state.ui.fontSizeOverride : state.ui.fontSize,
fontFamily: state.ui.fontFamily,
fontWeight: state.ui.fontWeight,
fontWeightBold: state.ui.fontWeightBold,
uiFontFamily: state.ui.uiFontFamily,
fontSmoothing: state.ui.fontSmoothingOverride,
padding: state.ui.padding,
cursorColor: state.ui.cursorColor,
cursorAccentColor: state.ui.cursorAccentColor,
cursorShape: state.ui.cursorShape,
cursorBlink: state.ui.cursorBlink,
borderColor: state.ui.borderColor,
selectionColor: state.ui.selectionColor,
colors: state.ui.colors,
foregroundColor: state.ui.foregroundColor,
backgroundColor: state.ui.backgroundColor,
@ -49,8 +52,7 @@ const TermsContainer = connect(
},
onTitle(uid, title) {
// we need to trim the title because `cmd.exe` likes to report ' ' as the title
dispatch(setSessionXtermTitle(uid, title.trim()));
dispatch(setSessionXtermTitle(uid, title));
},
onResize(uid, cols, rows) {
@ -63,6 +65,11 @@ const TermsContainer = connect(
onActive(uid) {
dispatch(setActiveSession(uid));
},
onContextMenu(uid, selection) {
dispatch(setActiveSession(uid));
dispatch(openContextMenu(uid, selection));
}
};
},

View file

@ -1,492 +0,0 @@
import {clipboard} from 'electron';
import {hterm, lib} from 'hterm-umdjs';
import runes from 'runes';
import fromCharCode from './utils/key-code';
import selection from './utils/selection';
import returnKey from './utils/keymaps';
import CommandRegistry from './command-registry';
hterm.defaultStorage = new lib.Storage.Memory();
// Provide selectAll to terminal viewport
hterm.Terminal.prototype.selectAll = function () {
// If the cursorNode_ having hyperCaret we need to remove it
if (this.cursorNode_.contains(this.hyperCaret)) {
this.cursorNode_.removeChild(this.hyperCaret);
// We need to clear the DOM range to reset anchorNode
selection.clear(this);
selection.all(this);
}
};
// override double click behavior to copy
const oldMouse = hterm.Terminal.prototype.onMouse_;
hterm.Terminal.prototype.onMouse_ = function (e) {
if (e.type === 'dblclick') {
selection.extend(this);
console.log('[hyper+hterm] ignore double click');
return;
}
return oldMouse.call(this, e);
};
function containsNonLatinCodepoints(s) {
return /[^\u0000-\u00ff]/.test(s);
}
// hterm Unicode patch
hterm.TextAttributes.splitWidecharString = function (str) {
const context = runes(str).reduce((ctx, rune) => {
const code = rune.codePointAt(0);
if (code < 128 || lib.wc.charWidth(code) === 1) {
ctx.acc += rune;
return ctx;
}
if (ctx.acc) {
ctx.items.push({str: ctx.acc});
ctx.acc = '';
}
ctx.items.push({str: rune, wcNode: true});
return ctx;
}, {items: [], acc: ''});
if (context.acc) {
context.items.push({str: context.acc});
}
return context.items;
};
// hterm Unicode patch
const cache = [];
lib.wc.strWidth = function (str) {
const shouldCache = str.length === 1;
if (shouldCache && cache[str] !== undefined) {
return cache[str];
}
const chars = runes(str);
let width = 0;
let rv = 0;
for (let i = 0; i < chars.length; i++) {
const codePoint = chars[i].codePointAt(0);
width = lib.wc.charWidth(codePoint);
if (width < 0) {
return -1;
}
rv += width * ((codePoint <= 0xFFFF) ? 1 : 2);
}
if (shouldCache) {
cache[str] = rv;
}
return rv;
};
// hterm Unicode patch
lib.wc.substr = function (str, start, optWidth) {
const chars = runes(str);
let startIndex;
let endIndex;
let width = 0;
for (let i = 0; i < chars.length; i++) {
const codePoint = chars[i].codePointAt(0);
const charWidth = lib.wc.charWidth(codePoint);
if ((width + charWidth) > start) {
startIndex = i;
break;
}
width += charWidth;
}
if (optWidth) {
width = 0;
for (endIndex = startIndex; endIndex < chars.length && width < optWidth; endIndex++) {
width += lib.wc.charWidth(chars[endIndex].charCodeAt(0));
}
if (width > optWidth) {
endIndex--;
}
return chars.slice(startIndex, endIndex).join('');
}
return chars.slice(startIndex).join('');
};
// MacOS emoji bar support
hterm.Keyboard.prototype.onTextInput_ = function (e) {
if (!e.data) {
return;
}
runes(e.data).forEach(this.terminal.onVTKeystroke.bind(this.terminal));
};
hterm.Terminal.IO.prototype.writeUTF8 = function (string) {
if (this.terminal_.io !== this) {
throw new Error('Attempt to print from inactive IO object.');
}
if (!containsNonLatinCodepoints(string)) {
this.terminal_.interpret(string);
return;
}
runes(string).forEach(rune => {
this.terminal_.getTextAttributes().unicodeNode = containsNonLatinCodepoints(rune);
this.terminal_.interpret(rune);
this.terminal_.getTextAttributes().unicodeNode = false;
});
};
const oldIsDefault = hterm.TextAttributes.prototype.isDefault;
hterm.TextAttributes.prototype.isDefault = function () {
return !this.unicodeNode && oldIsDefault.call(this);
};
const oldSetFontSize = hterm.Terminal.prototype.setFontSize;
hterm.Terminal.prototype.setFontSize = function (px) {
oldSetFontSize.call(this, px);
const doc = this.getDocument();
let unicodeNodeStyle = doc.getElementById('hyper-unicode-styles');
if (!unicodeNodeStyle) {
unicodeNodeStyle = doc.createElement('style');
unicodeNodeStyle.setAttribute('id', 'hyper-unicode-styles');
doc.head.appendChild(unicodeNodeStyle);
}
unicodeNodeStyle.innerHTML = `
.unicode-node {
display: inline-block;
vertical-align: top;
width: ${this.scrollPort_.characterSize.width}px;
}
`;
};
const oldCreateContainer = hterm.TextAttributes.prototype.createContainer;
hterm.TextAttributes.prototype.createContainer = function (text) {
const container = oldCreateContainer.call(this, text);
if (container.style && runes(text).length === 1 && containsNonLatinCodepoints(text)) {
container.className += ' unicode-node';
}
return container;
};
// Do not match containers when one of them has unicode text (unicode chars need to be alone in their containers)
const oldMatchesContainer = hterm.TextAttributes.prototype.matchesContainer;
hterm.TextAttributes.prototype.matchesContainer = function (obj) {
return oldMatchesContainer.call(this, obj) &&
!this.unicodeNode &&
!containsNonLatinCodepoints(obj.textContent);
};
// there's no option to turn off the size overlay
hterm.Terminal.prototype.overlaySize = function () {};
// fixing a bug in hterm where a double click triggers
// a non-collapsed selection whose text is '', and results
// in an infinite copy loop
hterm.Terminal.prototype.copySelectionToClipboard = function () {
const text = this.getSelectionText();
if (text) {
this.copyStringToClipboard(text);
}
};
let lastEventTimeStamp;
let lastEventKey;
// passthrough all the commands that are meant to control
// hyper and not the terminal itself
const oldKeyDown = hterm.Keyboard.prototype.onKeyDown_;
hterm.Keyboard.prototype.onKeyDown_ = function (e) {
const modifierKeysConf = this.terminal.modifierKeys;
if (e.timeStamp === lastEventTimeStamp && e.key === lastEventKey) {
// Event was already processed.
// It seems to occur after a char composition ended by Tab and cause a blur.
// See https://github.com/zeit/hyper/issues/1341
e.preventDefault();
return;
}
lastEventTimeStamp = e.timeStamp;
lastEventKey = e.key;
if (e.altKey &&
e.which !== 16 && // Ignore other modifer keys
e.which !== 17 &&
e.which !== 18 &&
e.which !== 91 &&
modifierKeysConf.altIsMeta) {
const char = fromCharCode(e);
this.terminal.onVTKeystroke('\x1b' + char);
e.preventDefault();
}
if (e.metaKey &&
e.code !== 'MetaLeft' &&
e.code !== 'MetaRight' &&
e.which !== 16 &&
e.which !== 17 &&
e.which !== 18 &&
e.which !== 91 &&
modifierKeysConf.cmdIsMeta) {
const char = fromCharCode(e);
this.terminal.onVTKeystroke('\x1b' + char);
e.preventDefault();
}
// test key from keymaps before moving forward with actions
const key = returnKey(e);
if (key) {
if (CommandRegistry.getCommand(key)) {
CommandRegistry.exec(key, e);
}
}
if (e.altKey || e.metaKey || key) {
// If the `hyperCaret` was removed on `selectAll`, we need to insert it back
if (e.key === 'v' && this.terminal.hyperCaret.parentNode !== this.terminal.cursorNode_) {
this.terminal.focusHyperCaret();
}
return;
}
// Test for valid keys in order to accept clear status
const clearBlacklist = [
'control',
'shift',
'capslock',
'dead'
];
if (!clearBlacklist.includes(e.code.toLowerCase()) &&
!clearBlacklist.includes(e.key.toLowerCase())) {
// Since Electron 1.6.X, there is a race condition with character composition
// if this selection clearing is made synchronously. See #2140.
setTimeout(() => selection.clear(this.terminal), 0);
}
// If the `hyperCaret` was removed on `selectAll`, we need to insert it back
if (this.terminal.hyperCaret.parentNode !== this.terminal.cursorNode_) {
this.terminal.focusHyperCaret();
}
return oldKeyDown.call(this, e);
};
const oldOnMouse = hterm.Terminal.prototype.onMouse_;
hterm.Terminal.prototype.onMouse_ = function (e) {
// override `preventDefault` to not actually
// prevent default when the type of event is
// mousedown, so that we can still trigger
// focus on the terminal when the underlying
// VT is interested in mouse events, as is the
// case of programs like `vtop` that allow for
// the user to click on rows
if (e.type === 'mousedown') {
e.preventDefault = function () { };
return;
}
return oldOnMouse.call(this, e);
};
hterm.Terminal.prototype.onMouseDown_ = function (e) {
// copy/paste on right click
if (e.button === 2) {
const text = this.getSelectionText();
if (text) {
this.copyStringToClipboard(text);
} else {
this.onVTKeystroke(clipboard.readText());
}
}
};
// override `ScrollPort.resize` to avoid an expensive calculation
// just to get the size of the scrollbar, which for Hyper is always
// set to overlay (hence with `0`)
hterm.ScrollPort.prototype.resize = function () {
this.currentScrollbarWidthPx = 0;
this.syncScrollHeight();
this.syncRowNodesDimensions_();
this.publish(
'resize',
{scrollPort: this},
() => {
this.scrollRowToBottom(this.rowProvider_.getRowCount());
this.scheduleRedraw();
}
);
};
// make background transparent to avoid transparency issues
hterm.ScrollPort.prototype.setBackgroundColor = function () {
this.screen_.style.backgroundColor = 'transparent';
};
// will be called by the <Term/> right after the `hterm.Terminal` is instantiated
hterm.Terminal.prototype.onHyperCaret = function (caret) {
this.hyperCaret = caret;
let ongoingComposition = false;
caret.addEventListener('compositionstart', () => {
ongoingComposition = true;
});
// we can ignore `compositionstart` since chromium always fire it with ''
caret.addEventListener('compositionupdate', () => {
this.cursorNode_.style.backgroundColor = 'yellow';
this.cursorNode_.style.borderColor = 'yellow';
});
// at this point the char(s) is ready
caret.addEventListener('compositionend', () => {
ongoingComposition = false;
this.cursorNode_.style.backgroundColor = '';
this.setCursorShape(this.getCursorShape());
this.cursorNode_.style.borderColor = this.getCursorColor();
caret.innerText = '';
});
// if you open the `Emoji & Symbols` (ctrl+cmd+space)
// and select an emoji, it'll be inserted into our caret
// and stay there until you star a compositon event.
// to avoid that, we'll just check if there's an ongoing
// compostion event. if there's one, we do nothing.
// otherwise, we just remove the emoji and stop the event
// propagation.
// PS: this event will *not* be fired when a standard char
// (a, b, c, 1, 2, 3, etc) is typed only for composed
// ones and `Emoji & Symbols`
caret.addEventListener('input', e => {
if (!ongoingComposition) {
caret.innerText = '';
e.stopPropagation();
e.preventDefault();
}
});
// we need to capture pastes, prevent them and send its contents to the terminal
caret.addEventListener('paste', e => {
e.stopPropagation();
e.preventDefault();
const text = e.clipboardData.getData('text');
this.onVTKeystroke(text);
});
// here we replicate the focus/blur state of our caret on the `hterm` caret
caret.addEventListener('focus', () => {
this.cursorNode_.setAttribute('focus', true);
this.restyleCursor_();
});
caret.addEventListener('blur', () => {
this.cursorNode_.setAttribute('focus', false);
this.restyleCursor_();
});
// this is necessary because we need to access the `document_` and the hyperCaret
// on `hterm.Screen.prototype.syncSelectionCaret`
this.primaryScreen_.terminal = this;
this.alternateScreen_.terminal = this;
};
// ensure that our contenteditable caret is injected
// inside the term's cursor node and that it's focused
hterm.Terminal.prototype.focusHyperCaret = function () {
if (!this.hyperCaret.parentNode !== this.cursorNode_) {
this.cursorNode_.appendChild(this.hyperCaret);
}
this.hyperCaret.focus();
};
hterm.Screen.prototype.syncSelectionCaret = function () {
const p = this.terminal.hyperCaret;
const doc = this.terminal.document_;
const win = doc.defaultView;
const s = win.getSelection();
const r = doc.createRange();
r.selectNodeContents(p);
s.removeAllRanges();
s.addRange(r);
};
// For some reason, when the original version of this function was called right
// after a new tab was created, it was breaking the focus of the other tab.
// After some investigation, I (matheuss) found that `this.iframe_.focus();` (from
// the original function) was causing the issue. So right now we're overriding
// the function to prevent the `iframe_` from being focused.
// This shouldn't create any side effects we're _stealing_ the focus from `htem` anyways.
hterm.ScrollPort.prototype.focus = function () {
this.screen_.focus();
};
// fixes a bug in hterm, where the cursor goes back to `BLOCK`
// after the bell rings
const oldRingBell = hterm.Terminal.prototype.ringBell;
hterm.Terminal.prototype.ringBell = function () {
oldRingBell.call(this);
setTimeout(() => {
this.restyleCursor_();
}, 200);
};
// fixes a bug in hterm, where the shorthand hex
// is not properly converted to rgb
lib.colors.hexToRGB = function (arg) {
const hex16 = lib.colors.re_.hex16;
const hex24 = lib.colors.re_.hex24;
function convert(hex) {
if (hex.length === 4) {
hex = hex.replace(hex16, (h, r, g, b) => {
return '#' + r + r + g + g + b + b;
});
}
const ary = hex.match(hex24);
if (!ary) {
return null;
}
return 'rgb(' +
parseInt(ary[1], 16) + ', ' +
parseInt(ary[2], 16) + ', ' +
parseInt(ary[3], 16) +
')';
}
if (Array.isArray(arg)) {
for (let i = 0; i < arg.length; i++) {
arg[i] = convert(arg[i]);
}
} else {
arg = convert(arg);
}
return arg;
};
// add support for cursor styles 5 and 6, fixes #270
hterm.VT.CSI[' q'] = function (parseState) {
const arg = parseState.args[0];
if (arg === '0' || arg === '1') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.BLOCK);
this.terminal.setCursorBlink(true);
} else if (arg === '2') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.BLOCK);
this.terminal.setCursorBlink(false);
} else if (arg === '3') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.UNDERLINE);
this.terminal.setCursorBlink(true);
} else if (arg === '4') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.UNDERLINE);
this.terminal.setCursorBlink(false);
} else if (arg === '5') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.BEAM);
this.terminal.setCursorBlink(true);
} else if (arg === '6') {
this.terminal.setCursorShape(hterm.Terminal.cursorShape.BEAM);
this.terminal.setCursorBlink(false);
} else {
console.warn('Unknown cursor style: ' + arg);
}
};
export default hterm;
export {lib};

View file

@ -44,30 +44,11 @@ rpc.on('session add', data => {
store_.dispatch(sessionActions.addSession(data));
});
// we aggregate all the incoming pty events by raf
// debouncing, to reduce allocation and iterations
let req;
let objects = {};
rpc.on('session data', d => {
// the uid is a uuid v4 so it's 36 chars long
const uid = d.slice(0, 36);
const data = d.slice(36);
if (objects[uid] === undefined) {
objects[uid] = data;
} else {
objects[uid] += data;
}
if (!req) {
req = requestAnimationFrame(() => {
for (const i in objects) {
if ({}.hasOwnProperty.call(objects, i)) {
store_.dispatch(sessionActions.addSessionData(i, objects[i]));
}
}
objects = {};
req = null;
});
}
store_.dispatch(sessionActions.addSessionData(uid, data));
});
rpc.on('session data send', ({uid, data, escaped}) => {
@ -86,6 +67,42 @@ rpc.on('session clear req', () => {
store_.dispatch(sessionActions.clearActiveSession());
});
rpc.on('session move word left req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x1bb'));
});
rpc.on('session move word right req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x1bf'));
});
rpc.on('session move line beginning req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x1bOH'));
});
rpc.on('session move line end req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x1bOF'));
});
rpc.on('session del word left req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x1b\x7f'));
});
rpc.on('session del word right req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x1bd'));
});
rpc.on('session del line beginning req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x1bw'));
});
rpc.on('session del line end req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x10B'));
});
rpc.on('session break req', () => {
store_.dispatch(sessionActions.sendSessionData(null, '\x03'));
});
rpc.on('termgroup add req', () => {
store_.dispatch(termGroupActions.requestTermGroup());
});
@ -118,6 +135,10 @@ rpc.on('move right req', () => {
store_.dispatch(uiActions.moveRight());
});
rpc.on('move jump req', index => {
store_.dispatch(uiActions.moveTo(index));
});
rpc.on('next pane req', () => {
store_.dispatch(uiActions.moveToNextPane());
});
@ -130,8 +151,12 @@ rpc.on('open file', ({path}) => {
store_.dispatch(uiActions.openFile(path));
});
rpc.on('update available', ({releaseName, releaseNotes}) => {
store_.dispatch(updaterActions.updateAvailable(releaseName, releaseNotes));
rpc.on('open ssh', url => {
store_.dispatch(uiActions.openSSH(url));
});
rpc.on('update available', ({releaseName, releaseNotes, releaseUrl, canInstall}) => {
store_.dispatch(updaterActions.updateAvailable(releaseName, releaseNotes, releaseUrl, canInstall));
});
rpc.on('move', () => {
@ -148,7 +173,7 @@ rpc.on('add notification', ({text, url, dismissable}) => {
const app = render(
<Provider store={store_}>
<HyperContainer/>
<HyperContainer />
</Provider>,
document.getElementById('mount')
);

View file

@ -35,15 +35,16 @@ function Session(obj) {
const reducer = (state = initialState, action) => {
switch (action.type) {
case SESSION_ADD:
return state
.set('activeUid', action.uid)
.setIn(['sessions', action.uid], Session({
return state.set('activeUid', action.uid).setIn(
['sessions', action.uid],
Session({
cols: action.cols,
rows: action.rows,
uid: action.uid,
shell: action.shell.split('/').pop(),
pid: action.pid
}));
})
);
case SESSION_URL_SET:
return state.setIn(['sessions', action.uid, 'url'], action.url);
@ -55,27 +56,31 @@ const reducer = (state = initialState, action) => {
return state.set('activeUid', action.uid);
case SESSION_CLEAR_ACTIVE:
return state.merge({
sessions: {
[state.activeUid]: {
cleared: true
return state.merge(
{
sessions: {
[state.activeUid]: {
cleared: true
}
}
}
}, {deep: true});
},
{deep: true}
);
case SESSION_PTY_DATA:
// we avoid a direct merge for perf reasons
// as this is the most common action
if (state.sessions[action.uid] &&
state.sessions[action.uid].cleared) {
return state
.merge({
if (state.sessions[action.uid] && state.sessions[action.uid].cleared) {
return state.merge(
{
sessions: {
[action.uid]: {
cleared: false
}
}
}, {deep: true});
},
{deep: true}
);
}
return state;
@ -83,6 +88,7 @@ const reducer = (state = initialState, action) => {
if (state.sessions[action.uid]) {
return deleteSession(state, action.uid);
}
// eslint-disable-next-line no-console
console.log('ignore pty exit: session removed by user');
return state;
@ -90,14 +96,22 @@ const reducer = (state = initialState, action) => {
return deleteSession(state, action.uid);
case SESSION_SET_XTERM_TITLE:
return state.setIn(['sessions', action.uid, 'title'], action.title);
return state.setIn(
['sessions', action.uid, 'title'],
// we need to trim the title because `cmd.exe`
// likes to report ' ' as the title
action.title.trim()
);
case SESSION_RESIZE:
return state.setIn(['sessions', action.uid], state.sessions[action.uid].merge({
rows: action.rows,
cols: action.cols,
resizeAt: action.now
}));
return state.setIn(
['sessions', action.uid],
state.sessions[action.uid].merge({
rows: action.rows,
cols: action.cols,
resizeAt: action.now
})
);
case SESSION_SET_CWD:
if (state.activeUid) {

View file

@ -40,9 +40,7 @@ const setActiveGroup = (state, action) => {
const childGroup = findBySession(state, action.uid);
const rootGroup = findRootGroup(state.termGroups, childGroup.uid);
return state
.set('activeRootGroup', rootGroup.uid)
.setIn(['activeSessions', rootGroup.uid], action.uid);
return state.set('activeRootGroup', rootGroup.uid).setIn(['activeSessions', rootGroup.uid], action.uid);
};
// Reduce existing sizes to fit a new split:
@ -50,7 +48,7 @@ const insertRebalance = (oldSizes, index) => {
const newSize = 1 / (oldSizes.length + 1);
// We spread out how much each pane should be reduced
// with based on their existing size:
const balanced = oldSizes.map(size => size - (newSize * size));
const balanced = oldSizes.map(size => size - newSize * size);
return [...balanced.slice(0, index), newSize, ...balanced.slice(index)];
};
@ -58,9 +56,7 @@ const insertRebalance = (oldSizes, index) => {
const removalRebalance = (oldSizes, index) => {
const removedSize = oldSizes[index];
const increase = removedSize / (oldSizes.length - 1);
return oldSizes
.filter((_size, i) => i !== index)
.map(size => size + increase);
return oldSizes.filter((_size, i) => i !== index).map(size => size + increase);
};
const splitGroup = (state, action) => {
@ -96,23 +92,27 @@ const splitGroup = (state, action) => {
parentUid: parentGroup.uid
});
return state
.setIn(['termGroups', existingSession.uid], existingSession)
.setIn(['termGroups', parentGroup.uid], parentGroup.merge({
return state.setIn(['termGroups', existingSession.uid], existingSession).setIn(
['termGroups', parentGroup.uid],
parentGroup.merge({
sessionUid: null,
direction: splitDirection,
children: [existingSession.uid, newSession.uid]
}));
})
);
}
const {children} = parentGroup;
// Insert the new child pane right after the active one:
const index = children.indexOf(activeGroup.uid) + 1;
const newChildren = [...children.slice(0, index), newSession.uid, ...children.slice(index)];
state = state.setIn(['termGroups', parentGroup.uid], parentGroup.merge({
direction: splitDirection,
children: newChildren
}));
state = state.setIn(
['termGroups', parentGroup.uid],
parentGroup.merge({
direction: splitDirection,
children: newChildren
})
);
if (parentGroup.sizes) {
const newSizes = insertRebalance(parentGroup.sizes, index);
@ -131,17 +131,13 @@ const replaceParent = (state, parent, child) => {
// If the parent we're replacing has a parent,
// we need to change the uid in its children array
// with `child`:
const newChildren = parentParent.children.map(uid =>
uid === parent.uid ? child.uid : uid
);
const newChildren = parentParent.children.map(uid => (uid === parent.uid ? child.uid : uid));
state = state.setIn(['termGroups', parentParent.uid, 'children'], newChildren);
} else {
// This means the given child will be
// a root group, so we need to set it up as such:
const newSessions = state.activeSessions
.without(parent.uid)
.set(child.uid, state.activeSessions[parent.uid]);
const newSessions = state.activeSessions.without(parent.uid).set(child.uid, state.activeSessions[parent.uid]);
state = state
.set('activeTermGroup', child.uid)
@ -185,10 +181,7 @@ const resizeGroup = (state, uid, sizes) => {
return state;
}
return state.setIn(
['termGroups', uid, 'sizes'],
sizes
);
return state.setIn(['termGroups', uid, 'sizes'], sizes);
};
const reducer = (state = initialState, action) => {

View file

@ -20,7 +20,6 @@ import {
SESSION_SET_CWD
} from '../constants/sessions';
import {UPDATE_AVAILABLE} from '../constants/updater';
import {values} from '../utils/object';
const allowedCursorShapes = new Set(['BEAM', 'BLOCK', 'UNDERLINE']);
const allowedCursorBlinkValues = new Set([true, false]);
@ -34,36 +33,41 @@ const initial = Immutable({
rows: null,
activeUid: null,
cursorColor: '#F81CE5',
cursorAccentColor: '#000',
cursorShape: 'BLOCK',
cursorBlink: false,
borderColor: '#333',
selectionColor: 'rgba(248,28,229,0.3)',
fontSize: 12,
padding: '12px 14px',
fontFamily: 'Menlo, "DejaVu Sans Mono", "Lucida Console", monospace',
uiFontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
uiFontFamily:
'-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
fontSizeOverride: null,
fontSmoothingOverride: 'antialiased',
fontWeight: 'normal',
fontWeightBold: 'bold',
css: '',
termCSS: '',
openAt: {},
resizeAt: 0,
colors: {
black: '#000000',
red: '#ff0000',
green: '#33ff00',
yellow: '#ffff00',
blue: '#0066ff',
magenta: '#cc00ff',
cyan: '#00ffff',
white: '#d0d0d0',
lightBlack: '#808080',
lightRed: '#ff0000',
lightGreen: '#33ff00',
lightYellow: '#ffff00',
lightBlue: '#0066ff',
lightMagenta: '#cc00ff',
lightCyan: '#00ffff',
lightWhite: '#ffffff'
red: '#C51E14',
green: '#1DC121',
yellow: '#C7C329',
blue: '#0A2FC4',
magenta: '#C839C5',
cyan: '#20C5C6',
white: '#C7C7C7',
lightBlack: '#686868',
lightRed: '#FD6F6B',
lightGreen: '#67F86F',
lightYellow: '#FFFA72',
lightBlue: '#6A76FB',
lightMagenta: '#FD7CFC',
lightCyan: '#68FDFE',
lightWhite: '#FFFFFF'
},
activityMarkers: {},
notifications: {
@ -97,128 +101,144 @@ const currentWindow = remote.getCurrentWindow();
const reducer = (state = initial, action) => {
let state_ = state;
let isMax;
switch (action.type) { // eslint-disable-line default-case
//eslint-disable-next-line default-case
switch (action.type) {
case CONFIG_LOAD:
case CONFIG_RELOAD: // eslint-disable-line no-case-declarations
const {config} = action;
// eslint-disable-next-line no-case-declarations, no-fallthrough
case CONFIG_RELOAD:
const {config, now} = action;
state_ = state
// unset the user font size override if the
// font size changed from the config
.merge((() => {
const ret = {};
.merge(
(() => {
const ret = {};
if (state.fontSizeOverride && config.fontSize !== state.fontSize) {
ret.fontSizeOverride = null;
}
if (state.fontSizeOverride && config.fontSize !== state.fontSize) {
ret.fontSizeOverride = null;
}
if (config.fontSize) {
ret.fontSize = config.fontSize;
}
if (config.fontSize) {
ret.fontSize = config.fontSize;
}
if (config.fontFamily) {
ret.fontFamily = config.fontFamily;
}
if (config.fontFamily) {
ret.fontFamily = config.fontFamily;
}
if (config.uiFontFamily) {
ret.uiFontFamily = config.uiFontFamily;
}
if (config.uiFontFamily) {
ret.uiFontFamily = config.uiFontFamily;
}
if (config.cursorColor) {
ret.cursorColor = config.cursorColor;
}
if (config.fontWeight) {
ret.fontWeight = config.fontWeight;
}
if (allowedCursorShapes.has(config.cursorShape)) {
ret.cursorShape = config.cursorShape;
}
if (config.fontWeightBold) {
ret.fontWeightBold = config.fontWeightBold;
}
if (allowedCursorBlinkValues.has(config.cursorBlink)) {
ret.cursorBlink = config.cursorBlink;
}
if (config.uiFontFamily) {
ret.uiFontFamily = config.uiFontFamily;
}
if (config.borderColor) {
ret.borderColor = config.borderColor;
}
if (config.cursorColor) {
ret.cursorColor = config.cursorColor;
}
if (typeof (config.padding) !== 'undefined' &&
config.padding !== null) {
ret.padding = config.padding;
}
if (config.cursorAccentColor) {
ret.cursorAccentColor = config.cursorAccentColor;
}
if (config.foregroundColor) {
ret.foregroundColor = config.foregroundColor;
}
if (allowedCursorShapes.has(config.cursorShape)) {
ret.cursorShape = config.cursorShape;
}
if (config.backgroundColor) {
ret.backgroundColor = config.backgroundColor;
}
if (allowedCursorBlinkValues.has(config.cursorBlink)) {
ret.cursorBlink = config.cursorBlink;
}
if (config.css) {
ret.css = config.css;
}
if (config.borderColor) {
ret.borderColor = config.borderColor;
}
if (config.termCSS) {
ret.termCSS = config.termCSS;
}
if (config.selectionColor) {
ret.selectionColor = config.selectionColor;
}
if (allowedBells.has(config.bell)) {
ret.bell = config.bell;
}
if (typeof config.padding !== 'undefined' && config.padding !== null) {
ret.padding = config.padding;
}
if (config.bellSoundURL) {
ret.bellSoundURL = config.bellSoundURL || initial.bellSoundURL;
}
if (config.foregroundColor) {
ret.foregroundColor = config.foregroundColor;
}
if (typeof (config.copyOnSelect) !== 'undefined' &&
config.copyOnSelect !== null) {
ret.copyOnSelect = config.copyOnSelect;
}
if (config.backgroundColor) {
ret.backgroundColor = config.backgroundColor;
}
if (config.colors) {
if (Array.isArray(config.colors)) {
const stateColors = Array.isArray(state.colors) ?
state.colors :
values(state.colors);
if (config.css || config.css === '') {
ret.css = config.css;
}
if (stateColors.toString() !== config.colors.toString()) {
if (config.termCSS) {
ret.termCSS = config.termCSS;
}
if (allowedBells.has(config.bell)) {
ret.bell = config.bell;
}
if (config.bellSoundURL) {
ret.bellSoundURL = config.bellSoundURL || initial.bellSoundURL;
}
if (typeof config.copyOnSelect !== 'undefined' && config.copyOnSelect !== null) {
ret.copyOnSelect = config.copyOnSelect;
}
if (config.colors) {
if (JSON.stringify(state.colors) !== JSON.stringify(config.colors)) {
ret.colors = config.colors;
}
} else if (JSON.stringify(state.colors) !== JSON.stringify(config.colors)) {
ret.colors = config.colors;
}
}
if (config.modifierKeys) {
ret.modifierKeys = config.modifierKeys;
}
if (config.modifierKeys) {
ret.modifierKeys = config.modifierKeys;
}
if (allowedHamburgerMenuValues.has(config.showHamburgerMenu)) {
ret.showHamburgerMenu = config.showHamburgerMenu;
}
if (allowedHamburgerMenuValues.has(config.showHamburgerMenu)) {
ret.showHamburgerMenu = config.showHamburgerMenu;
}
if (allowedWindowControlsValues.has(config.showWindowControls)) {
ret.showWindowControls = config.showWindowControls;
}
if (allowedWindowControlsValues.has(config.showWindowControls)) {
ret.showWindowControls = config.showWindowControls;
}
if (process.platform === 'win32' &&
(config.quickEdit === undefined || config.quickEdit === null)) {
ret.quickEdit = true;
} else if (typeof (config.quickEdit) !== 'undefined' &&
config.quickEdit !== null) {
ret.quickEdit = config.quickEdit;
}
if (process.platform === 'win32' && (config.quickEdit === undefined || config.quickEdit === null)) {
ret.quickEdit = true;
} else if (typeof config.quickEdit !== 'undefined' && config.quickEdit !== null) {
ret.quickEdit = config.quickEdit;
}
return ret;
})());
ret._lastUpdate = now;
return ret;
})()
);
break;
case SESSION_ADD:
state_ = state.merge({
activeUid: action.uid,
openAt: {
[action.uid]: action.now
}
}, {deep: true});
state_ = state.merge(
{
activeUid: action.uid,
openAt: {
[action.uid]: action.now
}
},
{deep: true}
);
break;
case SESSION_RESIZE:
@ -250,15 +270,19 @@ const reducer = (state = initial, action) => {
break;
case SESSION_SET_ACTIVE:
state_ = state.merge({
activeUid: action.uid,
activityMarkers: {
[action.uid]: false
}
}, {deep: true});
state_ = state.merge(
{
activeUid: action.uid,
activityMarkers: {
[action.uid]: false
}
},
{deep: true}
);
break;
case SESSION_PTY_DATA: // eslint-disable-line no-case-declarations
// eslint-disable-next-line no-case-declarations
case SESSION_PTY_DATA:
// ignore activity markers for current tab
if (action.uid === state.activeUid) {
break;
@ -274,11 +298,14 @@ const reducer = (state = initial, action) => {
// expect to get data packets from the resize
// of the ptys as a result
if (!state.resizeAt || action.now - state.resizeAt > 1000) {
state_ = state.merge({
activityMarkers: {
[action.uid]: true
}
}, {deep: true});
state_ = state.merge(
{
activityMarkers: {
[action.uid]: true
}
},
{deep: true}
);
}
break;
@ -315,11 +342,14 @@ const reducer = (state = initial, action) => {
break;
case NOTIFICATION_DISMISS:
state_ = state.merge({
notifications: {
[action.id]: false
}
}, {deep: true});
state_ = state.merge(
{
notifications: {
[action.id]: false
}
},
{deep: true}
);
break;
case NOTIFICATION_MESSAGE:
@ -333,22 +363,26 @@ const reducer = (state = initial, action) => {
case UPDATE_AVAILABLE:
state_ = state.merge({
updateVersion: action.version,
updateNotes: action.notes || ''
updateNotes: action.notes || '',
updateReleaseUrl: action.releaseUrl,
updateCanInstall: !!action.canInstall
});
break;
}
// Show a notification if any of the font size values have changed
if (CONFIG_LOAD !== action.type) {
if (state_.fontSize !== state.fontSize ||
state_.fontSizeOverride !== state.fontSizeOverride) {
if (state_.fontSize !== state.fontSize || state_.fontSizeOverride !== state.fontSizeOverride) {
state_ = state_.merge({notifications: {font: true}}, {deep: true});
}
}
if ((typeof (state.cols) !== 'undefined' && state.cols !== null) &&
(typeof (state.rows) !== 'undefined' && state.rows !== null) &&
(state.rows !== state_.rows || state.cols !== state_.cols)) {
if (
typeof state.cols !== 'undefined' &&
state.cols !== null &&
(typeof state.rows !== 'undefined' && state.rows !== null) &&
(state.rows !== state_.rows || state.cols !== state_.cols)
) {
state_ = state_.merge({notifications: {resize: true}}, {deep: true});
}

View file

@ -1,9 +1,8 @@
import {createSelector} from 'reselect';
const getTermGroups = ({termGroups}) => termGroups.termGroups;
const getRootGroups = createSelector(
getTermGroups,
termGroups => Object.keys(termGroups)
const getRootGroups = createSelector(getTermGroups, termGroups =>
Object.keys(termGroups)
.map(uid => termGroups[uid])
.filter(({parentUid}) => !parentUid)
);

View file

@ -1,31 +1,15 @@
import {createStore, applyMiddleware, compose} from 'redux';
import thunk from 'redux-thunk';
import {createLogger} from 'redux-logger';
import rootReducer from '../reducers/index';
import effects from '../utils/effects';
import * as plugins from '../utils/plugins';
import writeMiddleware from './write-middleware';
export default () => {
const logger = createLogger({
level: 'info',
collapsed: true
});
const enhancer = compose(
applyMiddleware(
thunk,
plugins.middleware,
thunk,
effects,
writeMiddleware,
logger
),
applyMiddleware(thunk, plugins.middleware, thunk, writeMiddleware, effects),
window.devToolsExtension()
);
return createStore(
rootReducer,
enhancer
);
return createStore(rootReducer, enhancer);
};

View file

@ -6,13 +6,4 @@ import * as plugins from '../utils/plugins';
import writeMiddleware from './write-middleware';
export default () =>
createStore(
rootReducer,
applyMiddleware(
thunk,
plugins.middleware,
thunk,
effects,
writeMiddleware
)
);
createStore(rootReducer, applyMiddleware(thunk, plugins.middleware, thunk, writeMiddleware, effects));

View file

@ -7,7 +7,7 @@ export default () => next => action => {
if (action.type === 'SESSION_PTY_DATA') {
const term = terms[action.uid];
if (term) {
term.write(action.data);
term.term.write(action.data);
}
}
next(action);

View file

@ -1,31 +0,0 @@
const colorList = [
'black',
'red',
'green',
'yellow',
'blue',
'magenta',
'cyan',
'white',
'lightBlack',
'lightRed',
'lightGreen',
'lightYellow',
'lightBlue',
'lightMagenta',
'lightCyan',
'lightWhite',
'colorCubes',
'grayscale'
];
export default function getColorList(colors) {
// For backwards compatibility, return early if it's already an array
if (Array.isArray(colors)) {
return colors;
}
return colorList.map(colorName => {
return colors[colorName];
});
}

View file

@ -15,9 +15,5 @@ export default function isExecutable(fileStat) {
return true;
}
return Boolean(
(fileStat.mode & 0o0001) ||
(fileStat.mode & 0o0010) ||
(fileStat.mode & 0o0100)
);
return Boolean(fileStat.mode & 0o0001 || fileStat.mode & 0o0010 || fileStat.mode & 0o0100);
}

View file

@ -1,79 +0,0 @@
/**
* Keyboard event keyCodes have proven to be really unreliable.
* This util function will cover most of the edge cases where
* String.fromCharCode() doesn't work.
*/
const _toAscii = {
188: '44',
109: '45',
190: '46',
191: '47',
192: '96',
220: '92',
222: '39',
221: '93',
219: '91',
173: '45',
187: '61', // IE Key codes
186: '59', // IE Key codes
189: '45' // IE Key codes
};
const _shiftUps = {
96: '~',
49: '!',
50: '@',
51: '#',
52: '$',
53: '%',
54: '^',
55: '&',
56: '*',
57: '(',
48: ')',
45: '_',
61: '+',
91: '{',
93: '}',
92: '|',
59: ':',
39: '\'',
44: '<',
46: '>',
47: '?'
};
const _arrowKeys = {
38: '',
40: '',
39: '',
37: ''
};
/**
* This fn takes a keyboard event and returns
* the character that was pressed. This fn
* purposely doesn't take into account if the alt/meta
* key was pressed.
*/
export default function fromCharCode(e) {
let code = String(e.which);
if ({}.hasOwnProperty.call(_arrowKeys, code)) {
return _arrowKeys[code];
}
if ({}.hasOwnProperty.call(_toAscii, code)) {
code = _toAscii[code];
}
const char = String.fromCharCode(code);
if (e.shiftKey) {
if ({}.hasOwnProperty.call(_shiftUps, code)) {
return _shiftUps[code];
}
return char.toUpperCase();
}
return char.toLowerCase();
}

View file

@ -1,34 +0,0 @@
import {remote} from 'electron';
const getCommand = remote.require('./utils/keymaps/get-command');
export default function returnKey(e) {
let keys = [];
if (e.metaKey && process.platform === 'darwin') {
keys.push('cmd');
} else if (e.metaKey) {
keys.push(e.key);
}
if (e.ctrlKey) {
keys.push('ctrl');
}
if (e.shiftKey) {
keys.push('shift');
}
if (e.altKey) {
keys.push('alt');
}
if (e.key === ' ') {
keys.push('space');
} else if (e.key !== 'Meta' && e.key !== 'Control' && e.key !== 'Shift' && e.key !== 'Alt') {
keys.push(e.key.replace('Arrow', ''));
}
keys = keys.join('+');
return getCommand(keys);
}

View file

@ -1,6 +1,7 @@
/* global Notification */
/* eslint no-new:0 */
export default function notify(title, body) {
//eslint-disable-next-line no-console
console.log(`[Notification] ${title}: ${body}`);
new Notification(title, {body});
}

21
lib/utils/paste.js Normal file
View file

@ -0,0 +1,21 @@
import {clipboard} from 'electron';
const getPath = platform => {
switch (platform) {
case 'darwin': {
const filepath = clipboard.read('public.file-url');
return filepath.replace('file://', '');
}
case 'win32': {
const filepath = clipboard.read('FileNameW');
return filepath.replace(new RegExp(String.fromCharCode(0), 'g'), '');
}
// Linux already pastes full path
default:
return null;
}
};
export default function processClipboard() {
return getPath(process.platform);
}

View file

@ -1,25 +1,26 @@
import {remote} from 'electron';
import {connect as reduxConnect} from 'react-redux';
import {basename} from 'path';
// patching Module._load
// so plugins can `require` them wihtout needing their own version
// so plugins can `require` them without needing their own version
// https://github.com/zeit/hyper/issues/619
import React from 'react';
import React, {PureComponent} from 'react';
import ReactDOM from 'react-dom';
import Component from '../component';
import Notification from '../components/notification';
import notify from './notify';
const Module = require('module'); // eslint-disable-line import/newline-after-import
//eslint-disable-next-line import/newline-after-import
const Module = require('module');
const originalLoad = Module._load;
Module._load = function (path) {
Module._load = function _load(path) {
switch (path) {
case 'react':
return React;
case 'react-dom':
return ReactDOM;
case 'hyper/component':
return Component;
return PureComponent;
case 'hyper/notify':
return notify;
case 'hyper/Notification':
@ -53,10 +54,10 @@ let termGroupPropsDecorators;
let propsDecorators;
let reducersDecorators;
// the fs locations where user plugins are stored
const {path, localPath} = plugins.getBasePaths();
const clearModulesCache = () => {
// the fs locations where user plugins are stored
const {path, localPath} = plugins.getBasePaths();
// trigger unload hooks
modules.forEach(mod => {
if (mod.onRendererUnload) {
@ -82,12 +83,14 @@ const getPluginVersion = path => {
try {
version = window.require(pathModule.resolve(path, 'package.json')).version;
} catch (err) {
//eslint-disable-next-line no-console
console.warn(`No package.json found in ${path}`);
}
return version;
};
const loadModules = () => {
//eslint-disable-next-line no-console
console.log('(re)loading renderer plugins');
const paths = plugins.getPaths();
@ -120,7 +123,10 @@ const loadModules = () => {
reduceTermGroups: termGroupsReducers
};
modules = paths.plugins.concat(paths.localPlugins)
const loadedPlugins = plugins.getLoadedPluginVersions().map(plugin => plugin.name);
modules = paths.plugins
.concat(paths.localPlugins)
.filter(plugin => loadedPlugins.indexOf(basename(plugin)) !== -1)
.map(path => {
let mod;
const pluginName = getPluginName(path);
@ -131,8 +137,12 @@ const loadModules = () => {
try {
mod = window.require(path);
} catch (err) {
//eslint-disable-next-line no-console
console.error(err.stack);
notify('Plugin load error', `"${pluginName}" failed to load in the renderer process. Check Developer Tools for details.`);
notify(
'Plugin load error',
`"${pluginName}" failed to load in the renderer process. Check Developer Tools for details.`
);
return undefined;
}
@ -146,12 +156,14 @@ const loadModules = () => {
// mapHyperTermState mapping for backwards compatibility with hyperterm
if (mod.mapHyperTermState) {
mod.mapHyperState = mod.mapHyperTermState;
//eslint-disable-next-line no-console
console.error('mapHyperTermState is deprecated. Use mapHyperState instead.');
}
// mapHyperTermDispatch mapping for backwards compatibility with hyperterm
if (mod.mapHyperTermDispatch) {
mod.mapHyperDispatch = mod.mapHyperTermDispatch;
//eslint-disable-next-line no-console
console.error('mapHyperTermDispatch is deprecated. Use mapHyperDispatch instead.');
}
@ -222,12 +234,21 @@ const loadModules = () => {
if (mod.onRendererWindow) {
mod.onRendererWindow(window);
}
//eslint-disable-next-line no-console
console.log(`Plugin ${pluginName} (${pluginVersion}) loaded.`);
return mod;
})
.filter(mod => Boolean(mod));
const deprecatedPlugins = plugins.getDeprecatedConfig();
Object.keys(deprecatedPlugins).forEach(name => {
const {css} = deprecatedPlugins[name];
if (css) {
//eslint-disable-next-line no-console
console.warn(`Warning: "${name}" plugin uses some deprecated CSS classes (${css.join(', ')}).`);
}
});
};
// load modules for initial decoration
@ -255,6 +276,7 @@ function getProps(name, props, ...fnArgs) {
try {
ret_ = fn(...fnArgs, props_);
} catch (err) {
//eslint-disable-next-line no-console
console.error(err.stack);
notify('Plugin error', `${fn._pluginName}: Error occurred in \`${name}\`. Check Developer Tools for details.`);
return;
@ -301,8 +323,12 @@ export function connect(stateFn, dispatchFn, c, d = {}) {
try {
ret_ = fn(state, ret);
} catch (err) {
//eslint-disable-next-line no-console
console.error(err.stack);
notify('Plugin error', `${fn._pluginName}: Error occurred in \`map${name}State\`. Check Developer Tools for details.`);
notify(
'Plugin error',
`${fn._pluginName}: Error occurred in \`map${name}State\`. Check Developer Tools for details.`
);
return;
}
@ -323,13 +349,20 @@ export function connect(stateFn, dispatchFn, c, d = {}) {
try {
ret_ = fn(dispatch, ret);
} catch (err) {
//eslint-disable-next-line no-console
console.error(err.stack);
notify('Plugin error', `${fn._pluginName}: Error occurred in \`map${name}Dispatch\`. Check Developer Tools for details.`);
notify(
'Plugin error',
`${fn._pluginName}: Error occurred in \`map${name}Dispatch\`. Check Developer Tools for details.`
);
return;
}
if (!ret_ || typeof ret_ !== 'object') {
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`map${name}Dispatch\` (object expected).`);
notify(
'Plugin error',
`${fn._pluginName}: Invalid return value of \`map${name}Dispatch\` (object expected).`
);
return;
}
@ -354,6 +387,7 @@ function decorateReducer(name, fn) {
try {
state__ = pluginReducer(state_, action);
} catch (err) {
//eslint-disable-next-line no-console
console.error(err.stack);
notify('Plugin error', `${fn._pluginName}: Error occurred in \`${name}\`. Check Developer Tools for details.`);
return;
@ -385,26 +419,29 @@ export function decorateSessionsReducer(fn) {
// 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);
const nextMiddleware = remaining => action_ =>
remaining.length ? remaining[0](store)(nextMiddleware(remaining.slice(1)))(action_) : next(action_);
nextMiddleware(middlewares)(action);
};
// expose decorated component instance to the higher-order components
function exposeDecorated(Component) {
return class extends React.Component {
function exposeDecorated(Component_) {
return class DecoratedComponent extends React.Component {
constructor(props, context) {
super(props, context);
this.onRef = this.onRef.bind(this);
}
onRef(decorated) {
onRef(decorated_) {
if (this.props.onDecorated) {
this.props.onDecorated(decorated);
try {
this.props.onDecorated(decorated_);
} catch (e) {
notify('Plugin error', `Error occurred. Check Developer Tools for details`);
}
}
}
render() {
return React.createElement(Component, Object.assign({}, this.props, {ref: this.onRef}));
return React.createElement(Component_, Object.assign({}, this.props, {ref: this.onRef}));
}
};
}
@ -422,16 +459,25 @@ function getDecorated(parent, name) {
let class__;
try {
class__ = fn(class_, {React, Component, Notification, notify});
class__ = fn(class_, {React, PureComponent, Notification, notify});
class__.displayName = `${fn._pluginName}(${name})`;
} catch (err) {
//eslint-disable-next-line no-console
console.error(err.stack);
notify('Plugin error', `${fn._pluginName}: Error occurred in \`${method}\`. Check Developer Tools for details`);
notify(
'Plugin error',
`${fn._pluginName}: Error occurred in \`${method}\`. Check Developer Tools for details`
);
return;
}
if (!class__ || typeof class__.prototype.render !== 'function') {
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`${method}\`. No \`render\` method found. Please return a \`React.Component\`.`);
notify(
'Plugin error',
`${
fn._pluginName
}: Invalid return value of \`${method}\`. No \`render\` method found. Please return a \`React.Component\`.`
);
return;
}
@ -448,10 +494,22 @@ function getDecorated(parent, name) {
// for each component, we return a higher-order component
// that wraps with the higher-order components
// exposed by plugins
export function decorate(Component, name) {
return class extends React.Component {
export function decorate(Component_, name) {
return class DecoratedComponent extends React.Component {
constructor(props) {
super(props);
this.state = {hasError: false};
}
componentDidCatch() {
this.setState({hasError: true});
// No need to detail this error because React print those informations.
notify(
'Plugin error',
`Plugins decorating ${name} has been disabled because of a plugin crash. Check Developer Tools for details.`
);
}
render() {
const Sub = getDecorated(Component, name);
const Sub = this.state.hasError ? Component_ : getDecorated(Component_, name);
return React.createElement(Sub, this.props);
}
};

View file

@ -1,5 +1,4 @@
export default class Client {
constructor() {
const electron = window.require('electron');
const EventEmitter = window.require('events');
@ -56,5 +55,4 @@ export default class Client {
this.removeAllListeners();
this.ipc.removeAllListeners();
}
}

View file

@ -1,11 +1,11 @@
// Clear selection range of current selected term view
// Fix event when terminal text is selected and keyboard action is invoked
exports.clear = function (terminal) {
exports.clear = terminal => {
terminal.document_.getSelection().removeAllRanges();
};
// Use selection extend upon dblclick
exports.extend = function (terminal) {
exports.extend = terminal => {
const sel = terminal.document_.getSelection();
// Test if focusNode exist and nodeName is #text
@ -19,7 +19,7 @@ exports.extend = function (terminal) {
// Fix a bug in ScrollPort selectAll behavior
// Select all rows in the viewport
exports.all = function (terminal) {
exports.all = terminal => {
const scrollPort = terminal.scrollPort_;
let firstRow;
let lastRow;

View file

@ -2,7 +2,7 @@ import path from 'path';
import * as regex from './url-regex';
export default function isUrlCommand(shell, data) {
const matcher = regex[path.parse(shell).name]; // eslint-disable-line import/namespace
const matcher = regex[path.parse(shell).name];
if (undefined === matcher || !data) {
return null;
}