2019-12-18 07:28:28 -09:00
|
|
|
import {app, BrowserWindow, shell, Menu, BrowserWindowConstructorOptions} from 'electron';
|
2020-07-09 13:45:12 -08:00
|
|
|
import {isAbsolute, normalize, sep} from 'path';
|
2019-11-28 05:17:01 -09:00
|
|
|
import {parse as parseUrl} from 'url';
|
2020-03-02 01:25:27 -09:00
|
|
|
import {v4 as uuidv4} from 'uuid';
|
2019-11-28 05:17:01 -09:00
|
|
|
import fileUriToPath from 'file-uri-to-path';
|
|
|
|
|
import isDev from 'electron-is-dev';
|
|
|
|
|
import updater from '../updater';
|
|
|
|
|
import toElectronBackgroundColor from '../utils/to-electron-background-color';
|
2020-01-25 09:34:41 -09:00
|
|
|
import {icon, homeDirectory} from '../config/paths';
|
2019-11-28 05:17:01 -09:00
|
|
|
import createRPC from '../rpc';
|
|
|
|
|
import notify from '../notify';
|
|
|
|
|
import fetchNotifications from '../notifications';
|
|
|
|
|
import Session from '../session';
|
|
|
|
|
import contextMenuTemplate from './contextmenu';
|
|
|
|
|
import {execCommand} from '../commands';
|
|
|
|
|
import {setRendererType, unsetRendererType} from '../utils/renderer-utils';
|
|
|
|
|
import {decorateSessionOptions, decorateSessionClass} from '../plugins';
|
|
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
export function newWindow(
|
|
|
|
|
options_: BrowserWindowConstructorOptions,
|
|
|
|
|
cfg: any,
|
|
|
|
|
fn?: (win: BrowserWindow) => void
|
|
|
|
|
): BrowserWindow {
|
2020-03-02 01:25:27 -09:00
|
|
|
const classOpts = Object.assign({uid: uuidv4()});
|
2019-12-18 07:28:28 -09:00
|
|
|
app.plugins.decorateWindowClass(classOpts);
|
|
|
|
|
|
2020-07-14 21:31:58 -08:00
|
|
|
const winOpts: BrowserWindowConstructorOptions = {
|
|
|
|
|
minWidth: 370,
|
|
|
|
|
minHeight: 190,
|
|
|
|
|
backgroundColor: toElectronBackgroundColor(cfg.backgroundColor || '#000'),
|
|
|
|
|
titleBarStyle: 'hiddenInset',
|
|
|
|
|
title: 'Hyper.app',
|
|
|
|
|
// we want to go frameless on Windows and Linux
|
|
|
|
|
frame: process.platform === 'darwin',
|
|
|
|
|
transparent: process.platform === 'darwin',
|
|
|
|
|
icon,
|
|
|
|
|
show: Boolean(process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev),
|
|
|
|
|
acceptFirstMouse: true,
|
|
|
|
|
webPreferences: {
|
|
|
|
|
nodeIntegration: true,
|
|
|
|
|
navigateOnDragDrop: true,
|
2021-03-03 18:06:06 -09:00
|
|
|
enableRemoteModule: true,
|
|
|
|
|
contextIsolation: false
|
2019-12-18 07:28:28 -09:00
|
|
|
},
|
2020-07-14 21:31:58 -08:00
|
|
|
...options_
|
|
|
|
|
};
|
2019-12-18 07:28:28 -09:00
|
|
|
const window = new BrowserWindow(app.plugins.getDecoratedBrowserOptions(winOpts));
|
|
|
|
|
window.uid = classOpts.uid;
|
|
|
|
|
|
|
|
|
|
app.plugins.onWindowClass(window);
|
|
|
|
|
window.uid = classOpts.uid;
|
|
|
|
|
|
|
|
|
|
const rpc = createRPC(window);
|
|
|
|
|
const sessions = new Map();
|
|
|
|
|
|
|
|
|
|
const updateBackgroundColor = () => {
|
|
|
|
|
const cfg_ = app.plugins.getDecoratedConfig();
|
|
|
|
|
window.setBackgroundColor(toElectronBackgroundColor(cfg_.backgroundColor || '#000'));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// set working directory
|
2020-07-09 13:45:12 -08:00
|
|
|
let argPath = process.argv[1];
|
|
|
|
|
if (argPath && process.platform === 'win32') {
|
|
|
|
|
if (/[a-zA-Z]:"/.test(argPath)) {
|
|
|
|
|
argPath = argPath.replace('"', sep);
|
|
|
|
|
}
|
|
|
|
|
argPath = normalize(argPath + sep);
|
|
|
|
|
}
|
2020-01-25 09:34:41 -09:00
|
|
|
let workingDirectory = homeDirectory;
|
2020-07-09 13:45:12 -08:00
|
|
|
if (argPath && isAbsolute(argPath)) {
|
|
|
|
|
workingDirectory = argPath;
|
2019-12-18 07:28:28 -09:00
|
|
|
} else if (cfg.workingDirectory && isAbsolute(cfg.workingDirectory)) {
|
|
|
|
|
workingDirectory = cfg.workingDirectory;
|
|
|
|
|
}
|
2019-03-02 13:09:15 -09:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
// config changes
|
|
|
|
|
const cfgUnsubscribe = app.config.subscribe(() => {
|
|
|
|
|
const cfg_ = app.plugins.getDecoratedConfig();
|
2017-08-02 11:20:03 -08:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
// notify renderer
|
|
|
|
|
window.webContents.send('config change');
|
2017-11-29 04:22:29 -09:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
// notify user that shell changes require new sessions
|
|
|
|
|
if (cfg_.shell !== cfg.shell || JSON.stringify(cfg_.shellArgs) !== JSON.stringify(cfg.shellArgs)) {
|
|
|
|
|
notify('Shell configuration changed!', 'Open a new tab or window to start using the new shell');
|
2019-10-04 12:27:58 -08:00
|
|
|
}
|
|
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
// update background color if necessary
|
|
|
|
|
updateBackgroundColor();
|
2017-08-02 11:20:03 -08:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
cfg = cfg_;
|
|
|
|
|
});
|
2017-08-02 11:20:03 -08:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
rpc.on('init', () => {
|
|
|
|
|
window.show();
|
|
|
|
|
updateBackgroundColor();
|
2017-08-02 11:20:03 -08:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
// If no callback is passed to createWindow,
|
|
|
|
|
// a new session will be created by default.
|
|
|
|
|
if (!fn) {
|
|
|
|
|
fn = (win: BrowserWindow) => win.rpc.emit('termgroup add req', {});
|
|
|
|
|
}
|
2017-11-29 04:22:29 -09:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
// app.windowCallback is the createWindow callback
|
|
|
|
|
// that can be set before the 'ready' app event
|
|
|
|
|
// and createWindow definition. It's executed in place of
|
|
|
|
|
// the callback passed as parameter, and deleted right after.
|
|
|
|
|
(app.windowCallback || fn)(window);
|
2020-09-15 06:54:35 -08:00
|
|
|
app.windowCallback = undefined;
|
2019-12-18 07:28:28 -09:00
|
|
|
fetchNotifications(window);
|
|
|
|
|
// auto updates
|
|
|
|
|
if (!isDev) {
|
|
|
|
|
updater(window);
|
|
|
|
|
} else {
|
|
|
|
|
console.log('ignoring auto updates during dev');
|
|
|
|
|
}
|
|
|
|
|
});
|
2017-08-02 11:20:03 -08:00
|
|
|
|
2020-01-02 05:44:11 -09:00
|
|
|
function createSession(extraOptions: any = {}) {
|
2020-03-02 01:25:27 -09:00
|
|
|
const uid = uuidv4();
|
2020-03-04 04:07:52 -09:00
|
|
|
const extraOptionsFiltered: any = {};
|
2020-03-25 02:15:08 -08:00
|
|
|
Object.keys(extraOptions).forEach((key) => {
|
2020-03-04 04:07:52 -09:00
|
|
|
if (extraOptions[key] !== undefined) extraOptionsFiltered[key] = extraOptions[key];
|
|
|
|
|
});
|
2017-08-02 11:20:03 -08:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
// remove the rows and cols, the wrong value of them will break layout when init create
|
|
|
|
|
const defaultOptions = Object.assign(
|
|
|
|
|
{
|
|
|
|
|
cwd: workingDirectory,
|
|
|
|
|
splitDirection: undefined,
|
|
|
|
|
shell: cfg.shell,
|
|
|
|
|
shellArgs: cfg.shellArgs && Array.from(cfg.shellArgs)
|
|
|
|
|
},
|
2020-03-04 04:07:52 -09:00
|
|
|
extraOptionsFiltered,
|
2019-12-18 07:28:28 -09:00
|
|
|
{uid}
|
|
|
|
|
);
|
|
|
|
|
const options = decorateSessionOptions(defaultOptions);
|
|
|
|
|
const DecoratedSession = decorateSessionClass(Session);
|
|
|
|
|
const session = new DecoratedSession(options);
|
|
|
|
|
sessions.set(uid, session);
|
|
|
|
|
return {session, options};
|
|
|
|
|
}
|
2018-12-19 17:03:22 -09:00
|
|
|
|
2020-03-25 02:15:08 -08:00
|
|
|
rpc.on('new', (extraOptions) => {
|
2019-12-18 07:28:28 -09:00
|
|
|
const {session, options} = createSession(extraOptions);
|
|
|
|
|
|
|
|
|
|
sessions.set(options.uid, session);
|
|
|
|
|
rpc.emit('session add', {
|
|
|
|
|
rows: options.rows,
|
|
|
|
|
cols: options.cols,
|
|
|
|
|
uid: options.uid,
|
|
|
|
|
splitDirection: options.splitDirection,
|
|
|
|
|
shell: session.shell,
|
|
|
|
|
pid: session.pty ? session.pty.pid : null,
|
|
|
|
|
activeUid: options.activeUid
|
2018-12-23 08:37:32 -09:00
|
|
|
});
|
2018-12-19 17:03:22 -09:00
|
|
|
|
2020-06-19 04:51:34 -08:00
|
|
|
session.on('data', (data: string) => {
|
2019-12-18 07:28:28 -09:00
|
|
|
rpc.emit('session data', data);
|
2018-12-23 08:37:32 -09:00
|
|
|
});
|
|
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
session.on('exit', () => {
|
|
|
|
|
rpc.emit('session exit', {uid: options.uid});
|
|
|
|
|
unsetRendererType(options.uid);
|
|
|
|
|
sessions.delete(options.uid);
|
2019-01-24 14:46:39 -09:00
|
|
|
});
|
2019-12-18 07:28:28 -09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
rpc.on('exit', ({uid}) => {
|
|
|
|
|
const session = sessions.get(uid);
|
|
|
|
|
if (session) {
|
|
|
|
|
session.exit();
|
2018-12-23 08:37:32 -09:00
|
|
|
}
|
2019-12-18 07:28:28 -09:00
|
|
|
});
|
|
|
|
|
rpc.on('unmaximize', () => {
|
|
|
|
|
window.unmaximize();
|
|
|
|
|
});
|
|
|
|
|
rpc.on('maximize', () => {
|
|
|
|
|
window.maximize();
|
|
|
|
|
});
|
|
|
|
|
rpc.on('minimize', () => {
|
|
|
|
|
window.minimize();
|
|
|
|
|
});
|
|
|
|
|
rpc.on('resize', ({uid, cols, rows}) => {
|
|
|
|
|
const session = sessions.get(uid);
|
|
|
|
|
if (session) {
|
|
|
|
|
session.resize({cols, rows});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
rpc.on('data', ({uid, data, escaped}) => {
|
|
|
|
|
const session = sessions.get(uid);
|
|
|
|
|
if (session) {
|
|
|
|
|
if (escaped) {
|
|
|
|
|
const escapedData = session.shell.endsWith('cmd.exe')
|
|
|
|
|
? `"${data}"` // This is how cmd.exe does it
|
|
|
|
|
: `'${data.replace(/'/g, `'\\''`)}'`; // Inside a single-quoted string nothing is interpreted
|
|
|
|
|
|
|
|
|
|
session.write(escapedData);
|
|
|
|
|
} else {
|
|
|
|
|
session.write(data);
|
2018-12-23 08:37:32 -09:00
|
|
|
}
|
2019-12-18 07:28:28 -09:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
rpc.on('info renderer', ({uid, type}) => {
|
|
|
|
|
// Used in the "About" dialog
|
|
|
|
|
setRendererType(uid, type);
|
|
|
|
|
});
|
|
|
|
|
rpc.on('open external', ({url}) => {
|
2021-03-28 08:42:20 -08:00
|
|
|
void shell.openExternal(url);
|
2019-12-18 07:28:28 -09:00
|
|
|
});
|
2020-03-25 02:15:08 -08:00
|
|
|
rpc.on('open context menu', (selection) => {
|
2019-12-18 07:28:28 -09:00
|
|
|
const {createWindow} = app;
|
|
|
|
|
const {buildFromTemplate} = Menu;
|
|
|
|
|
buildFromTemplate(contextMenuTemplate(createWindow, selection)).popup({window});
|
|
|
|
|
});
|
|
|
|
|
rpc.on('open hamburger menu', ({x, y}) => {
|
|
|
|
|
Menu.getApplicationMenu()!.popup({x: Math.ceil(x), y: Math.ceil(y)});
|
|
|
|
|
});
|
|
|
|
|
// Same deal as above, grabbing the window titlebar when the window
|
|
|
|
|
// is maximized on Windows results in unmaximize, without hitting any
|
|
|
|
|
// app buttons
|
|
|
|
|
for (const ev of ['maximize', 'unmaximize', 'minimize', 'restore'] as any) {
|
|
|
|
|
window.on(ev, () => rpc.emit('windowGeometry change', {}));
|
|
|
|
|
}
|
|
|
|
|
window.on('move', () => {
|
|
|
|
|
const position = window.getPosition();
|
|
|
|
|
rpc.emit('move', {bounds: {x: position[0], y: position[1]}});
|
|
|
|
|
});
|
|
|
|
|
rpc.on('close', () => {
|
|
|
|
|
window.close();
|
|
|
|
|
});
|
2020-03-25 02:15:08 -08:00
|
|
|
rpc.on('command', (command) => {
|
2019-12-18 07:28:28 -09:00
|
|
|
const focusedWindow = BrowserWindow.getFocusedWindow();
|
|
|
|
|
execCommand(command, focusedWindow!);
|
|
|
|
|
});
|
|
|
|
|
// pass on the full screen events from the window to react
|
|
|
|
|
rpc.win.on('enter-full-screen', () => {
|
|
|
|
|
rpc.emit('enter full screen', {});
|
|
|
|
|
});
|
|
|
|
|
rpc.win.on('leave-full-screen', () => {
|
|
|
|
|
rpc.emit('leave full screen', {});
|
|
|
|
|
});
|
|
|
|
|
const deleteSessions = () => {
|
|
|
|
|
sessions.forEach((session, key) => {
|
|
|
|
|
session.removeAllListeners();
|
|
|
|
|
session.destroy();
|
|
|
|
|
sessions.delete(key);
|
2018-12-23 08:37:32 -09:00
|
|
|
});
|
2019-12-18 07:28:28 -09:00
|
|
|
};
|
|
|
|
|
// we reset the rpc channel only upon
|
|
|
|
|
// subsequent refreshes (ie: F5)
|
|
|
|
|
let i = 0;
|
|
|
|
|
window.webContents.on('did-navigate', () => {
|
|
|
|
|
if (i++) {
|
|
|
|
|
deleteSessions();
|
|
|
|
|
}
|
|
|
|
|
});
|
2017-08-02 11:20:03 -08:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
// If file is dropped onto the terminal window, navigate event is prevented
|
|
|
|
|
// and his path is added to active session.
|
|
|
|
|
window.webContents.on('will-navigate', (event, url) => {
|
|
|
|
|
const protocol = typeof url === 'string' && parseUrl(url).protocol;
|
|
|
|
|
if (protocol === 'file:') {
|
|
|
|
|
event.preventDefault();
|
2017-08-02 11:20:03 -08:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
const path = fileUriToPath(url);
|
2017-09-13 13:12:30 -08:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
rpc.emit('session data send', {data: path, escaped: true});
|
|
|
|
|
} else if (protocol === 'http:' || protocol === 'https:') {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
rpc.emit('session data send', {data: url});
|
|
|
|
|
}
|
|
|
|
|
});
|
2017-08-02 11:20:03 -08:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
// xterm makes link clickable
|
|
|
|
|
window.webContents.on('new-window', (event, url) => {
|
|
|
|
|
const protocol = typeof url === 'string' && parseUrl(url).protocol;
|
|
|
|
|
if (protocol === 'http:' || protocol === 'https:') {
|
|
|
|
|
event.preventDefault();
|
2021-03-28 08:42:20 -08:00
|
|
|
void shell.openExternal(url);
|
2019-12-18 07:28:28 -09:00
|
|
|
}
|
|
|
|
|
});
|
2017-08-02 11:20:03 -08:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
// expose internals to extension authors
|
|
|
|
|
window.rpc = rpc;
|
|
|
|
|
window.sessions = sessions;
|
2017-08-02 11:20:03 -08:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
const load = () => {
|
|
|
|
|
app.plugins.onWindow(window);
|
|
|
|
|
};
|
2017-08-02 11:20:03 -08:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
// load plugins
|
|
|
|
|
load();
|
2017-08-02 11:20:03 -08:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
const pluginsUnsubscribe = app.plugins.subscribe((err: any) => {
|
|
|
|
|
if (!err) {
|
|
|
|
|
load();
|
|
|
|
|
window.webContents.send('plugins change');
|
|
|
|
|
updateBackgroundColor();
|
|
|
|
|
}
|
|
|
|
|
});
|
2017-08-02 11:20:03 -08:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
// Keep track of focus time of every window, to figure out
|
|
|
|
|
// which one of the existing window is the last focused.
|
|
|
|
|
// Works nicely even if a window is closed and removed.
|
|
|
|
|
const updateFocusTime = () => {
|
|
|
|
|
window.focusTime = process.uptime();
|
|
|
|
|
};
|
2018-12-16 13:46:37 -09:00
|
|
|
|
2019-12-18 07:28:28 -09:00
|
|
|
window.on('focus', () => {
|
2018-12-23 08:37:32 -09:00
|
|
|
updateFocusTime();
|
2019-12-18 07:28:28 -09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// the window can be closed by the browser process itself
|
|
|
|
|
window.clean = () => {
|
|
|
|
|
app.config.winRecord(window);
|
|
|
|
|
rpc.destroy();
|
|
|
|
|
deleteSessions();
|
|
|
|
|
cfgUnsubscribe();
|
|
|
|
|
pluginsUnsubscribe();
|
|
|
|
|
};
|
|
|
|
|
// Ensure focusTime is set on window open. The focus event doesn't
|
|
|
|
|
// fire from the dock (see bug #583)
|
|
|
|
|
updateFocusTime();
|
|
|
|
|
|
|
|
|
|
return window;
|
2019-11-28 05:17:01 -09:00
|
|
|
}
|