Update window creation (#2028)

* add ui/window

* move window and optimize code

* cleanup
This commit is contained in:
Philippe Potvin 2017-08-02 15:20:03 -04:00 committed by GitHub
parent 8a23b18f2d
commit 4fd115bf27
2 changed files with 251 additions and 255 deletions

View file

@ -35,24 +35,14 @@ if (process.platform === 'win32') {
}
// Native
const {resolve, isAbsolute} = require('path');
const {homedir} = require('os');
const {resolve} = require('path');
// Packages
const {parse: parseUrl} = require('url');
const {app, BrowserWindow, shell, Menu} = require('electron');
const {app, BrowserWindow, Menu} = require('electron');
const {gitDescribe} = require('git-describe');
const uuid = require('uuid');
const fileUriToPath = require('file-uri-to-path');
const isDev = require('electron-is-dev');
// Ours
const AutoUpdater = require('./auto-updater');
const toElectronBackgroundColor = require('./utils/to-electron-background-color');
const AppMenu = require('./menus/menu');
const createRPC = require('./rpc');
const notify = require('./notify');
const fetchNotifications = require('./notifications');
const config = require('./config');
app.commandLine.appendSwitch('js-flags', '--harmony-async-await');
@ -61,7 +51,8 @@ app.commandLine.appendSwitch('js-flags', '--harmony-async-await');
config.setup();
const plugins = require('./plugins');
const Session = require('./session');
const Window = require('./ui/window');
const windowSet = new Set([]);
@ -103,7 +94,7 @@ console.log('electron will open', url);
app.on('ready', () => installDevExtensions(isDev).then(() => {
function createWindow(fn, options = {}) {
let cfg = plugins.getDecoratedConfig();
const cfg = plugins.getDecoratedConfig();
const winSet = config.getWin();
let [startX, startY] = winSet.position;
@ -139,247 +130,17 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
}
}
const browserDefaults = {
width,
height,
minHeight: 190,
minWidth: 370,
titleBarStyle: 'hidden-inset', // macOS only
title: 'Hyper.app',
backgroundColor: toElectronBackgroundColor(cfg.backgroundColor || '#000'),
// we want to go frameless on windows and linux
frame: process.platform === 'darwin',
transparent: process.platform === 'darwin',
icon: resolve(__dirname, 'static/icon.png'),
// we only want to show when the prompt is ready for user input
// HYPERTERM_DEBUG for backwards compatibility with hyperterm
show: process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev,
x: startX,
y: startY,
acceptFirstMouse: true
};
const browserOptions = plugins.getDecoratedBrowserOptions(browserDefaults);
const win = new BrowserWindow(browserOptions);
windowSet.add(win);
win.loadURL(url);
const rpc = createRPC(win);
const sessions = new Map();
// config changes
const cfgUnsubscribe = config.subscribe(() => {
const cfg_ = plugins.getDecoratedConfig();
// notify renderer
win.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
cfg = cfg_;
});
rpc.on('init', () => {
// we update the backgroundColor once the init is called.
// when we do a win.reload() we need need to reset the backgroundColor
win.setBackgroundColor(toElectronBackgroundColor(cfg.backgroundColor || '#000'));
win.show();
// If no callback is passed to createWindow,
// a new session will be created by default.
if (!fn) {
fn = win => win.rpc.emit('termgroup add req');
}
// app.windowCallback is the createWindow callback
// that can be set before the 'ready' app event
// and createWindow deifinition. It's executed in place of
// the callback passed as parameter, and deleted right after.
(app.windowCallback || fn)(win);
delete (app.windowCallback);
fetchNotifications(win);
// auto updates
if (!isDev && process.platform !== 'linux') {
AutoUpdater(win);
} else {
console.log('ignoring auto updates during dev');
}
});
rpc.on('new', ({rows = 40, cols = 100, cwd = process.argv[1] && isAbsolute(process.argv[1]) ? process.argv[1] : homedir(), splitDirection}) => {
const shell = cfg.shell;
const shellArgs = cfg.shellArgs && Array.from(cfg.shellArgs);
initSession({rows, cols, cwd, shell, shellArgs}, (uid, session) => {
sessions.set(uid, session);
rpc.emit('session add', {
rows,
cols,
uid,
splitDirection,
shell: session.shell,
pid: session.pty.pid
});
session.on('data', data => {
rpc.emit('session data', uid + data);
});
session.on('exit', () => {
rpc.emit('session exit', {uid});
sessions.delete(uid);
});
});
});
rpc.on('exit', ({uid}) => {
const session = sessions.get(uid);
if (session) {
session.exit();
} else {
console.log('session not found by', uid);
}
});
rpc.on('unmaximize', () => {
win.unmaximize();
});
rpc.on('maximize', () => {
win.maximize();
});
rpc.on('resize', ({uid, cols, rows}) => {
const session = sessions.get(uid);
session.resize({cols, rows});
});
rpc.on('data', ({uid, data, escaped}) => {
const session = sessions.get(uid);
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('open external', ({url}) => {
shell.openExternal(url);
});
rpc.win.on('move', () => {
rpc.emit('move');
});
rpc.on('open hamburger menu', ({x, y}) => {
Menu.getApplicationMenu().popup(Math.ceil(x), Math.ceil(y));
});
rpc.on('minimize', () => {
win.minimize();
});
rpc.on('close', () => {
win.close();
});
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;
win.webContents.on('did-navigate', () => {
if (i++) {
deleteSessions();
}
});
// If file is dropped onto the terminal window, navigate event is prevented
// and his path is added to active session.
win.webContents.on('will-navigate', (event, url) => {
const protocol = typeof url === 'string' && parseUrl(url).protocol;
if (protocol === 'file:') {
event.preventDefault();
const path = fileUriToPath(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});
}
});
// expose internals to extension authors
win.rpc = rpc;
win.sessions = sessions;
const load = () => {
plugins.onWindow(win);
};
// load plugins
load();
const pluginsUnsubscribe = plugins.subscribe(err => {
if (!err) {
load();
win.webContents.send('plugins change');
}
});
// 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 = () => {
win.focusTime = process.uptime();
};
win.on('focus', () => {
updateFocusTime();
});
// Ensure focusTime is set on window open. The focus event doesn't
// fire from the dock (see bug #583)
updateFocusTime();
const hwin = new Window({width, height, x: startX, y: startY}, cfg, fn);
windowSet.add(hwin);
hwin.loadURL(url);
// the window can be closed by the browser process itself
win.on('close', () => {
config.winRecord(win);
windowSet.delete(win);
rpc.destroy();
deleteSessions();
cfgUnsubscribe();
pluginsUnsubscribe();
hwin.on('close', () => {
hwin.clean();
windowSet.delete(this);
});
// 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']) {
win.on(ev, () => rpc.emit('windowGeometry change'));
}
win.on('closed', () => {
hwin.on('closed', () => {
if (process.platform !== 'darwin' && windowSet.size === 0) {
app.quit();
}
@ -437,10 +198,6 @@ app.on('ready', () => installDevExtensions(isDev).then(() => {
console.error('Error while loading devtools extensions', err);
}));
function initSession(opts, fn) {
fn(uuid.v4(), new Session(opts));
}
app.on('open-file', (event, path) => {
const lastWindow = app.getLastFocusedWindow();
const callback = win => win.rpc.emit('open file', {path});

239
app/ui/window.js Normal file
View file

@ -0,0 +1,239 @@
const {app, BrowserWindow, shell, Menu} = require('electron');
const {isAbsolute} = require('path');
const {parse: parseUrl} = require('url');
const uuid = require('uuid');
const fileUriToPath = require('file-uri-to-path');
const isDev = require('electron-is-dev');
const AutoUpdater = require('../auto-updater');
const toElectronBackgroundColor = require('../utils/to-electron-background-color');
const {icon, cfgDir} = require('../config/paths');
const createRPC = require('../rpc');
const notify = require('../notify');
const fetchNotifications = require('../notifications');
const Session = require('../session');
module.exports = class Window extends BrowserWindow {
constructor(options, cfg, fn) {
const opts = Object.assign({
minWidth: 370,
minHeight: 190,
backgroundColor: toElectronBackgroundColor(cfg.backgroundColor || '#000'),
titleBarStyle: 'hidden-inset',
title: 'Hyper.app',
// we want to go frameless on windows and linux
frame: process.platform === 'darwin',
transparent: process.platform === 'darwin',
icon,
show: process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev,
acceptFirstMouse: true
}, options);
const modOpts = app.plugins.getDecoratedBrowserOptions(opts);
super(modOpts);
const rpc = createRPC(this);
const sessions = new Map();
// config changes
const cfgUnsubscribe = app.config.subscribe(() => {
const cfg_ = app.plugins.getDecoratedConfig();
// notify renderer
this.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
cfg = cfg_;
});
rpc.on('init', () => {
this.setBackgroundColor(toElectronBackgroundColor(cfg.backgroundColor || '#000'));
this.show();
// If no callback is passed to createWindow,
// a new session will be created by default.
if (!fn) {
fn = win => win.rpc.emit('termgroup add req');
}
// app.windowCallback is the createWindow callback
// that can be set before the 'ready' app event
// and createWindow deifinition. It's executed in place of
// the callback passed as parameter, and deleted right after.
(app.windowCallback || fn)(this);
delete (app.windowCallback);
fetchNotifications(this);
// auto updates
if (!isDev && process.platform !== 'linux') {
AutoUpdater(this);
} else {
console.log('ignoring auto updates during dev');
}
});
rpc.on('new', options => {
const opts = Object.assign({
rows: 40,
cols: 100,
cwd: process.argv[1] && isAbsolute(process.argv[1]) ? process.argv[1] : cfgDir,
splitDirection: undefined,
shell: cfg.shell,
shellArgs: cfg.shellArgs && Array.from(cfg.shellArgs)
}, options);
const initSession = (opts, fn) => {
fn(uuid.v4(), new Session(opts));
};
initSession(opts, (uid, session) => {
sessions.set(uid, session);
rpc.emit('session add', {
rows: opts.rows,
cols: opts.cols,
uid,
splitDirection: opts.splitDirection,
shell: session.shell,
pid: session.pty.pid
});
session.on('data', data => {
rpc.emit('session data', uid + data);
});
session.on('exit', () => {
rpc.emit('session exit', {uid});
});
});
});
rpc.on('exit', ({uid}) => {
const session = sessions.get(uid);
if (session) {
session.exit();
} else {
console.log('session not found by', uid);
}
});
rpc.on('unmaximize', () => {
this.unmaximize();
});
rpc.on('maximize', () => {
this.maximize();
});
rpc.on('minimize', () => {
this.minimize();
});
rpc.on('resize', ({uid, cols, rows}) => {
const session = sessions.get(uid);
session.resize({cols, rows});
});
rpc.on('data', ({uid, data, escaped}) => {
const session = sessions.get(uid);
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('open external', ({url}) => {
shell.openExternal(url);
});
rpc.on('open hamburger menu', ({x, y}) => {
Menu.getApplicationMenu().popup(Math.ceil(x), 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']) {
this.on(ev, () => rpc.emit('windowGeometry change'));
}
rpc.win.on('move', () => {
rpc.emit('move');
});
rpc.on('close', () => {
this.close();
});
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;
this.webContents.on('did-navigate', () => {
if (i++) {
deleteSessions();
}
});
// If file is dropped onto the terminal window, navigate event is prevented
// and his path is added to active session.
this.webContents.on('will-navigate', (event, url) => {
const protocol = typeof url === 'string' && parseUrl(url).protocol;
if (protocol === 'file:') {
event.preventDefault();
const path = fileUriToPath(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});
}
});
// expose internals to extension authors
this.rpc = rpc;
this.sessions = sessions;
const load = () => {
app.plugins.onWindow(this);
};
// load plugins
load();
const pluginsUnsubscribe = app.plugins.subscribe(err => {
if (!err) {
load();
this.webContents.send('plugins change');
}
});
// 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 = () => {
this.focusTime = process.uptime();
};
this.on('focus', () => {
updateFocusTime();
});
// the window can be closed by the browser process itself
this.clean = () => {
app.config.winRecord(this);
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();
}
};