diff --git a/app/index.js b/app/index.js index f962090711f4..eb2fc7a61157 100644 --- a/app/index.js +++ b/app/index.js @@ -1,18 +1,3 @@ -const {app} = require('electron'); - -// Multiple instance check on windows -if (process.platform === 'win32') { - const isSecondInstance = app.makeSingleInstance((commandLine, workingDirectory) => { - // When tried to run a second instance, load a preloaded window instead on the main instance - app.loadPreloadedWindow(workingDirectory); - app.createPreloadedWindow(); - }); - - if (isSecondInstance) { - app.quit(); - } -} - // Print diagnostic information for a few arguments instead of running Hyper. if (['--help', '-v', '--version'].includes(process.argv[1])) { const {version} = require('./package'); @@ -67,7 +52,7 @@ if (process.platform === 'win32') { const {resolve} = require('path'); // Packages -const {BrowserWindow, Menu, Tray} = require('electron'); +const {app, BrowserWindow, Menu} = require('electron'); const {gitDescribe} = require('git-describe'); const isDev = require('electron-is-dev'); @@ -123,10 +108,7 @@ const url = 'file://' + resolve(isDev ? __dirname : app.getAppPath(), 'index.htm //eslint-disable-next-line no-console console.log('electron will open', url); -let tray = null; -const {icon} = require('./config/paths'); - -app.on('ready', () => { +app.on('ready', () => installDevExtensions(isDev) .then(() => { function createWindow(fn, options = {}) { @@ -183,16 +165,11 @@ app.on('ready', () => { windowSet.delete(hwin); }); - // On Windows, do not quit app when all Hyper windows are closed - if (process.platform === 'win32') { - hwin.attachRPC(); - } else { - hwin.on('closed', () => { - if (process.platform !== 'darwin' && windowSet.size === 0) { - app.quit(); - } - }); - } + hwin.on('closed', () => { + if (process.platform !== 'darwin' && windowSet.size === 0) { + app.quit(); + } + }); return hwin; } @@ -212,82 +189,6 @@ app.on('ready', () => { } }); - // Create a hidden window without rpc - // Reference to this preloaded window is stored in app.preloadedWindow - function createPreloadedWindow(fn, options = {}) { - const cfg = plugins.getDecoratedConfig(); - - const winSet = config.getWin(); - let [startX, startY] = winSet.position; - - const [width, height] = options.size ? options.size : cfg.windowSize || winSet.size; - const {screen} = require('electron'); - - const winPos = options.position; - - // Open the new window roughly the height of the header away from the - // previous window. This also ensures in multi monitor setups that the - // new terminal is on the correct screen. - const focusedWindow = BrowserWindow.getFocusedWindow() || app.getLastFocusedWindow(); - // In case of options defaults position and size, we should ignore the focusedWindow. - if (winPos !== undefined) { - [startX, startY] = winPos; - } else if (focusedWindow) { - const points = focusedWindow.getPosition(); - const currentScreen = screen.getDisplayNearestPoint({ - x: points[0], - y: points[1] - }); - - const biggestX = points[0] + 100 + width - currentScreen.bounds.x; - const biggestY = points[1] + 100 + height - currentScreen.bounds.y; - - if (biggestX > currentScreen.size.width) { - startX = 50; - } else { - startX = points[0] + 34; - } - if (biggestY > currentScreen.size.height) { - startY = 50; - } else { - startY = points[1] + 34; - } - } - - if (!windowUtils.positionIsValid([startX, startY])) { - [startX, startY] = config.windowDefaults.windowPosition; - } - - const hwin = new Window({width, height, x: startX, y: startY, show: false}, cfg, fn); - windowSet.add(hwin); - hwin.loadURL(url); - - // the window can be closed by the browser process itself - hwin.on('close', () => { - hwin.clean(); - windowSet.delete(hwin); - }); - - app.preloadedWindow = hwin; - } - - // Preloads a window on start up - app.createPreloadedWindow = createPreloadedWindow; - app.createPreloadedWindow(); - - // Loads a preloaded window (attach all rpc related functions) - function loadPreloadedWindow(workingDirectory) { - if (app.preloadedWindow) { - app.preloadedWindow.show(); - app.preloadedWindow.attachRPC(workingDirectory); - app.preloadedWindow.webContents.emit('did-finish-load'); - } - - app.preloadedWindow = null; - } - - app.loadPreloadedWindow = loadPreloadedWindow; - const makeMenu = () => { const menu = plugins.decorateMenu(AppMenu.createMenu(createWindow, plugins.getLoadedPluginVersions)); @@ -324,41 +225,12 @@ app.on('ready', () => { } installCLI(false); } - - // Creates a system tray icon on Windows - if (process.platform === 'win32') { - tray = new Tray(icon); - - const contextMenu = Menu.buildFromTemplate([ - { - label: 'New Window', - type: 'normal', - click: () => { - app.loadPreloadedWindow(); - app.createPreloadedWindow(); - } - }, - {type: 'separator'}, - { - label: 'Exit', - type: 'normal', - click: () => { - if (app.preloadedWindow) { - app.preloadedWindow.emit('close'); - } - app.quit(); - } - } - ]); - tray.setToolTip('Hyper'); - tray.setContextMenu(contextMenu); - } }) .catch(err => { //eslint-disable-next-line no-console console.error('Error while loading devtools extensions', err); - }); -}); + }) +); app.on('open-file', (event, path) => { const lastWindow = app.getLastFocusedWindow(); diff --git a/app/ui/window.js b/app/ui/window.js index 078ff54c7f35..eb873506f16a 100644 --- a/app/ui/window.js +++ b/app/ui/window.js @@ -33,6 +33,7 @@ module.exports = class Window { options_ ); const window = new BrowserWindow(app.plugins.getDecoratedBrowserOptions(winOpts)); + const rpc = createRPC(window); const sessions = new Map(); const updateBackgroundColor = () => { @@ -58,270 +59,246 @@ module.exports = class Window { cfg = cfg_; }); - const attachRPC = workingDirectory => { - const rpc = createRPC(window); - - 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 => 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); - delete app.windowCallback; - fetchNotifications(window); - // auto updates - if (!isDev) { - updater(window); - } else { - //eslint-disable-next-line no-console - console.log('ignoring auto updates during dev'); - } - }); + rpc.on('init', () => { + window.show(); + updateBackgroundColor(); - function createSession(options) { - const uid = uuid.v4(); - const session = new Session(Object.assign({}, options, {uid})); - sessions.set(uid, session); - return {uid, session}; + // 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'); } - // Optimistically create the initial session so that when the window sends - // the first "new" IPC message, there's a session already warmed up. - function createInitialSession() { - let {session, uid} = createSession({}); - const initialEvents = []; - const handleData = data => initialEvents.push(['session data', uid + data]); - const handleExit = () => initialEvents.push(['session exit']); - session.on('data', handleData); - session.on('exit', handleExit); - - function flushEvents() { - for (let args of initialEvents) { - rpc.emit(...args); - } - session.removeListener('data', handleData); - session.removeListener('exit', handleExit); - } - return {session, uid, flushEvents}; + // 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); + delete app.windowCallback; + fetchNotifications(window); + // auto updates + if (!isDev) { + updater(window); + } else { + //eslint-disable-next-line no-console + console.log('ignoring auto updates during dev'); } - let initialSession = createInitialSession(); + }); - rpc.on('new', options => { - let cwd = null; - if (workingDirectory) { - // this is the case when on Windows and second instance tried to run - cwd = workingDirectory; - } else { - cwd = process.argv[1] && isAbsolute(process.argv[1]) ? process.argv[1] : cfgDir; - } + function createSession(options) { + const uid = uuid.v4(); + const session = new Session(Object.assign({}, options, {uid})); + sessions.set(uid, session); + return {uid, session}; + } - const sessionOpts = Object.assign( - { - rows: 40, - cols: 100, - cwd: cwd, - splitDirection: undefined, - shell: cfg.shell, - shellArgs: cfg.shellArgs && Array.from(cfg.shellArgs) - }, - options - ); - - const {uid, session} = initialSession || createSession(); - - sessions.set(uid, session); - rpc.emit('session add', { - rows: sessionOpts.rows, - cols: sessionOpts.cols, - uid, - splitDirection: sessionOpts.splitDirection, - shell: session.shell, - pid: session.pty.pid - }); - - // If this is the initial session, flush any events that might have - // occurred while the window was initializing - if (initialSession) { - initialSession.flushEvents(); - initialSession = null; + // Optimistically create the initial session so that when the window sends + // the first "new" IPC message, there's a session already warmed up. + function createInitialSession() { + let {session, uid} = createSession({}); + const initialEvents = []; + const handleData = data => initialEvents.push(['session data', uid + data]); + const handleExit = () => initialEvents.push(['session exit']); + session.on('data', handleData); + session.on('exit', handleExit); + + function flushEvents() { + for (let args of initialEvents) { + rpc.emit(...args); } + session.removeListener('data', handleData); + session.removeListener('exit', handleExit); + } + return {session, uid, flushEvents}; + } + let initialSession = createInitialSession(); + + rpc.on('new', options => { + const sessionOpts = 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 {uid, session} = initialSession || createSession(); + + sessions.set(uid, session); + rpc.emit('session add', { + rows: sessionOpts.rows, + cols: sessionOpts.cols, + uid, + splitDirection: sessionOpts.splitDirection, + shell: session.shell, + pid: session.pty.pid + }); - session.on('data', data => { - rpc.emit('session data', uid + data); - }); + // If this is the initial session, flush any events that might have + // occurred while the window was initializing + if (initialSession) { + initialSession.flushEvents(); + initialSession = null; + } - session.on('exit', () => { - rpc.emit('session exit', {uid}); - sessions.delete(uid); - }); - }); - rpc.on('exit', ({uid}) => { - const session = sessions.get(uid); - if (session) { - session.exit(); - } + session.on('data', data => { + rpc.emit('session data', uid + data); }); - 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); - } - } - }); - rpc.on('open external', ({url}) => { - shell.openExternal(url); - }); - rpc.on('open context menu', selection => { - const {createWindow} = app; - const {buildFromTemplate} = Menu; - buildFromTemplate(contextMenuTemplate(createWindow, selection)).popup(window); - }); - rpc.on('open hamburger menu', ({x, y}) => { - Menu.getApplicationMenu().popup(Math.ceil(x), Math.ceil(y)); + + session.on('exit', () => { + rpc.emit('session exit', {uid}); + sessions.delete(uid); }); - // 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']) { - window.on(ev, () => rpc.emit('windowGeometry change')); + }); + + rpc.on('exit', ({uid}) => { + const session = sessions.get(uid); + if (session) { + session.exit(); } - rpc.win.on('move', () => { - rpc.emit('move'); - }); - rpc.on('close', () => { - window.close(); - }); - rpc.on('command', command => { - const focusedWindow = BrowserWindow.getFocusedWindow(); - execCommand(command, focusedWindow); - }); - 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(); + }); + 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); } + } + }); + rpc.on('open external', ({url}) => { + shell.openExternal(url); + }); + rpc.on('open context menu', selection => { + const {createWindow} = app; + const {buildFromTemplate} = Menu; + buildFromTemplate(contextMenuTemplate(createWindow, selection)).popup(window); + }); + 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']) { + window.on(ev, () => rpc.emit('windowGeometry change')); + } + rpc.win.on('move', () => { + rpc.emit('move'); + }); + rpc.on('close', () => { + window.close(); + }); + rpc.on('command', command => { + const focusedWindow = BrowserWindow.getFocusedWindow(); + execCommand(command, focusedWindow); + }); + 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(); + } + }); - // 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(); - - const path = fileUriToPath(url); + // 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(); - rpc.emit('session data send', {data: path, escaped: true}); - } else if (protocol === 'http:' || protocol === 'https:') { - event.preventDefault(); - rpc.emit('session data send', {data: url}); - } - }); + const path = fileUriToPath(url); - // 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(); - shell.openExternal(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 - window.rpc = rpc; - window.sessions = sessions; + // 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(); + shell.openExternal(url); + } + }); - const load = () => { - app.plugins.onWindow(window); - }; + // expose internals to extension authors + window.rpc = rpc; + window.sessions = sessions; - // load plugins - load(); + const load = () => { + app.plugins.onWindow(window); + }; - const pluginsUnsubscribe = app.plugins.subscribe(err => { - if (!err) { - load(); - window.webContents.send('plugins change'); - updateBackgroundColor(); - } - }); + // load plugins + load(); - // 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(); - }; + const pluginsUnsubscribe = app.plugins.subscribe(err => { + if (!err) { + load(); + window.webContents.send('plugins change'); + updateBackgroundColor(); + } + }); - window.on('focus', () => { - updateFocusTime(); - }); + // 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(); + }; - // 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) + window.on('focus', () => { updateFocusTime(); - }; + }); - // Allows delayed rpc related calls on Windows - if (process.platform === 'win32') { - window.attachRPC = attachRPC; - - // Allows cleaning up window without rpc - window.clean = () => { - app.config.winRecord(window); - cfgUnsubscribe(); - }; - } else { - attachRPC(); - } + // 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; }