mirror of
https://github.com/quine-global/hyper.git
synced 2026-01-15 21:28:40 -09:00
strongly typed rpc
This commit is contained in:
parent
e31d72cc31
commit
2b644e1fbb
8 changed files with 169 additions and 40 deletions
33
app/rpc.ts
33
app/rpc.ts
|
|
@ -1,15 +1,18 @@
|
||||||
import {EventEmitter} from 'events';
|
import {EventEmitter} from 'events';
|
||||||
import {ipcMain, BrowserWindow} from 'electron';
|
import {ipcMain, BrowserWindow} from 'electron';
|
||||||
import {v4 as uuidv4} from 'uuid';
|
import {v4 as uuidv4} from 'uuid';
|
||||||
|
import {TypedEmitter, MainEvents, RendererEvents, FilterNever} from '../common';
|
||||||
|
|
||||||
export class Server extends EventEmitter {
|
export class Server {
|
||||||
|
emitter: TypedEmitter<MainEvents>;
|
||||||
destroyed = false;
|
destroyed = false;
|
||||||
win: BrowserWindow;
|
win: BrowserWindow;
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
constructor(win: BrowserWindow) {
|
constructor(win: BrowserWindow) {
|
||||||
super();
|
this.emitter = new EventEmitter();
|
||||||
this.win = win;
|
this.win = win;
|
||||||
this.ipcListener = this.ipcListener.bind(this);
|
this.emit = this.emit.bind(this);
|
||||||
|
|
||||||
if (this.destroyed) {
|
if (this.destroyed) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -18,7 +21,6 @@ export class Server extends EventEmitter {
|
||||||
const uid = uuidv4();
|
const uid = uuidv4();
|
||||||
this.id = uid;
|
this.id = uid;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
||||||
ipcMain.on(uid, this.ipcListener);
|
ipcMain.on(uid, this.ipcListener);
|
||||||
|
|
||||||
// we intentionally subscribe to `on` instead of `once`
|
// we intentionally subscribe to `on` instead of `once`
|
||||||
|
|
@ -33,23 +35,34 @@ export class Server extends EventEmitter {
|
||||||
return this.win.webContents;
|
return this.win.webContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcListener(event: any, {ev, data}: {ev: string; data: any}) {
|
ipcListener = (event: any, {ev, data}: {ev: keyof MainEvents; data: any}) => this.emitter.emit(ev, data);
|
||||||
super.emit(ev, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(ch: string, data: any = {}): any {
|
on = <U extends keyof MainEvents>(ev: U, fn: (arg0: MainEvents[U]) => void) => {
|
||||||
|
this.emitter.on(ev, fn);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
once = <U extends keyof MainEvents>(ev: U, fn: (arg0: MainEvents[U]) => void) => {
|
||||||
|
this.emitter.once(ev, fn);
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
emit<U extends Exclude<keyof RendererEvents, FilterNever<RendererEvents>>>(ch: U): boolean;
|
||||||
|
emit<U extends FilterNever<RendererEvents>>(ch: U, data: RendererEvents[U]): boolean;
|
||||||
|
emit<U extends keyof RendererEvents>(ch: U, data?: RendererEvents[U]) {
|
||||||
// This check is needed because data-batching can cause extra data to be
|
// This check is needed because data-batching can cause extra data to be
|
||||||
// emitted after the window has already closed
|
// emitted after the window has already closed
|
||||||
if (!this.win.isDestroyed()) {
|
if (!this.win.isDestroyed()) {
|
||||||
this.wc.send(this.id, {ch, data});
|
this.wc.send(this.id, {ch, data});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy() {
|
destroy() {
|
||||||
this.removeAllListeners();
|
this.emitter.removeAllListeners();
|
||||||
this.wc.removeAllListeners();
|
this.wc.removeAllListeners();
|
||||||
if (this.id) {
|
if (this.id) {
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
||||||
ipcMain.removeListener(this.id, this.ipcListener);
|
ipcMain.removeListener(this.id, this.ipcListener);
|
||||||
} else {
|
} else {
|
||||||
// mark for `genUid` in constructor
|
// mark for `genUid` in constructor
|
||||||
|
|
|
||||||
|
|
@ -206,8 +206,8 @@ export function newWindow(
|
||||||
session.resize({cols, rows});
|
session.resize({cols, rows});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
rpc.on('data', ({uid, data, escaped}: {uid: string; data: string; escaped: boolean}) => {
|
rpc.on('data', ({uid, data, escaped}) => {
|
||||||
const session = sessions.get(uid);
|
const session = uid && sessions.get(uid);
|
||||||
if (session) {
|
if (session) {
|
||||||
if (escaped) {
|
if (escaped) {
|
||||||
const escapedData = session.shell?.endsWith('cmd.exe')
|
const escapedData = session.shell?.endsWith('cmd.exe')
|
||||||
|
|
@ -255,10 +255,10 @@ export function newWindow(
|
||||||
});
|
});
|
||||||
// pass on the full screen events from the window to react
|
// pass on the full screen events from the window to react
|
||||||
rpc.win.on('enter-full-screen', () => {
|
rpc.win.on('enter-full-screen', () => {
|
||||||
rpc.emit('enter full screen', {});
|
rpc.emit('enter full screen');
|
||||||
});
|
});
|
||||||
rpc.win.on('leave-full-screen', () => {
|
rpc.win.on('leave-full-screen', () => {
|
||||||
rpc.emit('leave full screen', {});
|
rpc.emit('leave full screen');
|
||||||
});
|
});
|
||||||
const deleteSessions = () => {
|
const deleteSessions = () => {
|
||||||
sessions.forEach((session, key) => {
|
sessions.forEach((session, key) => {
|
||||||
|
|
@ -280,9 +280,9 @@ export function newWindow(
|
||||||
const protocol = typeof url === 'string' && new URL(url).protocol;
|
const protocol = typeof url === 'string' && new URL(url).protocol;
|
||||||
if (protocol === 'file:') {
|
if (protocol === 'file:') {
|
||||||
const path = fileURLToPath(url);
|
const path = fileURLToPath(url);
|
||||||
return {data: path, escaped: true};
|
return {uid: null, data: path, escaped: true};
|
||||||
} else if (protocol === 'http:' || protocol === 'https:') {
|
} else if (protocol === 'http:' || protocol === 'https:') {
|
||||||
return {data: url};
|
return {uid: null, data: url};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
96
common.d.ts
vendored
Normal file
96
common.d.ts
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
import type parseUrl from 'parse-url';
|
||||||
|
|
||||||
|
export type Session = {
|
||||||
|
uid: string;
|
||||||
|
rows: number | null;
|
||||||
|
cols: number | null;
|
||||||
|
splitDirection?: 'HORIZONTAL' | 'VERTICAL';
|
||||||
|
shell: string | null;
|
||||||
|
pid: number | null;
|
||||||
|
activeUid?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type sessionExtraOptions = {
|
||||||
|
cwd: string | undefined;
|
||||||
|
splitDirection?: 'HORIZONTAL' | 'VERTICAL';
|
||||||
|
activeUid?: string | null;
|
||||||
|
isNewGroup?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MainEvents = {
|
||||||
|
close: never;
|
||||||
|
command: string;
|
||||||
|
data: {uid: string | null; data: string; escaped?: boolean | null};
|
||||||
|
exit: {uid: string};
|
||||||
|
'info renderer': {uid: string; type: string};
|
||||||
|
init: null;
|
||||||
|
maximize: never;
|
||||||
|
minimize: never;
|
||||||
|
new: sessionExtraOptions;
|
||||||
|
'open context menu': string;
|
||||||
|
'open external': {url: string};
|
||||||
|
'open hamburger menu': {x: number; y: number};
|
||||||
|
'quit and install': never;
|
||||||
|
resize: {uid: string; cols: number; rows: number};
|
||||||
|
unmaximize: never;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RendererEvents = {
|
||||||
|
ready: never;
|
||||||
|
'add notification': {text: string; url: string; dismissable: boolean};
|
||||||
|
'update available': {releaseNotes: string; releaseName: string; releaseUrl: string; canInstall: boolean};
|
||||||
|
'open ssh': ReturnType<typeof parseUrl>;
|
||||||
|
'open file': {path: string};
|
||||||
|
'move jump req': number | 'last';
|
||||||
|
'reset fontSize req': never;
|
||||||
|
'move left req': never;
|
||||||
|
'move right req': never;
|
||||||
|
'prev pane req': never;
|
||||||
|
'decrease fontSize req': never;
|
||||||
|
'increase fontSize req': never;
|
||||||
|
'next pane req': never;
|
||||||
|
'session break req': never;
|
||||||
|
'session quit req': never;
|
||||||
|
'session search close': never;
|
||||||
|
'session search': never;
|
||||||
|
'session stop req': never;
|
||||||
|
'session tmux req': never;
|
||||||
|
'session del line beginning req': never;
|
||||||
|
'session del line end req': never;
|
||||||
|
'session del word left req': never;
|
||||||
|
'session del word right req': never;
|
||||||
|
'session move line beginning req': never;
|
||||||
|
'session move line end req': never;
|
||||||
|
'session move word left req': never;
|
||||||
|
'session move word right req': never;
|
||||||
|
'term selectAll': never;
|
||||||
|
reload: never;
|
||||||
|
'session clear req': never;
|
||||||
|
'split request horizontal': {activeUid?: string};
|
||||||
|
'split request vertical': {activeUid?: string};
|
||||||
|
'termgroup add req': {activeUid?: string};
|
||||||
|
'termgroup close req': never;
|
||||||
|
'session add': Session;
|
||||||
|
'session data': string;
|
||||||
|
'session exit': {uid: string};
|
||||||
|
'windowGeometry change': {isMaximized: boolean};
|
||||||
|
move: {bounds: {x: number; y: number}};
|
||||||
|
'enter full screen': never;
|
||||||
|
'leave full screen': never;
|
||||||
|
'session data send': {uid: string | null; data: string; escaped?: boolean};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get keys of T where the value is not never
|
||||||
|
*/
|
||||||
|
export type FilterNever<T> = {[K in keyof T]: T[K] extends never ? never : K}[keyof T];
|
||||||
|
|
||||||
|
export interface TypedEmitter<Events> {
|
||||||
|
on<E extends keyof Events>(event: E, listener: (args: Events[E]) => void): this;
|
||||||
|
once<E extends keyof Events>(event: E, listener: (args: Events[E]) => void): this;
|
||||||
|
emit<E extends Exclude<keyof Events, FilterNever<Events>>>(event: E): boolean;
|
||||||
|
emit<E extends FilterNever<Events>>(event: E, data: Events[E]): boolean;
|
||||||
|
emit<E extends keyof Events>(event: E, data?: Events[E]): boolean;
|
||||||
|
removeListener<E extends keyof Events>(event: E, listener: (args: Events[E]) => void): this;
|
||||||
|
removeAllListeners<E extends keyof Events>(event?: E): this;
|
||||||
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ export function maximize() {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UI_WINDOW_MAXIMIZE,
|
type: UI_WINDOW_MAXIMIZE,
|
||||||
effect() {
|
effect() {
|
||||||
rpc.emit('maximize', null);
|
rpc.emit('maximize');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -50,7 +50,7 @@ export function unmaximize() {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UI_WINDOW_UNMAXIMIZE,
|
type: UI_WINDOW_UNMAXIMIZE,
|
||||||
effect() {
|
effect() {
|
||||||
rpc.emit('unmaximize', null);
|
rpc.emit('unmaximize');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -72,7 +72,7 @@ export function minimize() {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UI_WINDOW_MINIMIZE,
|
type: UI_WINDOW_MINIMIZE,
|
||||||
effect() {
|
effect() {
|
||||||
rpc.emit('minimize', null);
|
rpc.emit('minimize');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
@ -83,7 +83,7 @@ export function close() {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: UI_WINDOW_CLOSE,
|
type: UI_WINDOW_CLOSE,
|
||||||
effect() {
|
effect() {
|
||||||
rpc.emit('close', null);
|
rpc.emit('close');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,15 @@ import {
|
||||||
} from '../constants/sessions';
|
} from '../constants/sessions';
|
||||||
import {HyperState, session, HyperDispatch, HyperActions} from '../hyper';
|
import {HyperState, session, HyperDispatch, HyperActions} from '../hyper';
|
||||||
|
|
||||||
export function addSession({uid, shell, pid, cols, rows, splitDirection, activeUid}: session) {
|
export function addSession({
|
||||||
|
uid,
|
||||||
|
shell,
|
||||||
|
pid,
|
||||||
|
cols,
|
||||||
|
rows,
|
||||||
|
splitDirection,
|
||||||
|
activeUid
|
||||||
|
}: Pick<session, 'uid' | 'shell' | 'pid' | 'cols' | 'rows' | 'splitDirection' | 'activeUid'>) {
|
||||||
return (dispatch: HyperDispatch, getState: () => HyperState) => {
|
return (dispatch: HyperDispatch, getState: () => HyperState) => {
|
||||||
const {sessions} = getState();
|
const {sessions} = getState();
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {setActiveSession, ptyExitSession, userExitSession} from './sessions';
|
||||||
import {ITermState, ITermGroup, HyperState, HyperDispatch, HyperActions} from '../hyper';
|
import {ITermState, ITermGroup, HyperState, HyperDispatch, HyperActions} from '../hyper';
|
||||||
|
|
||||||
function requestSplit(direction: 'VERTICAL' | 'HORIZONTAL') {
|
function requestSplit(direction: 'VERTICAL' | 'HORIZONTAL') {
|
||||||
return (activeUid: string) =>
|
return (activeUid: string | undefined) =>
|
||||||
(dispatch: HyperDispatch, getState: () => HyperState): void => {
|
(dispatch: HyperDispatch, getState: () => HyperState): void => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SESSION_REQUEST,
|
type: SESSION_REQUEST,
|
||||||
|
|
@ -40,7 +40,7 @@ export function resizeTermGroup(uid: string, sizes: number[]): HyperActions {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function requestTermGroup(activeUid: string) {
|
export function requestTermGroup(activeUid: string | undefined) {
|
||||||
return (dispatch: HyperDispatch, getState: () => HyperState) => {
|
return (dispatch: HyperDispatch, getState: () => HyperState) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: TERM_GROUP_REQUEST,
|
type: TERM_GROUP_REQUEST,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ export function installUpdate(): HyperActions {
|
||||||
return {
|
return {
|
||||||
type: UPDATE_INSTALL,
|
type: UPDATE_INSTALL,
|
||||||
effect: () => {
|
effect: () => {
|
||||||
rpc.emit('quit and install', null);
|
rpc.emit('quit and install');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
import {EventEmitter} from 'events';
|
import {EventEmitter} from 'events';
|
||||||
import {IpcRenderer, IpcRendererEvent} from 'electron';
|
import {IpcRenderer, IpcRendererEvent} from 'electron';
|
||||||
import electron from 'electron';
|
import electron from 'electron';
|
||||||
|
import {FilterNever, MainEvents, RendererEvents, TypedEmitter} from '../../common';
|
||||||
|
|
||||||
export default class Client {
|
export default class Client {
|
||||||
emitter: EventEmitter;
|
emitter: TypedEmitter<RendererEvents>;
|
||||||
ipc: IpcRenderer;
|
ipc: IpcRenderer;
|
||||||
id!: string;
|
id!: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.emitter = new EventEmitter();
|
this.emitter = new EventEmitter();
|
||||||
this.ipc = electron.ipcRenderer;
|
this.ipc = electron.ipcRenderer;
|
||||||
|
this.emit = this.emit.bind(this);
|
||||||
if (window.__rpcId) {
|
if (window.__rpcId) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.id = window.__rpcId;
|
this.id = window.__rpcId;
|
||||||
|
|
@ -27,35 +31,43 @@ export default class Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcListener = (event: any, {ch, data}: {ch: string; data: any}) => {
|
ipcListener = <U extends keyof RendererEvents>(
|
||||||
this.emitter.emit(ch, data);
|
event: IpcRendererEvent,
|
||||||
|
{ch, data}: {ch: U; data: RendererEvents[U]}
|
||||||
|
) => this.emitter.emit(ch, data);
|
||||||
|
|
||||||
|
on = <U extends keyof RendererEvents>(ev: U, fn: (arg0: RendererEvents[U]) => void) => {
|
||||||
|
this.emitter.on(ev, fn);
|
||||||
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
on(ev: string, fn: (...args: any[]) => void) {
|
once = <U extends keyof RendererEvents>(ev: U, fn: (arg0: RendererEvents[U]) => void) => {
|
||||||
this.emitter.on(ev, fn);
|
|
||||||
}
|
|
||||||
|
|
||||||
once(ev: string, fn: (...args: any[]) => void) {
|
|
||||||
this.emitter.once(ev, fn);
|
this.emitter.once(ev, fn);
|
||||||
}
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
emit(ev: string, data: any) {
|
emit<U extends Exclude<keyof MainEvents, FilterNever<MainEvents>>>(ch: U): boolean;
|
||||||
|
emit<U extends FilterNever<MainEvents>>(ch: U, data: MainEvents[U]): boolean;
|
||||||
|
emit<U extends keyof MainEvents>(ev: U, data?: MainEvents[U]) {
|
||||||
if (!this.id) {
|
if (!this.id) {
|
||||||
throw new Error('Not ready');
|
throw new Error('Not ready');
|
||||||
}
|
}
|
||||||
this.ipc.send(this.id, {ev, data});
|
this.ipc.send(this.id, {ev, data});
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeListener(ev: string, fn: (...args: any[]) => void) {
|
removeListener = <U extends keyof RendererEvents>(ev: U, fn: (arg0: RendererEvents[U]) => void) => {
|
||||||
this.emitter.removeListener(ev, fn);
|
this.emitter.removeListener(ev, fn);
|
||||||
}
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
removeAllListeners() {
|
removeAllListeners = () => {
|
||||||
this.emitter.removeAllListeners();
|
this.emitter.removeAllListeners();
|
||||||
}
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
destroy() {
|
destroy = () => {
|
||||||
this.removeAllListeners();
|
this.removeAllListeners();
|
||||||
this.ipc.removeAllListeners(this.id);
|
this.ipc.removeAllListeners(this.id);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue