porting files in lib/ actions, reducers and utils/plugins to ts (#3887)

* add type definitions

* rename files in lib/actions

* rename files from lib/reducers to ts

* renamed plugins.js to ts

* Add hyper types

* Fix ts errors in lib/reducers

* Fix ts errors in lib/actions

* Fix ts errors in plugins.ts
This commit is contained in:
Labhansh Agrawal 2019-10-19 22:29:56 +05:30 committed by Benjamin Staneck
parent 27c9cc3892
commit 537c746c75
14 changed files with 479 additions and 280 deletions

View file

@ -1,12 +1,13 @@
import rpc from '../rpc';
import INIT from '../constants';
import {Dispatch} from 'redux';
export default function init() {
return dispatch => {
return (dispatch: Dispatch<any>) => {
dispatch({
type: INIT,
effect: () => {
rpc.emit('init');
rpc.emit('init', null);
}
});
};

View file

@ -1,13 +1,13 @@
import {NOTIFICATION_MESSAGE, NOTIFICATION_DISMISS} from '../constants/notifications';
export function dismissNotification(id) {
export function dismissNotification(id: string) {
return {
type: NOTIFICATION_DISMISS,
id
};
}
export function addNotificationMessage(text, url = null, dismissable = true) {
export function addNotificationMessage(text: string, url: string | null = null, dismissable = true) {
return {
type: NOTIFICATION_MESSAGE,
text,

View file

@ -16,9 +16,11 @@ import {
SESSION_SEARCH,
SESSION_SEARCH_CLOSE
} from '../constants/sessions';
import {HyperState, session} from '../hyper';
import {Dispatch} from 'redux';
export function addSession({uid, shell, pid, cols, rows, splitDirection, activeUid}) {
return (dispatch, getState) => {
export function addSession({uid, shell, pid, cols, rows, splitDirection, activeUid}: session) {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
const {sessions} = getState();
const now = Date.now();
dispatch({
@ -36,7 +38,7 @@ export function addSession({uid, shell, pid, cols, rows, splitDirection, activeU
}
export function requestSession() {
return (dispatch, getState) => {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
dispatch({
type: SESSION_REQUEST,
effect: () => {
@ -49,8 +51,8 @@ export function requestSession() {
};
}
export function addSessionData(uid, data) {
return dispatch => {
export function addSessionData(uid: string, data: any) {
return (dispatch: Dispatch<any>) => {
dispatch({
type: SESSION_ADD_DATA,
data,
@ -67,8 +69,8 @@ export function addSessionData(uid, data) {
};
}
function createExitAction(type) {
return uid => (dispatch, getState) => {
function createExitAction(type: string) {
return (uid: string) => (dispatch: Dispatch<any>, getState: () => HyperState) => {
return dispatch({
type,
uid,
@ -91,8 +93,8 @@ function createExitAction(type) {
export const userExitSession = createExitAction(SESSION_USER_EXIT);
export const ptyExitSession = createExitAction(SESSION_PTY_EXIT);
export function setActiveSession(uid) {
return dispatch => {
export function setActiveSession(uid: string) {
return (dispatch: Dispatch<any>) => {
dispatch({
type: SESSION_SET_ACTIVE,
uid
@ -106,7 +108,7 @@ export function clearActiveSession() {
};
}
export function setSessionXtermTitle(uid, title) {
export function setSessionXtermTitle(uid: string, title: string) {
return {
type: SESSION_SET_XTERM_TITLE,
uid,
@ -114,10 +116,10 @@ export function setSessionXtermTitle(uid, title) {
};
}
export function resizeSession(uid, cols, rows) {
return (dispatch, getState) => {
export function resizeSession(uid: string, cols: number, rows: number) {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
const {termGroups} = getState();
const group = findBySession(termGroups, uid);
const group = findBySession(termGroups, uid)!;
const isStandaloneTerm = !group.parentUid && !group.children.length;
const now = Date.now();
dispatch({
@ -134,8 +136,8 @@ export function resizeSession(uid, cols, rows) {
};
}
export function onSearch(uid) {
return (dispatch, getState) => {
export function onSearch(uid: string) {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
const targetUid = uid || getState().sessions.activeUid;
dispatch({
type: SESSION_SEARCH,
@ -144,8 +146,8 @@ export function onSearch(uid) {
};
}
export function closeSearch(uid) {
return (dispatch, getState) => {
export function closeSearch(uid: string) {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
const targetUid = uid || getState().sessions.activeUid;
dispatch({
type: SESSION_SEARCH_CLOSE,
@ -154,8 +156,8 @@ export function closeSearch(uid) {
};
}
export function sendSessionData(uid, data, escaped) {
return (dispatch, getState) => {
export function sendSessionData(uid: string, data: any, escaped: any) {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
dispatch({
type: SESSION_USER_DATA,
data,

View file

@ -10,9 +10,12 @@ import {SESSION_REQUEST} from '../constants/sessions';
import findBySession from '../utils/term-groups';
import getRootGroups from '../selectors';
import {setActiveSession, ptyExitSession, userExitSession} from './sessions';
import {Dispatch} from 'redux';
import {ITermState, ITermGroup, HyperState} from '../hyper';
import {Immutable} from 'seamless-immutable';
function requestSplit(direction) {
return activeUid => (dispatch, getState) => {
function requestSplit(direction: string) {
return (activeUid: string) => (dispatch: Dispatch<any>, getState: () => HyperState): void => {
dispatch({
type: SESSION_REQUEST,
effect: () => {
@ -30,7 +33,7 @@ function requestSplit(direction) {
export const requestVerticalSplit = requestSplit(DIRECTION.VERTICAL);
export const requestHorizontalSplit = requestSplit(DIRECTION.HORIZONTAL);
export function resizeTermGroup(uid, sizes) {
export function resizeTermGroup(uid: string, sizes: number[]) {
return {
uid,
type: TERM_GROUP_RESIZE,
@ -38,8 +41,8 @@ export function resizeTermGroup(uid, sizes) {
};
}
export function requestTermGroup(activeUid) {
return (dispatch, getState) => {
export function requestTermGroup(activeUid: string) {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
dispatch({
type: TERM_GROUP_REQUEST,
effect: () => {
@ -55,8 +58,8 @@ export function requestTermGroup(activeUid) {
};
}
export function setActiveGroup(uid) {
return (dispatch, getState) => {
export function setActiveGroup(uid: string) {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
const {termGroups} = getState();
dispatch(setActiveSession(termGroups.activeSessions[uid]));
};
@ -65,12 +68,12 @@ export function setActiveGroup(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) => {
const findFirstSession = (state: Immutable<ITermState>, group: Immutable<ITermGroup>): string | undefined => {
if (group.sessionUid) {
return group.sessionUid;
}
for (const childUid of group.children) {
for (const childUid of group.children.asMutable()) {
const child = state.termGroups[childUid];
// We want to find the *leftmost* session,
// even if it's nested deep down:
@ -81,14 +84,14 @@ const findFirstSession = (state, group) => {
}
};
const findPrevious = (list, old) => {
const findPrevious = <T>(list: T[], old: T) => {
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) => {
const findNextSessionUid = (state: Immutable<ITermState>, group: Immutable<ITermGroup>) => {
// 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) {
@ -97,13 +100,13 @@ const findNextSessionUid = (state, group) => {
return findFirstSession(state, nextGroup);
}
const {children} = state.termGroups[group.parentUid];
const nextUid = findPrevious(children, group.uid);
return findFirstSession(state, state.termGroups[nextUid]);
const {children} = state.termGroups[group.parentUid!];
const nextUid = findPrevious(children.asMutable(), group.uid);
return findFirstSession(state, state.termGroups[nextUid!]);
};
export function ptyExitTermGroup(sessionUid) {
return (dispatch, getState) => {
export function ptyExitTermGroup(sessionUid: string) {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
const {termGroups} = getState();
const group = findBySession(termGroups, sessionUid);
// This might have already been closed:
@ -115,10 +118,10 @@ export function ptyExitTermGroup(sessionUid) {
type: TERM_GROUP_EXIT,
uid: group.uid,
effect: () => {
const activeSessionUid = termGroups.activeSessions[termGroups.activeRootGroup];
const activeSessionUid = termGroups.activeSessions[termGroups.activeRootGroup!];
if (Object.keys(termGroups.termGroups).length > 1 && activeSessionUid === sessionUid) {
const nextSessionUid = findNextSessionUid(termGroups, group);
dispatch(setActiveSession(nextSessionUid));
dispatch(setActiveSession(nextSessionUid!));
}
dispatch(ptyExitSession(sessionUid));
@ -127,8 +130,8 @@ export function ptyExitTermGroup(sessionUid) {
};
}
export function userExitTermGroup(uid) {
return (dispatch, getState) => {
export function userExitTermGroup(uid: string) {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
const {termGroups} = getState();
dispatch({
type: TERM_GROUP_EXIT,
@ -138,13 +141,13 @@ export function userExitTermGroup(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));
return dispatch(userExitSession(group.sessionUid!));
}
const activeSessionUid = termGroups.activeSessions[termGroups.activeRootGroup];
const activeSessionUid = termGroups.activeSessions[termGroups.activeRootGroup!];
if (termGroups.activeRootGroup === uid || activeSessionUid === group.sessionUid) {
const nextSessionUid = findNextSessionUid(termGroups, group);
dispatch(setActiveSession(nextSessionUid));
dispatch(setActiveSession(nextSessionUid!));
}
if (group.sessionUid) {
@ -160,13 +163,13 @@ export function userExitTermGroup(uid) {
}
export function exitActiveTermGroup() {
return (dispatch, getState) => {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
dispatch({
type: TERM_GROUP_EXIT_ACTIVE,
effect() {
const {sessions, termGroups} = getState();
const {uid} = findBySession(termGroups, sessions.activeUid);
dispatch(userExitTermGroup(uid));
const {uid} = findBySession(termGroups, sessions.activeUid!)!;
dispatch(userExitTermGroup(uid!));
}
});
};

View file

@ -28,11 +28,14 @@ import {
import {setActiveGroup} from './term-groups';
import parseUrl from 'parse-url';
import {Dispatch} from 'redux';
import {HyperState} from '../hyper';
import {Stats} from 'fs';
const {stat} = window.require('fs');
export function openContextMenu(uid, selection) {
return (dispatch, getState) => {
export function openContextMenu(uid: string, selection: any) {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
dispatch({
type: UI_CONTEXTMENU_OPEN,
uid,
@ -48,7 +51,7 @@ export function openContextMenu(uid, selection) {
}
export function increaseFontSize() {
return (dispatch, getState) => {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
dispatch({
type: UI_FONT_SIZE_INCR,
effect() {
@ -65,7 +68,7 @@ export function increaseFontSize() {
}
export function decreaseFontSize() {
return (dispatch, getState) => {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
dispatch({
type: UI_FONT_SIZE_DECR,
effect() {
@ -89,7 +92,7 @@ export function resetFontSize() {
}
export function setFontSmoothing() {
return dispatch => {
return (dispatch: Dispatch<any>) => {
setTimeout(() => {
const devicePixelRatio = window.devicePixelRatio;
const fontSmoothing = devicePixelRatio < 2 ? 'subpixel-antialiased' : 'antialiased';
@ -110,18 +113,21 @@ export function windowGeometryUpdated() {
// Find all sessions that are below the given
// termGroup uid in the hierarchy:
const findChildSessions = (termGroups, uid) => {
const findChildSessions = (termGroups: any, uid: string): string[] => {
const group = termGroups[uid];
if (group.sessionUid) {
return [uid];
}
return group.children.reduce((total, childUid) => total.concat(findChildSessions(termGroups, childUid)), []);
return group.children.reduce(
(total: string[], childUid: string) => total.concat(findChildSessions(termGroups, childUid)),
[]
);
};
// Get the index of the next or previous group,
// depending on the movement direction:
const getNeighborIndex = (groups, uid, type) => {
const getNeighborIndex = (groups: string[], uid: string, type: string) => {
if (type === UI_MOVE_NEXT_PANE) {
return (groups.indexOf(uid) + 1) % groups.length;
}
@ -129,21 +135,21 @@ const getNeighborIndex = (groups, uid, type) => {
return (groups.indexOf(uid) + groups.length - 1) % groups.length;
};
function moveToNeighborPane(type) {
return () => (dispatch, getState) => {
function moveToNeighborPane(type: string) {
return () => (dispatch: Dispatch<any>, getState: () => HyperState) => {
dispatch({
type,
effect() {
const {sessions, termGroups} = getState();
const {uid} = findBySession(termGroups, sessions.activeUid);
const childGroups = findChildSessions(termGroups.termGroups, termGroups.activeRootGroup);
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);
const index = getNeighborIndex(childGroups, uid!, type);
const {sessionUid} = termGroups.termGroups[childGroups[index]];
dispatch(setActiveSession(sessionUid));
dispatch(setActiveSession(sessionUid!));
}
}
});
@ -153,13 +159,13 @@ function moveToNeighborPane(type) {
export const moveToNextPane = moveToNeighborPane(UI_MOVE_NEXT_PANE);
export const moveToPreviousPane = moveToNeighborPane(UI_MOVE_PREV_PANE);
const getGroupUids = state => {
const getGroupUids = (state: HyperState) => {
const rootGroups = getRootGroups(state);
return rootGroups.map(({uid}) => uid);
};
export function moveLeft() {
return (dispatch, getState) => {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
dispatch({
type: UI_MOVE_LEFT,
effect() {
@ -180,7 +186,7 @@ export function moveLeft() {
}
export function moveRight() {
return (dispatch, getState) => {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
dispatch({
type: UI_MOVE_RIGHT,
effect() {
@ -200,8 +206,8 @@ export function moveRight() {
};
}
export function moveTo(i) {
return (dispatch, getState) => {
export function moveTo(i: number | 'last') {
return (dispatch: Dispatch<any>, getState: () => HyperState) => {
if (i === 'last') {
// Finding last tab index
const {termGroups} = getState().termGroups;
@ -217,11 +223,11 @@ export function moveTo(i) {
const state = getState();
const groupUids = getGroupUids(state);
const uid = state.termGroups.activeRootGroup;
if (uid === groupUids[i]) {
if (uid === groupUids[i as number]) {
//eslint-disable-next-line no-console
console.log('ignoring same uid');
} else if (groupUids[i]) {
dispatch(setActiveGroup(groupUids[i]));
} else if (groupUids[i as number]) {
dispatch(setActiveGroup(groupUids[i as number]));
} else {
//eslint-disable-next-line no-console
console.log('ignoring inexistent index', i);
@ -231,8 +237,8 @@ export function moveTo(i) {
};
}
export function windowMove(window) {
return dispatch => {
export function windowMove(window: any) {
return (dispatch: Dispatch<any>) => {
dispatch({
type: UI_WINDOW_MOVE,
window,
@ -244,7 +250,7 @@ export function windowMove(window) {
}
export function windowGeometryChange() {
return dispatch => {
return (dispatch: Dispatch<any>) => {
dispatch({
type: UI_WINDOW_MOVE,
effect() {
@ -254,12 +260,12 @@ export function windowGeometryChange() {
};
}
export function openFile(path) {
return dispatch => {
export function openFile(path: string) {
return (dispatch: Dispatch<any>) => {
dispatch({
type: UI_OPEN_FILE,
effect() {
stat(path, (err, stats) => {
stat(path, (err: any, stats: Stats) => {
if (err) {
notify('Unable to open path', `"${path}" doesn't exist.`, {error: err});
} else {
@ -271,7 +277,7 @@ export function openFile(path) {
}
rpc.once('session add', ({uid}) => {
rpc.once('session data', () => {
dispatch(sendSessionData(uid, command));
dispatch(sendSessionData(uid, command, null));
});
});
}
@ -294,12 +300,12 @@ export function leaveFullScreen() {
};
}
export function openSSH(url) {
return dispatch => {
export function openSSH(url: string) {
return (dispatch: Dispatch<any>) => {
dispatch({
type: UI_OPEN_SSH_URL,
effect() {
let parsedUrl = parseUrl(url, true);
const parsedUrl = parseUrl(url, true);
let command = parsedUrl.protocol + ' ' + (parsedUrl.user ? `${parsedUrl.user}@` : '') + parsedUrl.resource;
if (parsedUrl.port) command += ' -p ' + parsedUrl.port;
@ -308,7 +314,7 @@ export function openSSH(url) {
rpc.once('session add', ({uid}) => {
rpc.once('session data', () => {
dispatch(sendSessionData(uid, command));
dispatch(sendSessionData(uid, command, null));
});
});
@ -318,8 +324,8 @@ export function openSSH(url) {
};
}
export function execCommand(command, fn, e) {
return dispatch =>
export function execCommand(command: any, fn: any, e: any) {
return (dispatch: Dispatch<any>) =>
dispatch({
type: UI_COMMAND_EXEC,
command,

131
lib/hyper.d.ts vendored
View file

@ -1,3 +1,6 @@
import {Reducer} from 'redux';
import {Immutable} from 'seamless-immutable';
declare global {
interface Window {
__rpcId: string;
@ -20,3 +23,131 @@ export type ITermState = {
activeSessions: Record<string, string>;
activeRootGroup: string | null;
};
export type ITermGroupReducer = Reducer<Immutable<ITermState>, any>;
export type uiState = {
_lastUpdate: null;
activeUid: string | null;
activityMarkers: {};
backgroundColor: string;
bell: string;
bellSoundURL: string | null;
bellSound: string | null;
borderColor: string;
colors: {
black: string;
blue: string;
cyan: string;
green: string;
lightBlack: string;
lightBlue: string;
lightCyan: string;
lightGreen: string;
lightMagenta: string;
lightRed: string;
lightWhite: string;
lightYellow: string;
magenta: string;
red: string;
white: string;
yellow: string;
};
cols: number | null;
copyOnSelect: boolean;
css: string;
cursorAccentColor: string;
cursorBlink: boolean;
cursorColor: string;
cursorShape: string;
cwd?: string;
disableLigatures: boolean;
fontFamily: string;
fontSize: number;
fontSizeOverride: null | number;
fontSmoothingOverride: string;
fontWeight: string;
fontWeightBold: string;
foregroundColor: string;
fullScreen: boolean;
letterSpacing: number;
lineHeight: number;
macOptionSelectionMode: string;
maximized: boolean;
messageDismissable: null | boolean;
messageText: null;
messageURL: null;
modifierKeys: {
altIsMeta: boolean;
cmdIsMeta: boolean;
};
notifications: {
font: boolean;
message: boolean;
resize: boolean;
updates: boolean;
};
openAt: Record<string, number>;
padding: string;
quickEdit: boolean;
resizeAt: number;
rows: number | null;
scrollback: number;
selectionColor: string;
showHamburgerMenu: string;
showWindowControls: string;
termCSS: string;
uiFontFamily: string;
updateCanInstall: null | boolean;
updateNotes: null;
updateReleaseUrl: null;
updateVersion: null;
webGLRenderer: boolean;
};
export type IUiReducer = Reducer<Immutable<uiState>>;
export type session = {
cleared: boolean;
cols: null;
pid: null;
resizeAt: number;
rows: null;
search: boolean;
shell: string;
title: string;
uid: string;
url: null;
splitDirection: string;
activeUid: string;
};
export type sessionState = {
sessions: Record<string, Partial<session>>;
activeUid: string | null;
};
export type ISessionReducer = Reducer<Immutable<sessionState>>;
export type hyperPlugin = {
getTabProps: any;
getTabsProps: any;
getTermGroupProps: any;
getTermProps: any;
mapHeaderDispatch: any;
mapHyperDispatch: any;
mapHyperTermDispatch: any;
mapNotificationsDispatch: any;
mapTermsDispatch: any;
mapHeaderState: any;
mapHyperState: any;
mapHyperTermState: any;
mapNotificationsState: any;
mapTermsState: any;
middleware: any;
onRendererWindow: any;
reduceSessions: any;
reduceTermGroups: any;
reduceUI: any;
};
import rootReducer from './reducers/index';
export type HyperState = ReturnType<typeof rootReducer>;

8
lib/index.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
declare module 'php-escape-shell' {
// eslint-disable-next-line @typescript-eslint/camelcase
export function php_escapeshellcmd(path: string): string;
}
declare module 'parse-url' {
export default function(...args: any[]): any;
}

View file

@ -1,4 +1,4 @@
import Immutable from 'seamless-immutable';
import Immutable, {Immutable as ImmutableType} from 'seamless-immutable';
import {decorateSessionsReducer} from '../utils/plugins';
import {
SESSION_ADD,
@ -13,13 +13,14 @@ import {
SESSION_SEARCH,
SESSION_SEARCH_CLOSE
} from '../constants/sessions';
import {sessionState} from '../hyper';
const initialState = Immutable({
const initialState: ImmutableType<sessionState> = Immutable({
sessions: {},
activeUid: null
});
function Session(obj) {
function Session(obj: Immutable.DeepPartial<sessionState['sessions']>) {
return Immutable({
uid: '',
title: '',
@ -33,7 +34,15 @@ function Session(obj) {
}).merge(obj);
}
const reducer = (state = initialState, action) => {
function deleteSession(state: ImmutableType<sessionState>, uid: string) {
return state.updateIn(['sessions'], (sessions: ImmutableType<any>) => {
const sessions_ = sessions.asMutable();
delete sessions_[uid];
return sessions_;
});
}
const reducer = (state: ImmutableType<sessionState> = initialState, action: any) => {
switch (action.type) {
case SESSION_ADD:
return state.set('activeUid', action.uid).setIn(
@ -60,7 +69,7 @@ const reducer = (state = initialState, action) => {
return state.merge(
{
sessions: {
[state.activeUid]: {
[state.activeUid!]: {
cleared: true
}
}
@ -126,11 +135,3 @@ const reducer = (state = initialState, action) => {
};
export default decorateSessionsReducer(reducer);
function deleteSession(state, uid) {
return state.updateIn(['sessions'], sessions => {
const sessions_ = sessions.asMutable();
delete sessions_[uid];
return sessions_;
});
}

View file

@ -1,18 +1,19 @@
import uuid from 'uuid';
import Immutable from 'seamless-immutable';
import Immutable, {Immutable as ImmutableType} 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';
import {ITermGroup, ITermState, ITermGroups} from '../hyper';
const MIN_SIZE = 0.05;
const initialState = Immutable({
const initialState = Immutable<ITermState>({
termGroups: {},
activeSessions: {},
activeRootGroup: null
});
function TermGroup(obj) {
function TermGroup(obj: Immutable.DeepPartial<ITermGroup>) {
return Immutable({
uid: null,
sessionUid: null,
@ -20,11 +21,11 @@ function TermGroup(obj) {
direction: null,
sizes: null,
children: []
}).merge(obj);
} as ITermGroup).merge(obj);
}
// Recurse upwards until we find a root term group (no parent).
const findRootGroup = (termGroups, uid) => {
const findRootGroup = (termGroups: ImmutableType<ITermGroups>, uid: string): ImmutableType<ITermGroup> => {
const current = termGroups[uid];
if (!current.parentUid) {
return current;
@ -33,35 +34,40 @@ const findRootGroup = (termGroups, uid) => {
return findRootGroup(termGroups, current.parentUid);
};
const setActiveGroup = (state, action) => {
const setActiveGroup = (state: ImmutableType<ITermState>, action: {uid: string}) => {
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);
const childGroup = findBySession(state, action.uid)!;
const rootGroup = findRootGroup(state.termGroups, childGroup.uid!);
return state.set('activeRootGroup', rootGroup.uid).setIn(['activeSessions', rootGroup.uid as string], action.uid);
};
// Reduce existing sizes to fit a new split:
const insertRebalance = (oldSizes, index) => {
const insertRebalance = (oldSizes: ImmutableType<number[]>, index: any) => {
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)];
return [...balanced.slice(0, index).asMutable(), newSize, ...balanced.slice(index).asMutable()];
};
// Spread out the removed size to all the existing sizes:
const removalRebalance = (oldSizes, index) => {
const removalRebalance = (oldSizes: ImmutableType<number[]>, index: number) => {
const removedSize = oldSizes[index];
const increase = removedSize / (oldSizes.length - 1);
return oldSizes.filter((_size, i) => i !== index).map(size => size + increase);
return Immutable(
oldSizes
.asMutable()
.filter((_size: number, i: number) => i !== index)
.map((size: number) => size + increase)
);
};
const splitGroup = (state, action) => {
const splitGroup = (state: ImmutableType<ITermState>, action: {splitDirection: any; uid: any; activeUid: any}) => {
const {splitDirection, uid, activeUid} = action;
const activeGroup = findBySession(state, activeUid);
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:
@ -84,7 +90,7 @@ const splitGroup = (state, action) => {
parentUid: parentGroup.uid
});
state = state.setIn(['termGroups', newSession.uid], newSession);
state = state.setIn(['termGroups', newSession.uid as string], newSession);
if (parentGroup.sessionUid) {
const existingSession = TermGroup({
uid: uuid.v4(),
@ -92,22 +98,22 @@ const splitGroup = (state, action) => {
parentUid: parentGroup.uid
});
return state.setIn(['termGroups', existingSession.uid], existingSession).setIn(
['termGroups', parentGroup.uid],
return state.setIn(['termGroups', existingSession.uid as string], existingSession).setIn(
['termGroups', parentGroup.uid!],
parentGroup.merge({
sessionUid: null,
sessionUid: '',
direction: splitDirection,
children: [existingSession.uid, newSession.uid]
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)];
const index = children.indexOf(activeGroup.uid!) + 1;
const newChildren = [...children.slice(0, index).asMutable(), newSession.uid!, ...children.slice(index).asMutable()];
state = state.setIn(
['termGroups', parentGroup.uid],
['termGroups', parentGroup.uid!],
parentGroup.merge({
direction: splitDirection,
children: newChildren
@ -116,7 +122,7 @@ const splitGroup = (state, action) => {
if (parentGroup.sizes) {
const newSizes = insertRebalance(parentGroup.sizes, index);
state = state.setIn(['termGroups', parentGroup.uid, 'sizes'], newSizes);
state = state.setIn(['termGroups', parentGroup.uid!, 'sizes'], newSizes);
}
return state;
@ -125,19 +131,19 @@ const splitGroup = (state, action) => {
// 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) => {
const replaceParent = (state: ImmutableType<ITermState>, parent: ImmutableType<ITermGroup>, child: {uid: any}) => {
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));
const newChildren = parentParent.children.map((uid: any) => (uid === parent.uid ? child.uid : uid));
state = state.setIn(['termGroups', parentParent.uid, 'children'], newChildren);
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)
@ -146,17 +152,17 @@ const replaceParent = (state, parent, child) => {
}
return state
.set('termGroups', state.termGroups.without(parent.uid))
.set('termGroups', state.termGroups.without(parent.uid!))
.setIn(['termGroups', child.uid, 'parentUid'], parent.parentUid);
};
const removeGroup = (state, uid) => {
const removeGroup = (state: ImmutableType<ITermState>, uid: string) => {
const group = state.termGroups[uid];
// when close tab with multiple panes, it remove group from parent to child. so maybe the parentUid exists but parent group have removed.
// it's safe to remove the group.
if (group.parentUid && state.termGroups[group.parentUid]) {
const parent = state.termGroups[group.parentUid];
const newChildren = parent.children.filter(childUid => childUid !== uid);
const newChildren = parent.children.filter((childUid: any) => childUid !== uid);
if (newChildren.length === 1) {
// Since we only have one child left,
// we can merge the parent and child into one group:
@ -177,7 +183,7 @@ const removeGroup = (state, uid) => {
.set('activeSessions', state.activeSessions.without(uid));
};
const resizeGroup = (state, uid, sizes) => {
const resizeGroup = (state: ImmutableType<ITermState>, uid: any, sizes: number[]) => {
// Make sure none of the sizes fall below MIN_SIZE:
if (sizes.find(size => size < MIN_SIZE)) {
return state;
@ -186,7 +192,16 @@ const resizeGroup = (state, uid, sizes) => {
return state.setIn(['termGroups', uid, 'sizes'], sizes);
};
const reducer = (state = initialState, action) => {
const reducer = (
state = initialState,
action: {
splitDirection: any;
uid: any;
activeUid: any;
type: any;
sizes: any;
}
) => {
switch (action.type) {
case SESSION_ADD: {
if (action.splitDirection) {

View file

@ -1,5 +1,5 @@
import {remote} from 'electron';
import Immutable from 'seamless-immutable';
import Immutable, {Immutable as ImmutableType} from 'seamless-immutable';
import {decorateUIReducer} from '../utils/plugins';
import {CONFIG_LOAD, CONFIG_RELOAD} from '../constants/config';
import {
@ -22,6 +22,7 @@ import {
SESSION_SET_CWD
} from '../constants/sessions';
import {UPDATE_AVAILABLE} from '../constants/updater';
import {uiState} from '../hyper';
const allowedCursorShapes = new Set(['BEAM', 'BLOCK', 'UNDERLINE']);
const allowedCursorBlinkValues = new Set([true, false]);
@ -30,7 +31,7 @@ const allowedHamburgerMenuValues = new Set([true, false]);
const allowedWindowControlsValues = new Set([true, false, 'left']);
// Populate `config-default.js` from this :)
const initial = Immutable({
const initial: ImmutableType<uiState> = Immutable({
cols: null,
rows: null,
scrollback: 1000,
@ -87,6 +88,9 @@ const initial = Immutable({
maximized: false,
updateVersion: null,
updateNotes: null,
updateReleaseUrl: null,
updateCanInstall: null,
_lastUpdate: null,
messageText: null,
messageURL: null,
messageDismissable: null,
@ -108,7 +112,7 @@ const initial = Immutable({
const currentWindow = remote.getCurrentWindow();
const reducer = (state = initial, action) => {
const reducer = (state = initial, action: any) => {
let state_ = state;
let isMax;
//eslint-disable-next-line default-case
@ -122,7 +126,7 @@ const reducer = (state = initial, action) => {
// font size changed from the config
.merge(
(() => {
const ret = {};
const ret: Immutable.DeepPartial<uiState> = {};
if (config.scrollback) {
ret.scrollback = config.scrollback;
@ -295,12 +299,12 @@ const reducer = (state = initial, action) => {
case SESSION_PTY_EXIT:
state_ = state
.updateIn(['openAt'], times => {
.updateIn(['openAt'], (times: ImmutableType<any>) => {
const times_ = times.asMutable();
delete times_[action.uid];
return times_;
})
.updateIn(['activityMarkers'], markers => {
.updateIn(['activityMarkers'], (markers: ImmutableType<any>) => {
const markers_ = markers.asMutable();
delete markers_[action.uid];
return markers_;

View file

@ -9,10 +9,135 @@ import React, {PureComponent} from 'react';
import ReactDOM from 'react-dom';
import Notification from '../components/notification';
import notify from './notify';
import {hyperPlugin, IUiReducer, ISessionReducer, ITermGroupReducer} from '../hyper';
const Module = require('module');
// remote interface to `../plugins`
const plugins = remote.require('./plugins') as typeof import('../../app/plugins');
// `require`d modules
let modules: any;
// cache of decorated components
let decorated: Record<string, any> = {};
// various caches extracted of the plugin methods
let connectors: {
Terms: {state: any[]; dispatch: any[]};
Header: {state: any[]; dispatch: any[]};
Hyper: {state: any[]; dispatch: any[]};
Notifications: {state: any[]; dispatch: any[]};
};
let middlewares: any[];
let uiReducers: IUiReducer[];
let sessionsReducers: ISessionReducer[];
let termGroupsReducers: ITermGroupReducer[];
let tabPropsDecorators: any[];
let tabsPropsDecorators: any[];
let termPropsDecorators: any[];
let termGroupPropsDecorators: any[];
let propsDecorators: {
getTermProps: any[];
getTabProps: any[];
getTabsProps: any[];
getTermGroupProps: any[];
};
let reducersDecorators: {
reduceUI: IUiReducer[];
reduceSessions: ISessionReducer[];
reduceTermGroups: ITermGroupReducer[];
};
// expose decorated component instance to the higher-order components
function exposeDecorated(Component_: any) {
return class DecoratedComponent extends React.Component<any, any> {
constructor(props: any, context: any) {
super(props, context);
this.onRef = this.onRef.bind(this);
}
onRef(decorated_: any) {
if (this.props.onDecorated) {
try {
this.props.onDecorated(decorated_);
} catch (e) {
notify('Plugin error', `Error occurred. Check Developer Tools for details`, {error: e});
}
}
}
render() {
return React.createElement(Component_, Object.assign({}, this.props, {ref: this.onRef}));
}
};
}
function getDecorated(parent: any, name: string) {
if (!decorated[name]) {
let class_ = exposeDecorated(parent);
(class_ as any).displayName = `_exposeDecorated(${name})`;
modules.forEach((mod: any) => {
const method = 'decorate' + name;
const fn = mod[method];
if (fn) {
let class__;
try {
class__ = fn(class_, {React, PureComponent, Notification, notify});
class__.displayName = `${fn._pluginName}(${name})`;
} catch (err) {
notify(
'Plugin error',
`${fn._pluginName}: Error occurred in \`${method}\`. Check Developer Tools for details`,
{error: err}
);
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\`.`
);
return;
}
class_ = class__;
}
});
decorated[name] = class_;
}
return decorated[name];
}
// for each component, we return a higher-order component
// that wraps with the higher-order components
// exposed by plugins
export function decorate(Component_: any, name: string) {
return class DecoratedComponent extends React.Component<any, {hasError: boolean}> {
constructor(props: any) {
super(props);
this.state = {hasError: false};
}
componentDidCatch() {
this.setState({hasError: true});
// No need to detail this error because React print these information.
notify(
'Plugin error',
`Plugins decorating ${name} has been disabled because of a plugin crash. Check Developer Tools for details.`
);
}
render() {
const Sub = this.state.hasError ? Component_ : getDecorated(Component_, name);
return React.createElement(Sub, this.props);
}
};
}
const Module = require('module') as typeof import('module') & {_load: Function};
const originalLoad = Module._load;
Module._load = function _load(path) {
Module._load = function _load(path: string) {
// PLEASE NOTE: Code changes here, also need to be changed in
// app/plugins.js
switch (path) {
@ -37,38 +162,17 @@ Module._load = function _load(path) {
case 'hyper/decorate':
return decorate;
default:
// eslint-disable-next-line prefer-rest-params
return originalLoad.apply(this, arguments);
}
};
// remote interface to `../plugins`
const plugins = remote.require('./plugins');
// `require`d modules
let modules;
// cache of decorated components
let decorated = {};
// various caches extracted of the plugin methods
let connectors;
let middlewares;
let uiReducers;
let sessionsReducers;
let termGroupsReducers;
let tabPropsDecorators;
let tabsPropsDecorators;
let termPropsDecorators;
let termGroupPropsDecorators;
let propsDecorators;
let reducersDecorators;
const clearModulesCache = () => {
// the fs locations where user plugins are stored
const {path, localPath} = plugins.getBasePaths();
// trigger unload hooks
modules.forEach(mod => {
modules.forEach((mod: any) => {
if (mod.onRendererUnload) {
mod.onRendererUnload(window);
}
@ -83,14 +187,14 @@ const clearModulesCache = () => {
}
};
const pathModule = window.require('path');
const pathModule = window.require('path') as typeof import('path');
const getPluginName = path => pathModule.basename(path);
const getPluginName = (path: string) => pathModule.basename(path);
const getPluginVersion = path => {
const getPluginVersion = (path: string): string | null => {
let version = null;
try {
version = window.require(pathModule.resolve(path, 'package.json')).version;
version = (window.require(pathModule.resolve(path, 'package.json')) as any).version as string;
} catch (err) {
//eslint-disable-next-line no-console
console.warn(`No package.json found in ${path}`);
@ -132,19 +236,19 @@ const loadModules = () => {
reduceTermGroups: termGroupsReducers
};
const loadedPlugins = plugins.getLoadedPluginVersions().map(plugin => plugin.name);
const loadedPlugins = plugins.getLoadedPluginVersions().map((plugin: any) => plugin.name);
modules = paths.plugins
.concat(paths.localPlugins)
.filter(plugin => loadedPlugins.indexOf(basename(plugin)) !== -1)
.map(path => {
let mod;
.filter((plugin: any) => loadedPlugins.indexOf(basename(plugin)) !== -1)
.map((path: any) => {
let mod: hyperPlugin;
const pluginName = getPluginName(path);
const pluginVersion = getPluginVersion(path);
// window.require allows us to ensure this doesn't get
// in the way of our build
try {
mod = window.require(path);
mod = window.require(path) as any;
} catch (err) {
notify(
'Plugin load error',
@ -154,12 +258,12 @@ const loadModules = () => {
return undefined;
}
for (const i in mod) {
if ({}.hasOwnProperty.call(mod, i)) {
(Object.keys(mod) as (keyof typeof mod)[]).forEach(i => {
if (Object.hasOwnProperty.call(mod, i)) {
mod[i]._pluginName = pluginName;
mod[i]._pluginVersion = pluginVersion;
}
}
});
// mapHyperTermState mapping for backwards compatibility with hyperterm
if (mod.mapHyperTermState) {
@ -247,9 +351,9 @@ const loadModules = () => {
return mod;
})
.filter(mod => Boolean(mod));
.filter((mod: any) => Boolean(mod));
const deprecatedPlugins = plugins.getDeprecatedConfig();
const deprecatedPlugins: Record<string, any> = plugins.getDeprecatedConfig();
Object.keys(deprecatedPlugins).forEach(name => {
const {css} = deprecatedPlugins[name];
if (css) {
@ -270,9 +374,9 @@ export function reload() {
decorated = {};
}
function getProps(name, props, ...fnArgs) {
function getProps(name: keyof typeof propsDecorators, props: any, ...fnArgs: any[]) {
const decorators = propsDecorators[name];
let props_;
let props_: typeof props;
decorators.forEach(fn => {
let ret_;
@ -301,27 +405,27 @@ function getProps(name, props, ...fnArgs) {
return props_ || props;
}
export function getTermGroupProps(uid, parentProps, props) {
export function getTermGroupProps(uid: string, parentProps: any, props: any) {
return getProps('getTermGroupProps', props, uid, parentProps);
}
export function getTermProps(uid, parentProps, props) {
export function getTermProps(uid: string, parentProps: any, props: any) {
return getProps('getTermProps', props, uid, parentProps);
}
export function getTabsProps(parentProps, props) {
export function getTabsProps(parentProps: any, props: any) {
return getProps('getTabsProps', props, parentProps);
}
export function getTabProps(tab, parentProps, props) {
export function getTabProps(tab: any, parentProps: any, props: any) {
return getProps('getTabProps', props, tab, parentProps);
}
// connects + decorates a class
// plugins can override mapToState, dispatchToProps
// and the class gets decorated (proxied)
export function connect(stateFn, dispatchFn, c, d = {}) {
return (Class, name) => {
export function connect(stateFn: Function, dispatchFn: Function, c: any, d = {}) {
return (Class: any, name: keyof typeof connectors) => {
return reduxConnect(
state => {
let ret = stateFn(state);
@ -382,12 +486,16 @@ export function connect(stateFn, dispatchFn, c, d = {}) {
};
}
function decorateReducer(name, fn) {
const decorateReducer: {
(name: 'reduceUI', fn: IUiReducer): IUiReducer;
(name: 'reduceSessions', fn: ISessionReducer): ISessionReducer;
(name: 'reduceTermGroups', fn: ITermGroupReducer): ITermGroupReducer;
} = <T extends keyof typeof reducersDecorators>(name: T, fn: any) => {
const reducers = reducersDecorators[name];
return (state, action) => {
return (state: any, action: any) => {
let state_ = fn(state, action);
reducers.forEach(pluginReducer => {
reducers.forEach((pluginReducer: any) => {
let state__;
try {
@ -409,111 +517,23 @@ function decorateReducer(name, fn) {
return state_;
};
}
};
export function decorateTermGroupsReducer(fn) {
export function decorateTermGroupsReducer(fn: ITermGroupReducer) {
return decorateReducer('reduceTermGroups', fn);
}
export function decorateUIReducer(fn) {
export function decorateUIReducer(fn: IUiReducer) {
return decorateReducer('reduceUI', fn);
}
export function decorateSessionsReducer(fn) {
export function decorateSessionsReducer(fn: ISessionReducer) {
return decorateReducer('reduceSessions', fn);
}
// redux middleware generator
export const middleware = store => next => action => {
const nextMiddleware = remaining => action_ =>
export const middleware = (store: any) => (next: any) => (action: any) => {
const nextMiddleware = (remaining: any[]) => (action_: any) =>
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 DecoratedComponent extends React.Component {
constructor(props, context) {
super(props, context);
this.onRef = this.onRef.bind(this);
}
onRef(decorated_) {
if (this.props.onDecorated) {
try {
this.props.onDecorated(decorated_);
} catch (e) {
notify('Plugin error', `Error occurred. Check Developer Tools for details`, {error: e});
}
}
}
render() {
return React.createElement(Component_, Object.assign({}, this.props, {ref: this.onRef}));
}
};
}
function getDecorated(parent, name) {
if (!decorated[name]) {
let class_ = exposeDecorated(parent);
class_.displayName = `_exposeDecorated(${name})`;
modules.forEach(mod => {
const method = 'decorate' + name;
const fn = mod[method];
if (fn) {
let class__;
try {
class__ = fn(class_, {React, PureComponent, Notification, notify});
class__.displayName = `${fn._pluginName}(${name})`;
} catch (err) {
notify(
'Plugin error',
`${fn._pluginName}: Error occurred in \`${method}\`. Check Developer Tools for details`,
{error: err}
);
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\`.`
);
return;
}
class_ = class__;
}
});
decorated[name] = class_;
}
return decorated[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 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 these information.
notify(
'Plugin error',
`Plugins decorating ${name} has been disabled because of a plugin crash. Check Developer Tools for details.`
);
}
render() {
const Sub = this.state.hasError ? Component_ : getDecorated(Component_, name);
return React.createElement(Sub, this.props);
}
};
}

View file

@ -286,6 +286,7 @@
"@types/react-dom": "^16.9.1",
"@types/react-redux": "^7.1.4",
"@types/seamless-immutable": "7.1.11",
"@types/uuid": "3.4.5",
"@typescript-eslint/eslint-plugin": "2.4.0",
"@typescript-eslint/parser": "2.3.3",
"ava": "0.25.0",

View file

@ -513,6 +513,13 @@
resolved "https://registry.yarnpkg.com/@types/seamless-immutable/-/seamless-immutable-7.1.11.tgz#89250c3e2587a44c2a051f5798e6f29f0e91bbc9"
integrity sha512-U8Mp+Q6P5ZxG6KDRwWduQl822MnsnBtOq/Lb4HQB9Tzi8t1nr7PLqq88I4kTwEDHO95hcH8y8KhGvxWPIS6QoQ==
"@types/uuid@3.4.5":
version "3.4.5"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.5.tgz#d4dc10785b497a1474eae0ba7f0cb09c0ddfd6eb"
integrity sha512-MNL15wC3EKyw1VLF+RoVO4hJJdk9t/Hlv3rt1OL65Qvuadm4BYo6g9ZJQqoq7X8NBFSsQXgAujWciovh2lpVjA==
dependencies:
"@types/node" "*"
"@typescript-eslint/eslint-plugin@2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.4.0.tgz#aaf6b542ff75b78f4191a8bf1c519184817caa24"