Fixed merge conflicts
This commit is contained in:
parent
4d7ce43755
commit
4ce99863c4
133 changed files with 74506 additions and 53064 deletions
|
|
@ -8,8 +8,10 @@ export function loadConfig(config) {
|
|||
}
|
||||
|
||||
export function reloadConfig(config) {
|
||||
const now = Date.now();
|
||||
return {
|
||||
type: CONFIG_RELOAD,
|
||||
config
|
||||
config,
|
||||
now
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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'}}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
144
lib/components/style-sheet.js
Normal file
144
lib/components/style-sheet.js
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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_, {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
},
|
||||
|
|
|
|||
492
lib/hterm.js
492
lib/hterm.js
|
|
@ -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};
|
||||
71
lib/index.js
71
lib/index.js
|
|
@ -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')
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: '[A',
|
||||
40: '[B',
|
||||
39: '[C',
|
||||
37: '[D'
|
||||
};
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
21
lib/utils/paste.js
Normal 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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue