mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-17 05:58:41 -09:00
Split Panes (#693)
* npm: add .npmrc with save-exact=true * split panes: create initial implementation This allows users to split their Hyperterm terms into multiple nested splits, both vertical and horizontal. Fixes #56 * split panes: suport closing tabs and individual panes * split panes: ensure new splits are placed at the correct index New split panes should be placed after the currently active pane, not at the end like they were previously. * split panes: add explicit dependency to uuid * split panes: implement split pane cycling This adds menu buttons for moving back and forward between open split panes in the currect terminal tab. Doesn't add a hotkey yet, needs some bikeshedding. * split panes: move activeSessionUid to its own object It made little sense to have so many objects with `activeSessionUid` set to `null` when it only mattered on the top level. Now it's an object mapping term-group `uid` to `sessionUid` instead. * split panes: make sure closing the last split pane exits the app * split panes: fix a crash after closing specific panes Sometimes the terminal would crash when a specific split pane was closed, because the `activeSessions` mapping wasn't updated correctly. * split panes: fix a bug that caused initial session sizing to be wrong * fix all our focus / blur issues in one fell swoop :O (famous last words) * get rid of react warning * hterm: make sure not to lose focus when VT listens on clicks * term: restore onactive callback * add missing `return` to override (just in case) * split pane: new split pane implementation * goodbye react-split-pane * added term group resizing action and reducer * terms: supply border color so that we can use it for splits * term-group: add resizing hook * term-groups: add resizing constant * remove split pane css side-effect * split panes: pass existing hterm instances to Term * split panes: add keybindings for split pane cycling * split panes: remove unused action * split panes: remove unused styling * split-pane: remove `console.log` * split-pane: remove `console.log` * split panes: rebalance sizes on insert/removal * split panes: pass existing hterm instances to Term * split panes: add keybindings for split pane cycling * split panes: remove unused action * split panes: remove unused styling * split panes: rebalance sizes on insert/removal * split panes: set a minimum size for resizing * split-pane: fix vertical splits * css :| * package: bump electron * split panes: attach onFocus listener to webviews * 1.4.1 and 1.4.2 are broken. they have the following regression: - open google.com on the main window - open a new tab - come back to previous tab. webview is gone :| * split panes: handle PTY exits * split panes: add linux friendly keybindings
This commit is contained in:
parent
ee172c3164
commit
a7595c1a45
29 changed files with 1171 additions and 211 deletions
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
save-exact=true
|
||||||
1
app/.npmrc
Normal file
1
app/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
save-exact=true
|
||||||
14
app/index.js
14
app/index.js
|
|
@ -155,7 +155,7 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
|
||||||
// If no callback is passed to createWindow,
|
// If no callback is passed to createWindow,
|
||||||
// a new session will be created by default.
|
// a new session will be created by default.
|
||||||
if (!fn) {
|
if (!fn) {
|
||||||
fn = win => win.rpc.emit('session add req');
|
fn = win => win.rpc.emit('termgroup add req');
|
||||||
}
|
}
|
||||||
|
|
||||||
// app.windowCallback is the createWindow callback
|
// app.windowCallback is the createWindow callback
|
||||||
|
|
@ -173,14 +173,17 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
rpc.on('new', ({rows = 40, cols = 100, cwd = process.env.HOME}) => {
|
rpc.on('new', ({rows = 40, cols = 100, cwd = process.env.HOME, splitDirection}) => {
|
||||||
const shell = cfg.shell;
|
const shell = cfg.shell;
|
||||||
const shellArgs = cfg.shellArgs && Array.from(cfg.shellArgs);
|
const shellArgs = cfg.shellArgs && Array.from(cfg.shellArgs);
|
||||||
|
|
||||||
initSession({rows, cols, cwd, shell, shellArgs}, (uid, session) => {
|
initSession({rows, cols, cwd, shell, shellArgs}, (uid, session) => {
|
||||||
sessions.set(uid, session);
|
sessions.set(uid, session);
|
||||||
rpc.emit('session add', {
|
rpc.emit('session add', {
|
||||||
|
rows,
|
||||||
|
cols,
|
||||||
uid,
|
uid,
|
||||||
|
splitDirection,
|
||||||
shell: session.shell,
|
shell: session.shell,
|
||||||
pid: session.pty.pid
|
pid: session.pty.pid
|
||||||
});
|
});
|
||||||
|
|
@ -242,10 +245,9 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
|
||||||
win.maximize();
|
win.maximize();
|
||||||
});
|
});
|
||||||
|
|
||||||
rpc.on('resize', ({cols, rows}) => {
|
rpc.on('resize', ({uid, cols, rows}) => {
|
||||||
sessions.forEach(session => {
|
const session = sessions.get(uid);
|
||||||
session.resize({cols, rows});
|
session.resize({cols, rows});
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
rpc.on('data', ({uid, data}) => {
|
rpc.on('data', ({uid, data}) => {
|
||||||
|
|
|
||||||
46
app/menu.js
46
app/menu.js
|
|
@ -73,7 +73,7 @@ module.exports = function createMenu({createWindow, updatePlugins}) {
|
||||||
accelerator: 'CmdOrCtrl+T',
|
accelerator: 'CmdOrCtrl+T',
|
||||||
click(item, focusedWindow) {
|
click(item, focusedWindow) {
|
||||||
if (focusedWindow) {
|
if (focusedWindow) {
|
||||||
focusedWindow.rpc.emit('session add req');
|
focusedWindow.rpc.emit('termgroup add req');
|
||||||
} else {
|
} else {
|
||||||
createWindow();
|
createWindow();
|
||||||
}
|
}
|
||||||
|
|
@ -82,12 +82,33 @@ module.exports = function createMenu({createWindow, updatePlugins}) {
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Split Vertically',
|
||||||
|
accelerator: 'Ctrl+Shift+E',
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
if (focusedWindow) {
|
||||||
|
focusedWindow.rpc.emit('split request vertical');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Split Horizontally',
|
||||||
|
accelerator: 'Ctrl+Shift+O',
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
if (focusedWindow) {
|
||||||
|
focusedWindow.rpc.emit('split request horizontal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Close',
|
label: 'Close',
|
||||||
accelerator: 'CmdOrCtrl+W',
|
accelerator: 'CmdOrCtrl+W',
|
||||||
click(item, focusedWindow) {
|
click(item, focusedWindow) {
|
||||||
if (focusedWindow) {
|
if (focusedWindow) {
|
||||||
focusedWindow.rpc.emit('session close req');
|
focusedWindow.rpc.emit('termgroup close req');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -264,6 +285,27 @@ module.exports = function createMenu({createWindow, updatePlugins}) {
|
||||||
{
|
{
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Select Next Pane',
|
||||||
|
accelerator: 'Ctrl+Alt+Tab',
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
if (focusedWindow) {
|
||||||
|
focusedWindow.rpc.emit('next pane req');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Select Previous Pane',
|
||||||
|
accelerator: 'Ctrl+Shift+Alt+Tab',
|
||||||
|
click(item, focusedWindow) {
|
||||||
|
if (focusedWindow) {
|
||||||
|
focusedWindow.rpc.emit('prev pane req');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
role: 'front'
|
role: 'front'
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import {CLOSE_TAB, CHANGE_TAB} from '../constants/tabs';
|
import {CLOSE_TAB, CHANGE_TAB} from '../constants/tabs';
|
||||||
import {UI_WINDOW_MAXIMIZE, UI_WINDOW_UNMAXIMIZE} from '../constants/ui';
|
import {UI_WINDOW_MAXIMIZE, UI_WINDOW_UNMAXIMIZE} from '../constants/ui';
|
||||||
import rpc from '../rpc';
|
import rpc from '../rpc';
|
||||||
|
import {userExitTermGroup, setActiveGroup} from './term-groups';
|
||||||
import {userExitSession, setActiveSession} from './sessions';
|
|
||||||
|
|
||||||
export function closeTab(uid) {
|
export function closeTab(uid) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
|
|
@ -10,7 +9,7 @@ export function closeTab(uid) {
|
||||||
type: CLOSE_TAB,
|
type: CLOSE_TAB,
|
||||||
uid,
|
uid,
|
||||||
effect() {
|
effect() {
|
||||||
dispatch(userExitSession(uid));
|
dispatch(userExitTermGroup(uid));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -22,7 +21,7 @@ export function changeTab(uid) {
|
||||||
type: CHANGE_TAB,
|
type: CHANGE_TAB,
|
||||||
uid,
|
uid,
|
||||||
effect() {
|
effect() {
|
||||||
dispatch(setActiveSession(uid));
|
dispatch(setActiveGroup(uid));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import rpc from '../rpc';
|
import rpc from '../rpc';
|
||||||
import getURL from '../utils/url-command';
|
import getURL from '../utils/url-command';
|
||||||
import {keys} from '../utils/object';
|
import {keys} from '../utils/object';
|
||||||
|
import {findBySession} from '../utils/term-groups';
|
||||||
import {
|
import {
|
||||||
SESSION_ADD,
|
SESSION_ADD,
|
||||||
SESSION_RESIZE,
|
SESSION_RESIZE,
|
||||||
|
|
@ -9,7 +10,6 @@ import {
|
||||||
SESSION_PTY_DATA,
|
SESSION_PTY_DATA,
|
||||||
SESSION_PTY_EXIT,
|
SESSION_PTY_EXIT,
|
||||||
SESSION_USER_EXIT,
|
SESSION_USER_EXIT,
|
||||||
SESSION_USER_EXIT_ACTIVE,
|
|
||||||
SESSION_SET_ACTIVE,
|
SESSION_SET_ACTIVE,
|
||||||
SESSION_CLEAR_ACTIVE,
|
SESSION_CLEAR_ACTIVE,
|
||||||
SESSION_USER_DATA,
|
SESSION_USER_DATA,
|
||||||
|
|
@ -19,13 +19,18 @@ import {
|
||||||
SESSION_SET_PROCESS_TITLE
|
SESSION_SET_PROCESS_TITLE
|
||||||
} from '../constants/sessions';
|
} from '../constants/sessions';
|
||||||
|
|
||||||
export function addSession(uid, shell, pid) {
|
export function addSession({uid, shell, pid, cols, rows, splitDirection}) {
|
||||||
return dispatch => {
|
return (dispatch, getState) => {
|
||||||
|
const {sessions} = getState();
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SESSION_ADD,
|
type: SESSION_ADD,
|
||||||
uid,
|
uid,
|
||||||
shell,
|
shell,
|
||||||
pid
|
pid,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
splitDirection,
|
||||||
|
activeUid: sessions.activeUid
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -72,15 +77,16 @@ export function addSessionData(uid, data) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sessionExit(uid) {
|
function createExitAction(type) {
|
||||||
return (dispatch, getState) => {
|
return uid => (dispatch, getState) => {
|
||||||
return dispatch({
|
return dispatch({
|
||||||
type: SESSION_PTY_EXIT,
|
type,
|
||||||
uid,
|
uid,
|
||||||
effect() {
|
effect() {
|
||||||
// we reiterate the same logic as below
|
if (type === SESSION_USER_EXIT) {
|
||||||
// for SESSION_USER_EXIT since the exit
|
rpc.emit('exit', {uid});
|
||||||
// could happen pty side or optimistic
|
}
|
||||||
|
|
||||||
const sessions = keys(getState().sessions.sessions);
|
const sessions = keys(getState().sessions.sessions);
|
||||||
if (!sessions.length) {
|
if (!sessions.length) {
|
||||||
window.close();
|
window.close();
|
||||||
|
|
@ -92,33 +98,8 @@ export function sessionExit(uid) {
|
||||||
|
|
||||||
// we want to distinguish an exit
|
// we want to distinguish an exit
|
||||||
// that's UI initiated vs pty initiated
|
// that's UI initiated vs pty initiated
|
||||||
export function userExitSession(uid) {
|
export const userExitSession = createExitAction(SESSION_USER_EXIT);
|
||||||
return (dispatch, getState) => {
|
export const ptyExitSession = createExitAction(SESSION_PTY_EXIT);
|
||||||
return dispatch({
|
|
||||||
type: SESSION_USER_EXIT,
|
|
||||||
uid,
|
|
||||||
effect() {
|
|
||||||
rpc.emit('exit', {uid});
|
|
||||||
const sessions = keys(getState().sessions.sessions);
|
|
||||||
if (!sessions.length) {
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function userExitActiveSession() {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
dispatch({
|
|
||||||
type: SESSION_USER_EXIT_ACTIVE,
|
|
||||||
effect() {
|
|
||||||
const uid = getState().sessions.activeUid;
|
|
||||||
dispatch(userExitSession(uid));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setActiveSession(uid) {
|
export function setActiveSession(uid) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
|
|
@ -163,13 +144,20 @@ export function setSessionXtermTitle(uid, title) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resizeSession(uid, cols, rows) {
|
export function resizeSession(uid, cols, rows) {
|
||||||
return {
|
return (dispatch, getState) => {
|
||||||
type: SESSION_RESIZE,
|
const {termGroups} = getState();
|
||||||
cols,
|
const group = findBySession(termGroups, uid);
|
||||||
rows,
|
const isStandaloneTerm = !group.parentUid && !group.children.length;
|
||||||
effect() {
|
dispatch({
|
||||||
rpc.emit('resize', {cols, rows});
|
type: SESSION_RESIZE,
|
||||||
}
|
uid,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
isStandaloneTerm,
|
||||||
|
effect() {
|
||||||
|
rpc.emit('resize', {uid, cols, rows});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
173
lib/actions/term-groups.js
Normal file
173
lib/actions/term-groups.js
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
import rpc from '../rpc';
|
||||||
|
import {
|
||||||
|
DIRECTION,
|
||||||
|
TERM_GROUP_RESIZE,
|
||||||
|
TERM_GROUP_REQUEST,
|
||||||
|
TERM_GROUP_EXIT,
|
||||||
|
TERM_GROUP_EXIT_ACTIVE
|
||||||
|
} from '../constants/term-groups';
|
||||||
|
import {SESSION_REQUEST} from '../constants/sessions';
|
||||||
|
import {findBySession} from '../utils/term-groups';
|
||||||
|
import {getRootGroups} from '../selectors';
|
||||||
|
import {setActiveSession, ptyExitSession, userExitSession} from './sessions';
|
||||||
|
|
||||||
|
function requestSplit(direction) {
|
||||||
|
return () => (dispatch, getState) => {
|
||||||
|
const {ui} = getState();
|
||||||
|
dispatch({
|
||||||
|
type: SESSION_REQUEST,
|
||||||
|
effect: () => {
|
||||||
|
rpc.emit('new', {
|
||||||
|
splitDirection: direction,
|
||||||
|
cwd: ui.cwd
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requestVerticalSplit = requestSplit(DIRECTION.VERTICAL);
|
||||||
|
export const requestHorizontalSplit = requestSplit(DIRECTION.HORIZONTAL);
|
||||||
|
|
||||||
|
export function resizeTermGroup(uid, sizes) {
|
||||||
|
return {
|
||||||
|
uid,
|
||||||
|
type: TERM_GROUP_RESIZE,
|
||||||
|
sizes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestTermGroup() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const {ui} = getState();
|
||||||
|
const {cols, rows, cwd} = ui;
|
||||||
|
dispatch({
|
||||||
|
type: TERM_GROUP_REQUEST,
|
||||||
|
effect: () => {
|
||||||
|
rpc.emit('new', {
|
||||||
|
isNewGroup: true,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
cwd
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setActiveGroup(uid) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const {termGroups} = getState();
|
||||||
|
dispatch(setActiveSession(termGroups.activeSessions[uid]));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// When we've found the next group which we want to
|
||||||
|
// set as active (after closing something), we also need
|
||||||
|
// to find the first child group which has a sessionUid.
|
||||||
|
const findFirstSession = (state, group) => {
|
||||||
|
if (group.sessionUid) {
|
||||||
|
return group.sessionUid;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const childUid of group.children) {
|
||||||
|
const child = state.termGroups[childUid];
|
||||||
|
// We want to find the *leftmost* session,
|
||||||
|
// even if it's nested deep down:
|
||||||
|
const sessionUid = findFirstSession(state, child);
|
||||||
|
if (sessionUid) {
|
||||||
|
return sessionUid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const findPrevious = (list, old) => {
|
||||||
|
const index = list.indexOf(old);
|
||||||
|
// If `old` was the first item in the list,
|
||||||
|
// choose the other item available:
|
||||||
|
return index ? list[index - 1] : list[1];
|
||||||
|
};
|
||||||
|
|
||||||
|
const findNextSessionUid = (state, group) => {
|
||||||
|
// If we're closing a root group (i.e. a whole tab),
|
||||||
|
// the next group needs to be a root group as well:
|
||||||
|
if (state.activeRootGroup === group.uid) {
|
||||||
|
const rootGroups = getRootGroups({termGroups: state});
|
||||||
|
const nextGroup = findPrevious(rootGroups, group);
|
||||||
|
return findFirstSession(state, nextGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {children} = state.termGroups[group.parentUid];
|
||||||
|
const nextUid = findPrevious(children, group.uid);
|
||||||
|
return findFirstSession(state, state.termGroups[nextUid]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ptyExitTermGroup(sessionUid) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const {termGroups} = getState();
|
||||||
|
const group = findBySession(termGroups, sessionUid);
|
||||||
|
// This might have already been closed:
|
||||||
|
if (!group) {
|
||||||
|
return dispatch(ptyExitSession(sessionUid));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: TERM_GROUP_EXIT,
|
||||||
|
uid: group.uid,
|
||||||
|
effect: () => {
|
||||||
|
const activeSessionUid = termGroups.activeSessions[termGroups.activeRootGroup];
|
||||||
|
if (Object.keys(termGroups.termGroups).length > 1 && activeSessionUid === sessionUid) {
|
||||||
|
const nextSessionUid = findNextSessionUid(termGroups, group);
|
||||||
|
dispatch(setActiveSession(nextSessionUid));
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(ptyExitSession(sessionUid));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userExitTermGroup(uid) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const {termGroups} = getState();
|
||||||
|
dispatch({
|
||||||
|
type: TERM_GROUP_EXIT,
|
||||||
|
uid,
|
||||||
|
effect: () => {
|
||||||
|
const group = termGroups.termGroups[uid];
|
||||||
|
if (Object.keys(termGroups.termGroups).length <= 1) {
|
||||||
|
// No need to attempt finding a new active session
|
||||||
|
// if this is the last one we've got:
|
||||||
|
return dispatch(userExitSession(group.sessionUid));
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeSessionUid = termGroups.activeSessions[termGroups.activeRootGroup];
|
||||||
|
if (termGroups.activeRootGroup === uid || activeSessionUid === group.sessionUid) {
|
||||||
|
const nextSessionUid = findNextSessionUid(termGroups, group);
|
||||||
|
dispatch(setActiveSession(nextSessionUid));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.sessionUid) {
|
||||||
|
dispatch(userExitSession(group.sessionUid));
|
||||||
|
} else {
|
||||||
|
group.children.forEach(childUid => {
|
||||||
|
dispatch(userExitTermGroup(childUid));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exitActiveTermGroup() {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
|
type: TERM_GROUP_EXIT_ACTIVE,
|
||||||
|
effect() {
|
||||||
|
const {sessions, termGroups} = getState();
|
||||||
|
const {uid} = findBySession(termGroups, sessions.activeUid);
|
||||||
|
dispatch(userExitTermGroup(uid));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import * as shellEscape from 'php-escape-shell';
|
import * as shellEscape from 'php-escape-shell';
|
||||||
|
|
||||||
import {keys} from '../utils/object';
|
|
||||||
import {last} from '../utils/array';
|
import {last} from '../utils/array';
|
||||||
import {isExecutable} from '../utils/file';
|
import {isExecutable} from '../utils/file';
|
||||||
|
import {getRootGroups} from '../selectors';
|
||||||
|
import {findBySession} from '../utils/term-groups';
|
||||||
import notify from '../utils/notify';
|
import notify from '../utils/notify';
|
||||||
import rpc from '../rpc';
|
import rpc from '../rpc';
|
||||||
import {
|
import {
|
||||||
requestSession,
|
requestSession,
|
||||||
sendSessionData
|
sendSessionData,
|
||||||
|
setActiveSession
|
||||||
} from '../actions/sessions';
|
} from '../actions/sessions';
|
||||||
import {
|
import {
|
||||||
UI_FONT_SIZE_SET,
|
UI_FONT_SIZE_SET,
|
||||||
|
|
@ -18,12 +19,14 @@ import {
|
||||||
UI_MOVE_LEFT,
|
UI_MOVE_LEFT,
|
||||||
UI_MOVE_RIGHT,
|
UI_MOVE_RIGHT,
|
||||||
UI_MOVE_TO,
|
UI_MOVE_TO,
|
||||||
|
UI_MOVE_NEXT_PANE,
|
||||||
|
UI_MOVE_PREV_PANE,
|
||||||
UI_SHOW_PREFERENCES,
|
UI_SHOW_PREFERENCES,
|
||||||
UI_WINDOW_MOVE,
|
UI_WINDOW_MOVE,
|
||||||
UI_OPEN_FILE
|
UI_OPEN_FILE
|
||||||
} from '../constants/ui';
|
} from '../constants/ui';
|
||||||
|
|
||||||
import {setActiveSession} from './sessions';
|
import {setActiveGroup} from './term-groups';
|
||||||
|
|
||||||
const {stat} = window.require('fs');
|
const {stat} = window.require('fs');
|
||||||
|
|
||||||
|
|
@ -83,20 +86,73 @@ export function setFontSmoothing() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find all sessions that are below the given
|
||||||
|
// termGroup uid in the hierarchy:
|
||||||
|
const findChildSessions = (termGroups, uid) => {
|
||||||
|
const group = termGroups[uid];
|
||||||
|
if (group.sessionUid) {
|
||||||
|
return [uid];
|
||||||
|
}
|
||||||
|
|
||||||
|
return group
|
||||||
|
.children
|
||||||
|
.reduce((total, childUid) => total.concat(
|
||||||
|
findChildSessions(termGroups, childUid)
|
||||||
|
), []);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the index of the next or previous group,
|
||||||
|
// depending on the movement direction:
|
||||||
|
const getNeighborIndex = (groups, uid, type) => {
|
||||||
|
if (type === UI_MOVE_NEXT_PANE) {
|
||||||
|
return (groups.indexOf(uid) + 1) % groups.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (groups.indexOf(uid) + groups.length - 1) % groups.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
function moveToNeighborPane(type) {
|
||||||
|
return () => (dispatch, getState) => {
|
||||||
|
dispatch({
|
||||||
|
type,
|
||||||
|
effect() {
|
||||||
|
const {sessions, termGroups} = getState();
|
||||||
|
const {uid} = findBySession(termGroups, sessions.activeUid);
|
||||||
|
const childGroups = findChildSessions(termGroups.termGroups, termGroups.activeRootGroup);
|
||||||
|
if (childGroups.length === 1) {
|
||||||
|
console.log('ignoring move for single group');
|
||||||
|
} else {
|
||||||
|
const index = getNeighborIndex(childGroups, uid, type);
|
||||||
|
const {sessionUid} = termGroups.termGroups[childGroups[index]];
|
||||||
|
dispatch(setActiveSession(sessionUid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const moveToNextPane = moveToNeighborPane(UI_MOVE_NEXT_PANE);
|
||||||
|
export const moveToPreviousPane = moveToNeighborPane(UI_MOVE_PREV_PANE);
|
||||||
|
|
||||||
|
const getGroupUids = state => {
|
||||||
|
const rootGroups = getRootGroups(state);
|
||||||
|
return rootGroups.map(({uid}) => uid);
|
||||||
|
};
|
||||||
|
|
||||||
export function moveLeft() {
|
export function moveLeft() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UI_MOVE_LEFT,
|
type: UI_MOVE_LEFT,
|
||||||
effect() {
|
effect() {
|
||||||
const {sessions} = getState();
|
const state = getState();
|
||||||
const uid = sessions.activeUid;
|
const uid = state.termGroups.activeRootGroup;
|
||||||
const sessionUids = keys(sessions.sessions);
|
const groupUids = getGroupUids(state);
|
||||||
const index = sessionUids.indexOf(uid);
|
const index = groupUids.indexOf(uid);
|
||||||
const next = sessionUids[index - 1] || last(sessionUids);
|
const next = groupUids[index - 1] || last(groupUids);
|
||||||
if (!next || uid === next) {
|
if (!next || uid === next) {
|
||||||
console.log('ignoring left move action');
|
console.log('ignoring left move action');
|
||||||
} else {
|
} else {
|
||||||
dispatch(setActiveSession(next));
|
dispatch(setActiveGroup(next));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -108,15 +164,15 @@ export function moveRight() {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UI_MOVE_RIGHT,
|
type: UI_MOVE_RIGHT,
|
||||||
effect() {
|
effect() {
|
||||||
const {sessions} = getState();
|
const state = getState();
|
||||||
const uid = sessions.activeUid;
|
const groupUids = getGroupUids(state);
|
||||||
const sessionUids = keys(sessions.sessions);
|
const uid = state.termGroups.activeRootGroup;
|
||||||
const index = sessionUids.indexOf(uid);
|
const index = groupUids.indexOf(uid);
|
||||||
const next = sessionUids[index + 1] || sessionUids[0];
|
const next = groupUids[index + 1] || groupUids[0];
|
||||||
if (!next || uid === next) {
|
if (!next || uid === next) {
|
||||||
console.log('ignoring right move action');
|
console.log('ignoring right move action');
|
||||||
} else {
|
} else {
|
||||||
dispatch(setActiveSession(next));
|
dispatch(setActiveGroup(next));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -129,15 +185,15 @@ export function moveTo(i) {
|
||||||
type: UI_MOVE_TO,
|
type: UI_MOVE_TO,
|
||||||
index: i,
|
index: i,
|
||||||
effect() {
|
effect() {
|
||||||
const {sessions} = getState();
|
const state = getState();
|
||||||
const uid = sessions.activeUid;
|
const groupUids = getGroupUids(state);
|
||||||
const sessionUids = keys(sessions.sessions);
|
const uid = state.termGroups.activeRootGroup;
|
||||||
if (uid === sessionUids[i]) {
|
if (uid === groupUids[i]) {
|
||||||
console.log('ignoring same uid');
|
console.log('ignoring same uid');
|
||||||
} else if (sessionUids[i] === null) {
|
} else if (groupUids[i]) {
|
||||||
console.log('ignoring inexistent index', i);
|
dispatch(setActiveGroup(groupUids[i]));
|
||||||
} else {
|
} else {
|
||||||
dispatch(setActiveSession(sessionUids[i]));
|
console.log('ignoring inexistent index', i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,14 @@ export default class Header extends Component {
|
||||||
this.props.onChangeTab(active);
|
this.props.onChangeTab(active);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleHeaderMouseDown() {
|
handleHeaderMouseDown(ev) {
|
||||||
|
// the hack of all hacks, this prevents the term
|
||||||
|
// iframe from losing focus, for example, when
|
||||||
|
// the user drags the nav around
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
// persist start positions of a potential drag motion
|
||||||
|
// to differentiate dragging from clicking
|
||||||
this.headerMouseDownWindowX = window.screenX;
|
this.headerMouseDownWindowX = window.screenX;
|
||||||
this.headerMouseDownWindowY = window.screenY;
|
this.headerMouseDownWindowY = window.screenY;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
184
lib/components/split-pane.js
Normal file
184
lib/components/split-pane.js
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
import React from 'react';
|
||||||
|
import Component from '../component';
|
||||||
|
|
||||||
|
export default class SplitPane extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleDragStart = this.handleDragStart.bind(this);
|
||||||
|
this.onDrag = this.onDrag.bind(this);
|
||||||
|
this.onDragEnd = this.onDragEnd.bind(this);
|
||||||
|
this.state = {dragging: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.state.dragging && prevProps.sizes !== this.props.sizes) {
|
||||||
|
// recompute positions for ongoing dragging
|
||||||
|
this.dragPanePosition = this.dragTarget.getBoundingClientRect()[this.d2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDragStart(ev) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.setState({dragging: true});
|
||||||
|
window.addEventListener('mousemove', this.onDrag);
|
||||||
|
window.addEventListener('mouseup', this.onDragEnd);
|
||||||
|
|
||||||
|
// dimensions to consider
|
||||||
|
if (this.props.direction === 'horizontal') {
|
||||||
|
this.d1 = 'height';
|
||||||
|
this.d2 = 'top';
|
||||||
|
this.d3 = 'clientY';
|
||||||
|
} else {
|
||||||
|
this.d1 = 'width';
|
||||||
|
this.d2 = 'left';
|
||||||
|
this.d3 = 'clientX';
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDrag(ev) {
|
||||||
|
let {sizes} = this.props;
|
||||||
|
let sizes_;
|
||||||
|
if (sizes) {
|
||||||
|
sizes_ = [].concat(sizes);
|
||||||
|
} else {
|
||||||
|
const total = this.props.children.length;
|
||||||
|
sizes = sizes_ = new Array(total).fill(1 / total);
|
||||||
|
}
|
||||||
|
const i = this.paneIndex;
|
||||||
|
const pos = ev[this.d3];
|
||||||
|
const d = Math.abs(this.dragPanePosition - pos) / this.panesSize;
|
||||||
|
if (pos > this.dragPanePosition) {
|
||||||
|
sizes_[i] += d;
|
||||||
|
sizes_[i + 1] -= d;
|
||||||
|
} else {
|
||||||
|
sizes_[i] -= d;
|
||||||
|
sizes_[i + 1] += d;
|
||||||
|
}
|
||||||
|
this.props.onResize(sizes_);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDragEnd() {
|
||||||
|
if (this.state.dragging) {
|
||||||
|
window.removeEventListener('mousemove', this.onDrag);
|
||||||
|
window.removeEventListener('mouseup', this.onDragEnd);
|
||||||
|
this.setState({dragging: false});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template(css) {
|
||||||
|
const children = this.props.children;
|
||||||
|
const {direction, borderColor} = this.props;
|
||||||
|
let {sizes} = this.props;
|
||||||
|
if (!sizes) {
|
||||||
|
// workaround for the fact that if we don't specify
|
||||||
|
// sizes, sometimes flex fails to calculate the
|
||||||
|
// 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) => {
|
||||||
|
const style = {
|
||||||
|
flexBasis: (sizes[i] * 100) + '%',
|
||||||
|
flexGrow: 0
|
||||||
|
};
|
||||||
|
return [
|
||||||
|
<div className={css('pane')} style={style}>
|
||||||
|
{ child }
|
||||||
|
</div>,
|
||||||
|
i < children.length - 1 ?
|
||||||
|
<div
|
||||||
|
onMouseDown={this.handleDragStart}
|
||||||
|
style={{backgroundColor: borderColor}}
|
||||||
|
className={css('divider', `divider_${direction}`)}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
];
|
||||||
|
})
|
||||||
|
}
|
||||||
|
<div
|
||||||
|
style={{display: this.state.dragging ? 'block' : 'none'}}
|
||||||
|
className={css('shim')}
|
||||||
|
/>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
styles() {
|
||||||
|
return {
|
||||||
|
panes: {
|
||||||
|
display: 'flex',
|
||||||
|
flex: 1,
|
||||||
|
outline: 'none',
|
||||||
|
position: 'relative',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%'
|
||||||
|
},
|
||||||
|
|
||||||
|
'panes_vertical': {
|
||||||
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
|
||||||
|
'panes_horizontal': {
|
||||||
|
flexDirection: 'column'
|
||||||
|
},
|
||||||
|
|
||||||
|
pane: {
|
||||||
|
flex: 1,
|
||||||
|
outline: 'none',
|
||||||
|
position: 'relative'
|
||||||
|
},
|
||||||
|
|
||||||
|
divider: {
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
zIndex: '1',
|
||||||
|
backgroundClip: 'padding-box',
|
||||||
|
flexShrink: 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'
|
||||||
|
},
|
||||||
|
|
||||||
|
'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%'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
// ensure drag end
|
||||||
|
if (this.dragging) {
|
||||||
|
this.onDragEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
118
lib/components/term-group.js
Normal file
118
lib/components/term-group.js
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {connect} from 'react-redux';
|
||||||
|
import Component from '../component';
|
||||||
|
import {decorate, getTermProps} from '../utils/plugins';
|
||||||
|
import {resizeTermGroup} from '../actions/term-groups';
|
||||||
|
import Term_ from './term';
|
||||||
|
import SplitPane_ from './split-pane';
|
||||||
|
|
||||||
|
const Term = decorate(Term_, 'Term');
|
||||||
|
const SplitPane = decorate(SplitPane_, 'SplitPane');
|
||||||
|
|
||||||
|
class TermGroup_ extends Component {
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
this.bound = new WeakMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
bind(fn, thisObj, uid) {
|
||||||
|
if (!this.bound.has(fn)) {
|
||||||
|
this.bound.set(fn, {});
|
||||||
|
}
|
||||||
|
const map = this.bound.get(fn);
|
||||||
|
if (!map[uid]) {
|
||||||
|
map[uid] = fn.bind(thisObj, uid);
|
||||||
|
}
|
||||||
|
return map[uid];
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSplit(groups) {
|
||||||
|
const [first, ...rest] = groups;
|
||||||
|
if (!rest.length) {
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
|
const direction = this.props.termGroup.direction.toLowerCase();
|
||||||
|
return (<SplitPane
|
||||||
|
direction={direction}
|
||||||
|
sizes={this.props.termGroup.sizes}
|
||||||
|
onResize={this.props.onTermGroupResize}
|
||||||
|
borderColor={this.props.borderColor}
|
||||||
|
>
|
||||||
|
{ groups }
|
||||||
|
</SplitPane>);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTerm(uid) {
|
||||||
|
const session = this.props.sessions[uid];
|
||||||
|
const termRef = this.props.terms[uid];
|
||||||
|
const props = getTermProps(uid, this.props, {
|
||||||
|
term: termRef ? termRef.term : null,
|
||||||
|
customCSS: this.props.customCSS,
|
||||||
|
fontSize: this.props.fontSize,
|
||||||
|
cursorColor: this.props.cursorColor,
|
||||||
|
cursorShape: this.props.cursorShape,
|
||||||
|
fontFamily: this.props.fontFamily,
|
||||||
|
fontSmoothing: this.props.fontSmoothing,
|
||||||
|
foregroundColor: this.props.foregroundColor,
|
||||||
|
backgroundColor: this.props.backgroundColor,
|
||||||
|
modifierKeys: this.props.modifierKeys,
|
||||||
|
padding: this.props.padding,
|
||||||
|
colors: this.props.colors,
|
||||||
|
url: session.url,
|
||||||
|
cleared: session.cleared,
|
||||||
|
cols: session.cols,
|
||||||
|
rows: session.rows,
|
||||||
|
onActive: this.bind(this.props.onActive, null, uid),
|
||||||
|
onResize: this.bind(this.props.onResize, null, uid),
|
||||||
|
onTitle: this.bind(this.props.onTitle, null, uid),
|
||||||
|
onData: this.bind(this.props.onData, null, uid),
|
||||||
|
onURLAbort: this.bind(this.props.onURLAbort, null, uid)
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: This will create a new ref_ function for every render,
|
||||||
|
// which is inefficient. Should maybe do something similar
|
||||||
|
// to this.bind.
|
||||||
|
return (<Term
|
||||||
|
ref_={term => this.props.ref_(uid, term)}
|
||||||
|
key={uid}
|
||||||
|
{...props}
|
||||||
|
/>);
|
||||||
|
}
|
||||||
|
|
||||||
|
template() {
|
||||||
|
const {childGroups, termGroup} = this.props;
|
||||||
|
if (termGroup.sessionUid) {
|
||||||
|
return this.renderTerm(termGroup.sessionUid);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = childGroups.map(child => {
|
||||||
|
const props = Object.assign({}, this.props, {
|
||||||
|
termGroup: child
|
||||||
|
});
|
||||||
|
|
||||||
|
return (<TermGroup
|
||||||
|
key={child.uid}
|
||||||
|
{...props}
|
||||||
|
/>);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.renderSplit(groups);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TermGroup = connect(
|
||||||
|
(state, ownProps) => ({
|
||||||
|
childGroups: ownProps.termGroup.children.map(uid =>
|
||||||
|
state.termGroups.termGroups[uid]
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
(dispatch, ownProps) => ({
|
||||||
|
onTermGroupResize(splitSizes) {
|
||||||
|
dispatch(resizeTermGroup(ownProps.termGroup.uid, splitSizes));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)(TermGroup_);
|
||||||
|
|
||||||
|
export default TermGroup;
|
||||||
|
|
@ -10,15 +10,17 @@ export default class Term extends Component {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.onWheel = this.onWheel.bind(this);
|
this.handleWheel = this.handleWheel.bind(this);
|
||||||
|
this.handleMouseDown = this.handleMouseDown.bind(this);
|
||||||
this.handleScrollEnter = this.handleScrollEnter.bind(this);
|
this.handleScrollEnter = this.handleScrollEnter.bind(this);
|
||||||
this.handleScrollLeave = this.handleScrollLeave.bind(this);
|
this.handleScrollLeave = this.handleScrollLeave.bind(this);
|
||||||
|
this.handleFocus = this.handleFocus.bind(this);
|
||||||
props.ref_(this);
|
props.ref_(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {props} = this;
|
const {props} = this;
|
||||||
this.term = new hterm.Terminal();
|
this.term = props.term || new hterm.Terminal();
|
||||||
|
|
||||||
// the first term that's created has unknown size
|
// the first term that's created has unknown size
|
||||||
// subsequent new tabs have size
|
// subsequent new tabs have size
|
||||||
|
|
@ -72,10 +74,12 @@ export default class Term extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
const iframeWindow = this.getTermDocument().defaultView;
|
const iframeWindow = this.getTermDocument().defaultView;
|
||||||
iframeWindow.addEventListener('wheel', this.onWheel);
|
iframeWindow.addEventListener('wheel', this.handleWheel);
|
||||||
|
|
||||||
|
this.getScreenNode().addEventListener('focus', this.handleFocus);
|
||||||
}
|
}
|
||||||
|
|
||||||
onWheel(e) {
|
handleWheel(e) {
|
||||||
if (this.props.onWheel) {
|
if (this.props.onWheel) {
|
||||||
this.props.onWheel(e);
|
this.props.onWheel(e);
|
||||||
}
|
}
|
||||||
|
|
@ -99,6 +103,13 @@ export default class Term extends Component {
|
||||||
this.scrollMouseEnter = false;
|
this.scrollMouseEnter = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleFocus() {
|
||||||
|
// TODO: This will in turn result in `this.focus()` being
|
||||||
|
// called, which is unecessary.
|
||||||
|
// Should investigate if it matters.
|
||||||
|
this.props.onActive();
|
||||||
|
}
|
||||||
|
|
||||||
write(data) {
|
write(data) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.term.io.writeUTF8(data);
|
this.term.io.writeUTF8(data);
|
||||||
|
|
@ -152,6 +163,10 @@ export default class Term extends Component {
|
||||||
this.term.selectAll();
|
this.term.selectAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getScreenNode() {
|
||||||
|
return this.term.scrollPort_.getScreenNode();
|
||||||
|
}
|
||||||
|
|
||||||
getTermDocument() {
|
getTermDocument() {
|
||||||
return this.term.document_;
|
return this.term.document_;
|
||||||
}
|
}
|
||||||
|
|
@ -175,6 +190,15 @@ export default class Term extends Component {
|
||||||
return alternative;
|
return alternative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMouseDown(ev) {
|
||||||
|
// we prevent losing focus when clicking the boundary
|
||||||
|
// wrappers of the main terminal element
|
||||||
|
if (ev.target === this.refs.term_wrapper ||
|
||||||
|
ev.target === this.refs.term) {
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (this.props.url !== nextProps.url) {
|
if (this.props.url !== nextProps.url) {
|
||||||
// when the url prop changes, we make sure
|
// when the url prop changes, we make sure
|
||||||
|
|
@ -248,12 +272,18 @@ export default class Term extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
template(css) {
|
template(css) {
|
||||||
return (<div className={css('fit')}>
|
return (<div
|
||||||
|
ref="term_wrapper"
|
||||||
|
className={css('fit')}
|
||||||
|
onMouseDown={this.handleMouseDown}
|
||||||
|
style={{padding: this.props.padding}}
|
||||||
|
>
|
||||||
{ this.props.customChildrenBefore }
|
{ this.props.customChildrenBefore }
|
||||||
<div ref="term" className={css('fit', 'term')}/>
|
<div ref="term" className={css('fit', 'term')}/>
|
||||||
{ this.props.url ?
|
{ this.props.url ?
|
||||||
<webview
|
<webview
|
||||||
src={this.props.url}
|
src={this.props.url}
|
||||||
|
onFocus={this.handleFocus}
|
||||||
style={{
|
style={{
|
||||||
background: '#000',
|
background: '#000',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|
@ -277,6 +307,7 @@ export default class Term extends Component {
|
||||||
styles() {
|
styles() {
|
||||||
return {
|
return {
|
||||||
fit: {
|
fit: {
|
||||||
|
display: 'block',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%'
|
height: '100%'
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Component from '../component';
|
import Component from '../component';
|
||||||
import {last} from '../utils/array';
|
import {decorate, getTermGroupProps} from '../utils/plugins';
|
||||||
import {decorate, getTermProps} from '../utils/plugins';
|
import TermGroup_ from './term-group';
|
||||||
|
|
||||||
import Term_ from './term';
|
const TermGroup = decorate(TermGroup_, 'TermGroup');
|
||||||
|
|
||||||
const Term = decorate(Term_, 'Term');
|
|
||||||
|
|
||||||
export default class Terms extends Component {
|
export default class Terms extends Component {
|
||||||
|
|
||||||
|
|
@ -23,41 +20,6 @@ export default class Terms extends Component {
|
||||||
if (write && this.props.write !== write) {
|
if (write && this.props.write !== write) {
|
||||||
this.getTermByUid(write.uid).write(write.data);
|
this.getTermByUid(write.uid).write(write.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we just rendered, we consider the first tab active
|
|
||||||
// why is this decided here? because what session becomes
|
|
||||||
// active is a *view* and *layout* concern. for example,
|
|
||||||
// if a split is closed (and we had split), the next active
|
|
||||||
// session after the close would be the one next to it
|
|
||||||
// *in the view*, not necessarily the model datastructure
|
|
||||||
if (next.sessions && next.sessions.length) {
|
|
||||||
if (!this.props.activeSession && next.sessions.length) {
|
|
||||||
this.props.onActive(next.sessions[0].uid);
|
|
||||||
} else if (this.props.sessions.length !== next.sessions.length) {
|
|
||||||
if (next.sessions.length > this.props.sessions.length) {
|
|
||||||
// if we are adding, we focused on the new one
|
|
||||||
this.props.onActive(last(next.sessions).uid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newUids = uids(next.sessions);
|
|
||||||
const curActive = this.props.activeSession;
|
|
||||||
|
|
||||||
// if we closed an item that wasn't focused, nothing changes
|
|
||||||
if (newUids.indexOf(curActive) !== -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldIndex = uids(this.props.sessions).indexOf(curActive);
|
|
||||||
if (newUids[oldIndex]) {
|
|
||||||
this.props.onActive(newUids[oldIndex]);
|
|
||||||
} else {
|
|
||||||
this.props.onActive(last(next.sessions).uid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.props.onActive(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps) {
|
shouldComponentUpdate(nextProps) {
|
||||||
|
|
@ -83,7 +45,7 @@ export default class Terms extends Component {
|
||||||
onRef(uid, term) {
|
onRef(uid, term) {
|
||||||
if (term) {
|
if (term) {
|
||||||
this.terms[uid] = term;
|
this.terms[uid] = term;
|
||||||
} else {
|
} else if (!this.props.sessions[uid]) {
|
||||||
delete this.terms[uid];
|
delete this.terms[uid];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -100,21 +62,6 @@ export default class Terms extends Component {
|
||||||
return this.props.sessions.length - 1;
|
return this.props.sessions.length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bind(fn, thisObj, uid) {
|
|
||||||
if (!this.bound.has(fn)) {
|
|
||||||
this.bound.set(fn, {});
|
|
||||||
}
|
|
||||||
const map = this.bound.get(fn);
|
|
||||||
if (!map[uid]) {
|
|
||||||
map[uid] = fn.bind(thisObj, uid);
|
|
||||||
}
|
|
||||||
return map[uid];
|
|
||||||
}
|
|
||||||
|
|
||||||
getTermProps(uid) {
|
|
||||||
return getTermProps(uid, this.props);
|
|
||||||
}
|
|
||||||
|
|
||||||
onTerminal(uid, term) {
|
onTerminal(uid, term) {
|
||||||
this.terms[uid] = term;
|
this.terms[uid] = term;
|
||||||
}
|
}
|
||||||
|
|
@ -125,46 +72,51 @@ export default class Terms extends Component {
|
||||||
|
|
||||||
template(css) {
|
template(css) {
|
||||||
return (<div
|
return (<div
|
||||||
style={{padding: this.props.padding}}
|
|
||||||
className={css('terms')}
|
className={css('terms')}
|
||||||
>
|
>
|
||||||
{ this.props.customChildrenBefore }
|
{ this.props.customChildrenBefore }
|
||||||
{
|
{
|
||||||
this.props.sessions.map(session => {
|
this.props.termGroups.map(termGroup => {
|
||||||
const uid = session.uid;
|
const {uid} = termGroup;
|
||||||
const isActive = uid === this.props.activeSession;
|
const isActive = uid === this.props.activeRootGroup;
|
||||||
const props = getTermProps(uid, this.props, {
|
const props = getTermGroupProps(uid, this.props, {
|
||||||
cols: this.props.cols,
|
termGroup,
|
||||||
rows: this.props.rows,
|
terms: this.terms,
|
||||||
|
sessions: this.props.sessions,
|
||||||
customCSS: this.props.customCSS,
|
customCSS: this.props.customCSS,
|
||||||
fontSize: this.props.fontSize,
|
fontSize: this.props.fontSize,
|
||||||
|
borderColor: this.props.borderColor,
|
||||||
cursorColor: this.props.cursorColor,
|
cursorColor: this.props.cursorColor,
|
||||||
cursorShape: this.props.cursorShape,
|
cursorShape: this.props.cursorShape,
|
||||||
fontFamily: this.props.fontFamily,
|
fontFamily: this.props.fontFamily,
|
||||||
fontSmoothing: this.props.fontSmoothing,
|
fontSmoothing: this.props.fontSmoothing,
|
||||||
foregroundColor: this.props.foregroundColor,
|
foregroundColor: this.props.foregroundColor,
|
||||||
|
backgroundColor: this.props.backgroundColor,
|
||||||
|
padding: this.props.padding,
|
||||||
colors: this.props.colors,
|
colors: this.props.colors,
|
||||||
url: session.url,
|
|
||||||
cleared: session.cleared,
|
|
||||||
onResize: this.bind(this.props.onResize, null, uid),
|
|
||||||
onTitle: this.bind(this.props.onTitle, null, uid),
|
|
||||||
onData: this.bind(this.props.onData, null, uid),
|
|
||||||
onURLAbort: this.bind(this.props.onURLAbort, null, uid),
|
|
||||||
bell: this.props.bell,
|
bell: this.props.bell,
|
||||||
bellSoundURL: this.props.bellSoundURL,
|
bellSoundURL: this.props.bellSoundURL,
|
||||||
copyOnSelect: this.props.copyOnSelect,
|
copyOnSelect: this.props.copyOnSelect,
|
||||||
modifierKeys: this.props.modifierKeys
|
modifierKeys: this.props.modifierKeys,
|
||||||
|
onActive: this.props.onActive,
|
||||||
|
onResize: this.props.onResize,
|
||||||
|
onTitle: this.props.onTitle,
|
||||||
|
onData: this.props.onData,
|
||||||
|
onURLAbort: this.props.onURLAbort
|
||||||
});
|
});
|
||||||
return (<div
|
|
||||||
key={`d${uid}`}
|
return (
|
||||||
className={css('term', isActive && 'termActive')}
|
<div
|
||||||
>
|
key={`d${uid}`}
|
||||||
<Term
|
className={css('termGroup', isActive && 'termGroupActive')}
|
||||||
key={uid}
|
>
|
||||||
ref_={this.bind(this.onRef, this, uid)}
|
<TermGroup
|
||||||
{...props}
|
key={uid}
|
||||||
/>
|
ref_={this.onRef}
|
||||||
</div>);
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
{ this.props.customChildren }
|
{ this.props.customChildren }
|
||||||
|
|
@ -183,24 +135,15 @@ export default class Terms extends Component {
|
||||||
color: '#fff'
|
color: '#fff'
|
||||||
},
|
},
|
||||||
|
|
||||||
term: {
|
termGroup: {
|
||||||
display: 'none',
|
display: 'none',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%'
|
height: '100%'
|
||||||
},
|
},
|
||||||
|
|
||||||
termActive: {
|
termGroupActive: {
|
||||||
display: 'block'
|
display: 'block'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// little memoized helper to compute a map of uids
|
|
||||||
function uids(sessions) {
|
|
||||||
if (!sessions._uids) {
|
|
||||||
sessions._uids = sessions.map(s => s.uid);
|
|
||||||
}
|
|
||||||
return sessions._uids;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ export const SESSION_ADD_DATA = 'SESSION_ADD_DATA';
|
||||||
export const SESSION_PTY_DATA = 'SESSION_PTY_DATA';
|
export const SESSION_PTY_DATA = 'SESSION_PTY_DATA';
|
||||||
export const SESSION_PTY_EXIT = 'SESSION_PTY_EXIT';
|
export const SESSION_PTY_EXIT = 'SESSION_PTY_EXIT';
|
||||||
export const SESSION_USER_EXIT = 'SESSION_USER_EXIT';
|
export const SESSION_USER_EXIT = 'SESSION_USER_EXIT';
|
||||||
export const SESSION_USER_EXIT_ACTIVE = 'SESSION_USER_EXIT_ACTIVE';
|
|
||||||
export const SESSION_SET_ACTIVE = 'SESSION_SET_ACTIVE';
|
export const SESSION_SET_ACTIVE = 'SESSION_SET_ACTIVE';
|
||||||
export const SESSION_CLEAR_ACTIVE = 'SESSION_CLEAR_ACTIVE';
|
export const SESSION_CLEAR_ACTIVE = 'SESSION_CLEAR_ACTIVE';
|
||||||
export const SESSION_USER_DATA = 'SESSION_USER_DATA';
|
export const SESSION_USER_DATA = 'SESSION_USER_DATA';
|
||||||
|
|
|
||||||
8
lib/constants/term-groups.js
Normal file
8
lib/constants/term-groups.js
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
export const TERM_GROUP_REQUEST = 'TERM_GROUP_REQUEST';
|
||||||
|
export const TERM_GROUP_EXIT = 'TERM_GROUP_EXIT';
|
||||||
|
export const TERM_GROUP_RESIZE = 'TERM_GROUP_RESIZE';
|
||||||
|
export const TERM_GROUP_EXIT_ACTIVE = 'TERM_GROUP_EXIT_ACTIVE';
|
||||||
|
export const DIRECTION = {
|
||||||
|
HORIZONTAL: 'HORIZONTAL',
|
||||||
|
VERTICAL: 'VERTICAL'
|
||||||
|
};
|
||||||
|
|
@ -6,6 +6,8 @@ export const UI_FONT_SMOOTHING_SET = 'UI_FONT_SMOOTHING_SET';
|
||||||
export const UI_MOVE_LEFT = 'UI_MOVE_LEFT';
|
export const UI_MOVE_LEFT = 'UI_MOVE_LEFT';
|
||||||
export const UI_MOVE_RIGHT = 'UI_MOVE_RIGHT';
|
export const UI_MOVE_RIGHT = 'UI_MOVE_RIGHT';
|
||||||
export const UI_MOVE_TO = 'UI_MOVE_TO';
|
export const UI_MOVE_TO = 'UI_MOVE_TO';
|
||||||
|
export const UI_MOVE_NEXT_PANE = 'UI_MOVE_NEXT_PANE';
|
||||||
|
export const UI_MOVE_PREV_PANE = 'UI_MOVE_PREV_PANE';
|
||||||
export const UI_SHOW_PREFERENCES = 'UI_SHOW_PREFERENCES';
|
export const UI_SHOW_PREFERENCES = 'UI_SHOW_PREFERENCES';
|
||||||
export const UI_WINDOW_MOVE = 'UI_WINDOW_MOVE';
|
export const UI_WINDOW_MOVE = 'UI_WINDOW_MOVE';
|
||||||
export const UI_WINDOW_MAXIMIZE = 'UI_WINDOW_MAXIMIZE';
|
export const UI_WINDOW_MAXIMIZE = 'UI_WINDOW_MAXIMIZE';
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,27 @@
|
||||||
|
/* eslint-disable max-params */
|
||||||
import {createSelector} from 'reselect';
|
import {createSelector} from 'reselect';
|
||||||
|
|
||||||
import Header from '../components/header';
|
import Header from '../components/header';
|
||||||
import {closeTab, changeTab, maximize, unmaximize} from '../actions/header';
|
import {closeTab, changeTab, maximize, unmaximize} from '../actions/header';
|
||||||
import {values} from '../utils/object';
|
|
||||||
import {connect} from '../utils/plugins';
|
import {connect} from '../utils/plugins';
|
||||||
|
import {getRootGroups} from '../selectors';
|
||||||
|
|
||||||
const isMac = /Mac/.test(navigator.userAgent);
|
const isMac = /Mac/.test(navigator.userAgent);
|
||||||
|
|
||||||
const getSessions = sessions => sessions.sessions;
|
const getSessions = ({sessions}) => sessions.sessions;
|
||||||
const getActiveUid = sessions => sessions.activeUid;
|
const getActiveRootGroup = ({termGroups}) => termGroups.activeRootGroup;
|
||||||
const getActivityMarkers = (sessions, ui) => ui.activityMarkers;
|
const getActiveSessions = ({termGroups}) => termGroups.activeSessions;
|
||||||
|
const getActivityMarkers = ({ui}) => ui.activityMarkers;
|
||||||
const getTabs = createSelector(
|
const getTabs = createSelector(
|
||||||
[getSessions, getActiveUid, getActivityMarkers],
|
[getSessions, getRootGroups, getActiveSessions, getActiveRootGroup, getActivityMarkers],
|
||||||
(sessions, activeUid, activityMarkers) => values(sessions).map(s => {
|
(sessions, rootGroups, activeSessions, activeRootGroup, activityMarkers) => rootGroups.map(t => {
|
||||||
|
const activeSessionUid = activeSessions[t.uid];
|
||||||
|
const session = sessions[activeSessionUid];
|
||||||
return {
|
return {
|
||||||
uid: s.uid,
|
uid: t.uid,
|
||||||
title: s.title,
|
title: session.title,
|
||||||
isActive: s.uid === activeUid,
|
isActive: t.uid === activeRootGroup,
|
||||||
hasActivity: activityMarkers[s.uid]
|
hasActivity: activityMarkers[session.uid]
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -27,7 +31,7 @@ const HeaderContainer = connect(
|
||||||
return {
|
return {
|
||||||
// active is an index
|
// active is an index
|
||||||
isMac,
|
isMac,
|
||||||
tabs: getTabs(state.sessions, state.ui),
|
tabs: getTabs(state),
|
||||||
activeMarkers: state.ui.activityMarkers,
|
activeMarkers: state.ui.activityMarkers,
|
||||||
borderColor: state.ui.borderColor,
|
borderColor: state.ui.borderColor,
|
||||||
backgroundColor: state.ui.backgroundColor,
|
backgroundColor: state.ui.backgroundColor,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import React from 'react';
|
||||||
import Component from '../component';
|
import Component from '../component';
|
||||||
import {connect} from '../utils/plugins';
|
import {connect} from '../utils/plugins';
|
||||||
import * as uiActions from '../actions/ui';
|
import * as uiActions from '../actions/ui';
|
||||||
|
import * as termGroupActions from '../actions/term-groups';
|
||||||
|
|
||||||
import HeaderContainer from './header';
|
import HeaderContainer from './header';
|
||||||
import TermsContainer from './terms';
|
import TermsContainer from './terms';
|
||||||
|
|
@ -34,7 +35,7 @@ class HyperTerm extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
attachKeyListeners() {
|
attachKeyListeners() {
|
||||||
const {moveTo, moveLeft, moveRight} = this.props;
|
const {moveTo, moveLeft, moveRight, splitHorizontal, splitVertical} = this.props;
|
||||||
const term = this.terms.getActiveTerm();
|
const term = this.terms.getActiveTerm();
|
||||||
if (!term) {
|
if (!term) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -42,6 +43,11 @@ class HyperTerm extends Component {
|
||||||
const lastIndex = this.terms.getLastTermIndex();
|
const lastIndex = this.terms.getLastTermIndex();
|
||||||
const document = term.getTermDocument();
|
const document = term.getTermDocument();
|
||||||
const keys = new Mousetrap(document);
|
const keys = new Mousetrap(document);
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
keys.bind('command+d', splitVertical);
|
||||||
|
keys.bind('command+shift+d', splitHorizontal);
|
||||||
|
}
|
||||||
|
|
||||||
keys.bind('command+1', moveTo.bind(this, 0));
|
keys.bind('command+1', moveTo.bind(this, 0));
|
||||||
keys.bind('command+2', moveTo.bind(this, 1));
|
keys.bind('command+2', moveTo.bind(this, 1));
|
||||||
keys.bind('command+3', moveTo.bind(this, 2));
|
keys.bind('command+3', moveTo.bind(this, 2));
|
||||||
|
|
@ -96,7 +102,7 @@ class HyperTerm extends Component {
|
||||||
|
|
||||||
template(css) {
|
template(css) {
|
||||||
const {isMac, customCSS, borderColor} = this.props;
|
const {isMac, customCSS, borderColor} = this.props;
|
||||||
return (<div onClick={this.handleFocusActive}>
|
return (<div>
|
||||||
<div
|
<div
|
||||||
style={{borderColor}}
|
style={{borderColor}}
|
||||||
className={css('main', isMac && 'mainRounded')}
|
className={css('main', isMac && 'mainRounded')}
|
||||||
|
|
@ -152,6 +158,14 @@ const HyperTermContainer = connect(
|
||||||
|
|
||||||
moveRight: () => {
|
moveRight: () => {
|
||||||
dispatch(uiActions.moveRight());
|
dispatch(uiActions.moveRight());
|
||||||
|
},
|
||||||
|
|
||||||
|
splitHorizontal: () => {
|
||||||
|
dispatch(termGroupActions.requestHorizontalSplit());
|
||||||
|
},
|
||||||
|
|
||||||
|
splitVertical: () => {
|
||||||
|
dispatch(termGroupActions.requestVerticalSplit());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import Terms from '../components/terms';
|
import Terms from '../components/terms';
|
||||||
import {values} from '../utils/object';
|
|
||||||
import {connect} from '../utils/plugins';
|
import {connect} from '../utils/plugins';
|
||||||
import {
|
import {
|
||||||
resizeSession,
|
resizeSession,
|
||||||
|
|
@ -8,14 +7,17 @@ import {
|
||||||
setSessionXtermTitle,
|
setSessionXtermTitle,
|
||||||
setActiveSession
|
setActiveSession
|
||||||
} from '../actions/sessions';
|
} from '../actions/sessions';
|
||||||
|
import {getRootGroups} from '../selectors';
|
||||||
|
|
||||||
const TermsContainer = connect(
|
const TermsContainer = connect(
|
||||||
state => {
|
state => {
|
||||||
const sessions = state.sessions.sessions;
|
const {sessions} = state.sessions;
|
||||||
return {
|
return {
|
||||||
|
sessions,
|
||||||
cols: state.ui.cols,
|
cols: state.ui.cols,
|
||||||
rows: state.ui.rows,
|
rows: state.ui.rows,
|
||||||
sessions: values(sessions),
|
termGroups: getRootGroups(state),
|
||||||
|
activeRootGroup: state.termGroups.activeRootGroup,
|
||||||
activeSession: state.sessions.activeUid,
|
activeSession: state.sessions.activeUid,
|
||||||
customCSS: state.ui.termCSS,
|
customCSS: state.ui.termCSS,
|
||||||
write: state.sessions.write,
|
write: state.sessions.write,
|
||||||
|
|
|
||||||
46
lib/hterm.js
46
lib/hterm.js
|
|
@ -3,6 +3,14 @@ import fromCharCode from './utils/key-code';
|
||||||
|
|
||||||
const selection = require('./utils/selection');
|
const selection = require('./utils/selection');
|
||||||
|
|
||||||
|
// Keys that are used in combination
|
||||||
|
// with ctrl based hotkeys:
|
||||||
|
const IGNORED_CTRL_KEYS = [
|
||||||
|
'Tab',
|
||||||
|
'KeyO',
|
||||||
|
'KeyE'
|
||||||
|
];
|
||||||
|
|
||||||
hterm.defaultStorage = new lib.Storage.Memory();
|
hterm.defaultStorage = new lib.Storage.Memory();
|
||||||
|
|
||||||
// Provide selectAll to terminal viewport
|
// Provide selectAll to terminal viewport
|
||||||
|
|
@ -129,7 +137,7 @@ hterm.Keyboard.prototype.onKeyDown_ = function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.metaKey || e.altKey || (e.ctrlKey && e.code === 'Tab')) {
|
if (e.metaKey || e.altKey || (e.ctrlKey && IGNORED_CTRL_KEYS.includes(e.code))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((!e.ctrlKey || e.code !== 'ControlLeft') && !e.shiftKey && e.code !== 'CapsLock') {
|
if ((!e.ctrlKey || e.code !== 'ControlLeft') && !e.shiftKey && e.code !== 'CapsLock') {
|
||||||
|
|
@ -191,6 +199,42 @@ hterm.Terminal.prototype.clearPreserveCursorRow = function () {
|
||||||
this.scrollPort_.redraw_();
|
this.scrollPort_.redraw_();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 oldOnMouse.call(this, e);
|
||||||
|
};
|
||||||
|
|
||||||
|
// sine above we're no longer relying on `preventDefault`
|
||||||
|
// to avoid selections, we use css instead, so that
|
||||||
|
// focus is not lost, but selections are still not possible
|
||||||
|
// when the appropiate VT mode is set
|
||||||
|
hterm.VT.prototype.__defineSetter__('mouseReport', function (val) {
|
||||||
|
this.mouseReport_ = val;
|
||||||
|
const rowNodes = this.terminal.scrollPort_.rowNodes_;
|
||||||
|
if (rowNodes) {
|
||||||
|
if (val === this.MOUSE_REPORT_DISABLED) {
|
||||||
|
rowNodes.style.webkitUserSelect = 'text';
|
||||||
|
} else {
|
||||||
|
rowNodes.style.webkitUserSelect = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
hterm.VT.prototype.__defineGetter__('mouseReport', function () {
|
||||||
|
return this.mouseReport_;
|
||||||
|
});
|
||||||
|
|
||||||
// fixes a bug in hterm, where the shorthand hex
|
// fixes a bug in hterm, where the shorthand hex
|
||||||
// is not properly converted to rgb
|
// is not properly converted to rgb
|
||||||
lib.colors.hexToRGB = function (arg) {
|
lib.colors.hexToRGB = function (arg) {
|
||||||
|
|
|
||||||
35
lib/index.js
35
lib/index.js
|
|
@ -11,6 +11,7 @@ import * as plugins from './utils/plugins';
|
||||||
import * as uiActions from './actions/ui';
|
import * as uiActions from './actions/ui';
|
||||||
import * as updaterActions from './actions/updater';
|
import * as updaterActions from './actions/updater';
|
||||||
import * as sessionActions from './actions/sessions';
|
import * as sessionActions from './actions/sessions';
|
||||||
|
import * as termGroupActions from './actions/term-groups';
|
||||||
import HyperTermContainer from './containers/hyperterm';
|
import HyperTermContainer from './containers/hyperterm';
|
||||||
import {loadConfig, reloadConfig} from './actions/config';
|
import {loadConfig, reloadConfig} from './actions/config';
|
||||||
import configureStore from './store/configure-store';
|
import configureStore from './store/configure-store';
|
||||||
|
|
@ -38,8 +39,8 @@ rpc.on('ready', () => {
|
||||||
store_.dispatch(uiActions.setFontSmoothing());
|
store_.dispatch(uiActions.setFontSmoothing());
|
||||||
});
|
});
|
||||||
|
|
||||||
rpc.on('session add', ({uid, shell, pid}) => {
|
rpc.on('session add', data => {
|
||||||
store_.dispatch(sessionActions.addSession(uid, shell, pid));
|
store_.dispatch(sessionActions.addSession(data));
|
||||||
});
|
});
|
||||||
|
|
||||||
rpc.on('session data', ({uid, data}) => {
|
rpc.on('session data', ({uid, data}) => {
|
||||||
|
|
@ -55,21 +56,29 @@ rpc.on('session title', ({uid, title}) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
rpc.on('session exit', ({uid}) => {
|
rpc.on('session exit', ({uid}) => {
|
||||||
store_.dispatch(sessionActions.sessionExit(uid));
|
store_.dispatch(termGroupActions.ptyExitTermGroup(uid));
|
||||||
});
|
});
|
||||||
|
|
||||||
rpc.on('session add req', () => {
|
rpc.on('termgroup close req', () => {
|
||||||
store_.dispatch(sessionActions.requestSession());
|
store_.dispatch(termGroupActions.exitActiveTermGroup());
|
||||||
});
|
|
||||||
|
|
||||||
rpc.on('session close req', () => {
|
|
||||||
store_.dispatch(sessionActions.userExitActiveSession());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
rpc.on('session clear req', () => {
|
rpc.on('session clear req', () => {
|
||||||
store_.dispatch(sessionActions.clearActiveSession());
|
store_.dispatch(sessionActions.clearActiveSession());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
rpc.on('termgroup add req', () => {
|
||||||
|
store_.dispatch(termGroupActions.requestTermGroup());
|
||||||
|
});
|
||||||
|
|
||||||
|
rpc.on('split request horizontal', () => {
|
||||||
|
store_.dispatch(termGroupActions.requestHorizontalSplit());
|
||||||
|
});
|
||||||
|
|
||||||
|
rpc.on('split request vertical', () => {
|
||||||
|
store_.dispatch(termGroupActions.requestVerticalSplit());
|
||||||
|
});
|
||||||
|
|
||||||
rpc.on('reset fontSize req', () => {
|
rpc.on('reset fontSize req', () => {
|
||||||
store_.dispatch(uiActions.resetFontSize());
|
store_.dispatch(uiActions.resetFontSize());
|
||||||
});
|
});
|
||||||
|
|
@ -90,6 +99,14 @@ rpc.on('move right req', () => {
|
||||||
store_.dispatch(uiActions.moveRight());
|
store_.dispatch(uiActions.moveRight());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
rpc.on('next pane req', () => {
|
||||||
|
store_.dispatch(uiActions.moveToNextPane());
|
||||||
|
});
|
||||||
|
|
||||||
|
rpc.on('prev pane req', () => {
|
||||||
|
store_.dispatch(uiActions.moveToPreviousPane());
|
||||||
|
});
|
||||||
|
|
||||||
rpc.on('preferences', () => {
|
rpc.on('preferences', () => {
|
||||||
store_.dispatch(uiActions.showPreferences());
|
store_.dispatch(uiActions.showPreferences());
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import {combineReducers} from 'redux';
|
import {combineReducers} from 'redux';
|
||||||
import ui from './ui';
|
import ui from './ui';
|
||||||
import sessions from './sessions';
|
import sessions from './sessions';
|
||||||
|
import termGroups from './term-groups';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
ui,
|
ui,
|
||||||
sessions
|
sessions,
|
||||||
|
termGroups
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
SESSION_CLEAR_ACTIVE,
|
SESSION_CLEAR_ACTIVE,
|
||||||
SESSION_URL_SET,
|
SESSION_URL_SET,
|
||||||
SESSION_URL_UNSET,
|
SESSION_URL_UNSET,
|
||||||
|
SESSION_RESIZE,
|
||||||
SESSION_SET_XTERM_TITLE,
|
SESSION_SET_XTERM_TITLE,
|
||||||
SESSION_SET_PROCESS_TITLE
|
SESSION_SET_PROCESS_TITLE
|
||||||
} from '../constants/sessions';
|
} from '../constants/sessions';
|
||||||
|
|
@ -23,6 +24,8 @@ function Session(obj) {
|
||||||
return Immutable({
|
return Immutable({
|
||||||
uid: '',
|
uid: '',
|
||||||
title: '',
|
title: '',
|
||||||
|
cols: null,
|
||||||
|
rows: null,
|
||||||
write: null,
|
write: null,
|
||||||
url: null,
|
url: null,
|
||||||
cleared: false,
|
cleared: false,
|
||||||
|
|
@ -41,11 +44,15 @@ function Write(obj) {
|
||||||
const reducer = (state = initialState, action) => {
|
const reducer = (state = initialState, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case SESSION_ADD:
|
case SESSION_ADD:
|
||||||
return state.setIn(['sessions', action.uid], Session({
|
return state
|
||||||
uid: action.uid,
|
.set('activeUid', action.uid)
|
||||||
shell: action.shell.split('/').pop(),
|
.setIn(['sessions', action.uid], Session({
|
||||||
pid: action.pid
|
cols: action.cols,
|
||||||
}));
|
rows: action.rows,
|
||||||
|
uid: action.uid,
|
||||||
|
shell: action.shell.split('/').pop(),
|
||||||
|
pid: action.pid
|
||||||
|
}));
|
||||||
|
|
||||||
case SESSION_URL_SET:
|
case SESSION_URL_SET:
|
||||||
return state.setIn(['sessions', action.uid, 'url'], action.url);
|
return state.setIn(['sessions', action.uid, 'url'], action.url);
|
||||||
|
|
@ -90,6 +97,13 @@ const reducer = (state = initialState, action) => {
|
||||||
case SESSION_SET_PROCESS_TITLE:
|
case SESSION_SET_PROCESS_TITLE:
|
||||||
return state.setIn(['sessions', action.uid, 'title'], action.title);
|
return state.setIn(['sessions', action.uid, 'title'], action.title);
|
||||||
|
|
||||||
|
case SESSION_RESIZE:
|
||||||
|
return state.setIn(['sessions', action.uid], state.sessions[action.uid].merge({
|
||||||
|
rows: action.rows,
|
||||||
|
cols: action.cols,
|
||||||
|
resizeAt: Date.now()
|
||||||
|
}));
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
223
lib/reducers/term-groups.js
Normal file
223
lib/reducers/term-groups.js
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
import uuid from 'uuid';
|
||||||
|
import Immutable from 'seamless-immutable';
|
||||||
|
import {TERM_GROUP_EXIT, TERM_GROUP_RESIZE} from '../constants/term-groups';
|
||||||
|
import {SESSION_ADD, SESSION_SET_ACTIVE} from '../constants/sessions';
|
||||||
|
import {findBySession} from '../utils/term-groups';
|
||||||
|
import {decorateTermGroupsReducer} from '../utils/plugins';
|
||||||
|
|
||||||
|
const MIN_SIZE = 0.05;
|
||||||
|
const initialState = Immutable({
|
||||||
|
termGroups: {},
|
||||||
|
activeSessions: {},
|
||||||
|
activeRootGroup: null
|
||||||
|
});
|
||||||
|
|
||||||
|
function TermGroup(obj) {
|
||||||
|
return Immutable({
|
||||||
|
uid: null,
|
||||||
|
sessionUid: null,
|
||||||
|
parentUid: null,
|
||||||
|
direction: null,
|
||||||
|
sizes: null,
|
||||||
|
children: []
|
||||||
|
}).merge(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse upwards until we find a root term group (no parent).
|
||||||
|
const findRootGroup = (termGroups, uid) => {
|
||||||
|
const current = termGroups[uid];
|
||||||
|
if (!current.parentUid) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return findRootGroup(termGroups, current.parentUid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setActiveGroup = (state, action) => {
|
||||||
|
if (!action.uid) {
|
||||||
|
return state.set('activeRootGroup', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reduce existing sizes to fit a new split:
|
||||||
|
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));
|
||||||
|
return [...balanced.slice(0, index), newSize, ...balanced.slice(index)];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spread out the removed size to all the existing sizes:
|
||||||
|
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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const splitGroup = (state, action) => {
|
||||||
|
const {splitDirection, uid, activeUid} = action;
|
||||||
|
const activeGroup = findBySession(state, activeUid);
|
||||||
|
// If we're splitting in the same direction as the current active
|
||||||
|
// group's parent - or if it's the first split for that group -
|
||||||
|
// we want the parent to get another child:
|
||||||
|
let parentGroup = activeGroup.parentUid ? state.termGroups[activeGroup.parentUid] : activeGroup;
|
||||||
|
// If we're splitting in a different direction, we want the current
|
||||||
|
// active group to become a new parent instead:
|
||||||
|
if (parentGroup.direction && parentGroup.direction !== splitDirection) {
|
||||||
|
parentGroup = activeGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the group has a session (i.e. we're creating a new parent)
|
||||||
|
// we need to create two new groups,
|
||||||
|
// one for the existing session and one for the new split:
|
||||||
|
// P
|
||||||
|
// P -> / \
|
||||||
|
// G G
|
||||||
|
const newSession = TermGroup({
|
||||||
|
uid: uuid.v4(),
|
||||||
|
sessionUid: uid,
|
||||||
|
parentUid: parentGroup.uid
|
||||||
|
});
|
||||||
|
|
||||||
|
state = state.setIn(['termGroups', newSession.uid], newSession);
|
||||||
|
if (parentGroup.sessionUid) {
|
||||||
|
const existingSession = TermGroup({
|
||||||
|
uid: uuid.v4(),
|
||||||
|
sessionUid: parentGroup.sessionUid,
|
||||||
|
parentUid: parentGroup.uid
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (parentGroup.sizes) {
|
||||||
|
const newSizes = insertRebalance(parentGroup.sizes, index);
|
||||||
|
state = state.setIn(['termGroups', parentGroup.uid, 'sizes'], newSizes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Replace the parent by the given child in the tree,
|
||||||
|
// used when we remove another child and we're left
|
||||||
|
// with a one-to-one mapping between parent and child.
|
||||||
|
const replaceParent = (state, parent, child) => {
|
||||||
|
if (parent.parentUid) {
|
||||||
|
const parentParent = state.termGroups[parent.parentUid];
|
||||||
|
// 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
|
||||||
|
);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
state = state
|
||||||
|
.set('activeTermGroup', child.uid)
|
||||||
|
.set('activeSessions', newSessions);
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
.set('termGroups', state.termGroups.without(parent.uid))
|
||||||
|
.setIn(['termGroups', child.uid, 'parentUid'], parent.parentUid);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeGroup = (state, uid) => {
|
||||||
|
const group = state.termGroups[uid];
|
||||||
|
if (group.parentUid) {
|
||||||
|
const parent = state.termGroups[group.parentUid];
|
||||||
|
const newChildren = parent.children.filter(childUid => childUid !== uid);
|
||||||
|
if (newChildren.length === 1) {
|
||||||
|
// Since we only have one child left,
|
||||||
|
// we can merge the parent and child into one group:
|
||||||
|
const child = state.termGroups[newChildren[0]];
|
||||||
|
state = replaceParent(state, parent, child);
|
||||||
|
} else {
|
||||||
|
state = state.setIn(['termGroups', group.parentUid, 'children'], newChildren);
|
||||||
|
if (parent.sizes) {
|
||||||
|
const childIndex = parent.children.indexOf(uid);
|
||||||
|
const newSizes = removalRebalance(parent.sizes, childIndex);
|
||||||
|
state = state.setIn(['termGroups', group.parentUid, 'sizes'], newSizes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
.set('termGroups', state.termGroups.without(uid))
|
||||||
|
.set('activeSessions', state.activeSessions.without(uid));
|
||||||
|
};
|
||||||
|
|
||||||
|
const resizeGroup = (state, uid, sizes) => {
|
||||||
|
// Make sure none of the sizes fall below MIN_SIZE:
|
||||||
|
if (sizes.find(size => size < MIN_SIZE)) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.setIn(
|
||||||
|
['termGroups', uid, 'sizes'],
|
||||||
|
sizes
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducer = (state = initialState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SESSION_ADD: {
|
||||||
|
if (action.splitDirection) {
|
||||||
|
state = splitGroup(state, action);
|
||||||
|
return setActiveGroup(state, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uid = uuid.v4();
|
||||||
|
const termGroup = TermGroup({
|
||||||
|
uid,
|
||||||
|
sessionUid: action.uid
|
||||||
|
});
|
||||||
|
|
||||||
|
return state
|
||||||
|
.setIn(['termGroups', uid], termGroup)
|
||||||
|
.setIn(['activeSessions', uid], action.uid)
|
||||||
|
.set('activeRootGroup', uid);
|
||||||
|
}
|
||||||
|
case SESSION_SET_ACTIVE:
|
||||||
|
return setActiveGroup(state, action);
|
||||||
|
case TERM_GROUP_RESIZE:
|
||||||
|
return resizeGroup(state, action.uid, action.sizes);
|
||||||
|
case TERM_GROUP_EXIT:
|
||||||
|
return removeGroup(state, action.uid);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default decorateTermGroupsReducer(reducer);
|
||||||
|
|
@ -178,6 +178,12 @@ const reducer = (state = initial, action) => {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SESSION_RESIZE:
|
case SESSION_RESIZE:
|
||||||
|
// We only care about the sizes
|
||||||
|
// of standalone terms (i.e. not splits):
|
||||||
|
if (!action.isStandaloneTerm) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
state_ = state.merge({
|
state_ = state.merge({
|
||||||
rows: action.rows,
|
rows: action.rows,
|
||||||
cols: action.cols,
|
cols: action.cols,
|
||||||
|
|
|
||||||
9
lib/selectors.js
Normal file
9
lib/selectors.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import {createSelector} from 'reselect';
|
||||||
|
|
||||||
|
const getTermGroups = ({termGroups}) => termGroups.termGroups;
|
||||||
|
export const getRootGroups = createSelector(
|
||||||
|
getTermGroups,
|
||||||
|
termGroups => Object.keys(termGroups)
|
||||||
|
.map(uid => termGroups[uid])
|
||||||
|
.filter(({parentUid}) => !parentUid)
|
||||||
|
);
|
||||||
|
|
@ -29,9 +29,11 @@ let connectors;
|
||||||
let middlewares;
|
let middlewares;
|
||||||
let uiReducers;
|
let uiReducers;
|
||||||
let sessionsReducers;
|
let sessionsReducers;
|
||||||
|
let termGroupsReducers;
|
||||||
let tabPropsDecorators;
|
let tabPropsDecorators;
|
||||||
let tabsPropsDecorators;
|
let tabsPropsDecorators;
|
||||||
let termPropsDecorators;
|
let termPropsDecorators;
|
||||||
|
let termGroupPropsDecorators;
|
||||||
|
|
||||||
// the fs locations where usr plugins are stored
|
// the fs locations where usr plugins are stored
|
||||||
const {path, localPath} = plugins.getBasePaths();
|
const {path, localPath} = plugins.getBasePaths();
|
||||||
|
|
@ -69,9 +71,11 @@ const loadModules = () => {
|
||||||
uiReducers = [];
|
uiReducers = [];
|
||||||
middlewares = [];
|
middlewares = [];
|
||||||
sessionsReducers = [];
|
sessionsReducers = [];
|
||||||
|
termGroupsReducers = [];
|
||||||
tabPropsDecorators = [];
|
tabPropsDecorators = [];
|
||||||
tabsPropsDecorators = [];
|
tabsPropsDecorators = [];
|
||||||
termPropsDecorators = [];
|
termPropsDecorators = [];
|
||||||
|
termGroupPropsDecorators = [];
|
||||||
|
|
||||||
modules = paths.plugins.concat(paths.localPlugins)
|
modules = paths.plugins.concat(paths.localPlugins)
|
||||||
.map(path => {
|
.map(path => {
|
||||||
|
|
@ -138,6 +142,10 @@ const loadModules = () => {
|
||||||
connectors.Notifications.dispatch.push(mod.mapNotificationsDispatch);
|
connectors.Notifications.dispatch.push(mod.mapNotificationsDispatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mod.getTermGroupProps) {
|
||||||
|
termGroupPropsDecorators.push(mod.getTermGroupProps);
|
||||||
|
}
|
||||||
|
|
||||||
if (mod.getTermProps) {
|
if (mod.getTermProps) {
|
||||||
termPropsDecorators.push(mod.getTermProps);
|
termPropsDecorators.push(mod.getTermProps);
|
||||||
}
|
}
|
||||||
|
|
@ -170,6 +178,35 @@ export function reload() {
|
||||||
decorated = {};
|
decorated = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTermGroupProps(uid, parentProps, props) {
|
||||||
|
let props_;
|
||||||
|
|
||||||
|
termPropsDecorators.forEach(fn => {
|
||||||
|
let ret_;
|
||||||
|
|
||||||
|
if (!props_) {
|
||||||
|
props_ = Object.assign({}, props);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ret_ = fn(uid, parentProps, props_);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.stack);
|
||||||
|
notify('Plugin error', `${fn._pluginName}: Error occurred in \`getTermGroupProps\`. Check Developer Tools for details.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ret_ || typeof ret_ !== 'object') {
|
||||||
|
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`getTermGroupProps\` (object expected).`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
props = ret_;
|
||||||
|
});
|
||||||
|
|
||||||
|
return props_ || props;
|
||||||
|
}
|
||||||
|
|
||||||
export function getTermProps(uid, parentProps, props) {
|
export function getTermProps(uid, parentProps, props) {
|
||||||
let props_;
|
let props_;
|
||||||
|
|
||||||
|
|
@ -313,6 +350,33 @@ export function connect(stateFn, dispatchFn, c, d = {}) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function decorateTermGroupsReducer(fn) {
|
||||||
|
return (state, action) => {
|
||||||
|
let state_ = fn(state, action);
|
||||||
|
|
||||||
|
termGroupsReducers.forEach(pluginReducer => {
|
||||||
|
let state__;
|
||||||
|
|
||||||
|
try {
|
||||||
|
state__ = pluginReducer(state_, action);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err.stack);
|
||||||
|
notify('Plugin error', `${fn._pluginName}: Error occurred in \`reduceTermGroups\`. Check Developer Tools for details.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state__ || typeof state__ !== 'object') {
|
||||||
|
notify('Plugin error', `${fn._pluginName}: Invalid return value of \`reduceTermGroups\`.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state_ = state__;
|
||||||
|
});
|
||||||
|
|
||||||
|
return state_;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function decorateUIReducer(fn) {
|
export function decorateUIReducer(fn) {
|
||||||
return (state, action) => {
|
return (state, action) => {
|
||||||
let state_ = fn(state, action);
|
let state_ = fn(state, action);
|
||||||
|
|
|
||||||
6
lib/utils/term-groups.js
Normal file
6
lib/utils/term-groups.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export function findBySession(termGroupState, sessionUid) {
|
||||||
|
const {termGroups} = termGroupState;
|
||||||
|
return Object.keys(termGroups)
|
||||||
|
.map(uid => termGroups[uid])
|
||||||
|
.find(group => group.sessionUid === sessionUid);
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
"redux": "3.6.0",
|
"redux": "3.6.0",
|
||||||
"redux-thunk": "2.1.0",
|
"redux-thunk": "2.1.0",
|
||||||
"reselect": "2.5.4",
|
"reselect": "2.5.4",
|
||||||
|
"uuid": "2.0.2",
|
||||||
"seamless-immutable": "6.1.3"
|
"seamless-immutable": "6.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -38,7 +39,7 @@
|
||||||
"babel-preset-react": "^6.11.1",
|
"babel-preset-react": "^6.11.1",
|
||||||
"concurrently": "^3.0.0",
|
"concurrently": "^3.0.0",
|
||||||
"copy-webpack-plugin": "^3.0.1",
|
"copy-webpack-plugin": "^3.0.1",
|
||||||
"electron": "1.4.2",
|
"electron": "1.4.0",
|
||||||
"electron-builder": "^7.0.1",
|
"electron-builder": "^7.0.1",
|
||||||
"electron-devtools-installer": "^2.0.0",
|
"electron-devtools-installer": "^2.0.0",
|
||||||
"eslint-config-xo-react": "^0.10.0",
|
"eslint-config-xo-react": "^0.10.0",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue