hyper/app/ui/window.ts

327 lines
9.9 KiB
TypeScript
Raw Normal View History

import {app, BrowserWindow, shell, Menu, BrowserWindowConstructorOptions, Event} from 'electron';
import {isAbsolute, normalize, sep} from 'path';
2021-07-20 23:43:01 -08:00
import {URL, fileURLToPath} from 'url';
import {v4 as uuidv4} from 'uuid';
2019-11-28 05:17:01 -09:00
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';
import {enable as remoteEnable} from '@electron/remote/main';
import {configOptions} from '../../lib/config';
2019-11-28 05:17:01 -09:00
export function newWindow(
options_: BrowserWindowConstructorOptions,
cfg: configOptions,
fn?: (win: BrowserWindow) => void
): BrowserWindow {
const classOpts = Object.assign({uid: uuidv4()});
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
contextIsolation: false
},
2020-07-14 21:31:58 -08:00
...options_
};
const window = new BrowserWindow(app.plugins.getDecoratedBrowserOptions(winOpts));
// Enable remote module on this window
remoteEnable(window.webContents);
window.uid = classOpts.uid;
app.plugins.onWindowClass(window);
window.uid = classOpts.uid;
const rpc = createRPC(window);
const sessions = new Map<string, Session>();
const updateBackgroundColor = () => {
const cfg_ = app.plugins.getDecoratedConfig();
window.setBackgroundColor(toElectronBackgroundColor(cfg_.backgroundColor || '#000'));
};
// set working directory
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;
if (argPath && isAbsolute(argPath)) {
workingDirectory = argPath;
} else if (cfg.workingDirectory && isAbsolute(cfg.workingDirectory)) {
workingDirectory = cfg.workingDirectory;
}
// config changes
const cfgUnsubscribe = app.config.subscribe(() => {
const cfg_ = app.plugins.getDecoratedConfig();
// notify renderer
window.webContents.send('config change');
// 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');
}
// update background color if necessary
updateBackgroundColor();
cfg = cfg_;
});
rpc.on('init', () => {
window.show();
updateBackgroundColor();
// 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', {});
};
}
// 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;
fetchNotifications(window);
// auto updates
if (!isDev) {
updater(window);
} else {
console.log('ignoring auto updates during dev');
}
});
2020-01-02 05:44:11 -09:00
function createSession(extraOptions: any = {}) {
const uid = uuidv4();
const extraOptionsFiltered: any = {};
2020-03-25 02:15:08 -08:00
Object.keys(extraOptions).forEach((key) => {
if (extraOptions[key] !== undefined) extraOptionsFiltered[key] = extraOptions[key];
});
// 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)
},
extraOptionsFiltered,
{uid}
);
const options = decorateSessionOptions(defaultOptions);
const DecoratedSession = decorateSessionClass(Session);
const session = new DecoratedSession(options);
sessions.set(uid, session);
return {session, options};
}
2020-03-25 02:15:08 -08:00
rpc.on('new', (extraOptions) => {
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
});
2020-06-19 04:51:34 -08:00
session.on('data', (data: string) => {
rpc.emit('session data', data);
});
session.on('exit', () => {
rpc.emit('session exit', {uid: options.uid});
unsetRendererType(options.uid);
sessions.delete(options.uid);
});
});
rpc.on('exit', ({uid}) => {
const session = sessions.get(uid);
if (session) {
session.exit();
}
});
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}: {uid: string; data: string; escaped: boolean}) => {
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);
}
}
});
rpc.on('info renderer', ({uid, type}) => {
// Used in the "About" dialog
setRendererType(uid, type);
});
rpc.on('open external', ({url}) => {
void shell.openExternal(url);
});
2020-03-25 02:15:08 -08:00
rpc.on('open context menu', (selection) => {
const {createWindow} = app;
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', {isMaximized: window.isMaximized()});
});
}
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) => {
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);
});
};
// we reset the rpc channel only upon
// subsequent refreshes (ie: F5)
let i = 0;
window.webContents.on('did-navigate', () => {
if (i++) {
deleteSessions();
}
});
2021-07-20 23:43:01 -08:00
const handleDrop = (event: Event, url: string) => {
const protocol = typeof url === 'string' && new URL(url).protocol;
if (protocol === 'file:') {
event.preventDefault();
2021-07-20 23:43:01 -08:00
const path = fileURLToPath(url);
rpc.emit('session data send', {data: path, escaped: true});
} else if (protocol === 'http:' || protocol === 'https:') {
event.preventDefault();
rpc.emit('session data send', {data: url});
}
2021-07-20 23:43:01 -08:00
};
2021-07-20 23:43:01 -08:00
// If file is dropped onto the terminal window, navigate and new-window events are prevented
// and his path is added to active session.
window.webContents.on('will-navigate', handleDrop);
window.webContents.on('new-window', handleDrop);
// expose internals to extension authors
window.rpc = rpc;
window.sessions = sessions;
const load = () => {
app.plugins.onWindow(window);
};
// load plugins
load();
const pluginsUnsubscribe = app.plugins.subscribe((err: any) => {
if (!err) {
load();
window.webContents.send('plugins change');
updateBackgroundColor();
}
});
// 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();
};
window.on('focus', () => {
updateFocusTime();
});
// 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
}