diff --git a/app/ext-modules.d.ts b/app/ext-modules.d.ts new file mode 100644 index 00000000..cbe6f667 --- /dev/null +++ b/app/ext-modules.d.ts @@ -0,0 +1,3 @@ +declare module 'git-describe' { + export function gitDescribe(...args: any[]): void; +} diff --git a/app/index.d.ts b/app/index.d.ts new file mode 100644 index 00000000..1349d7d9 --- /dev/null +++ b/app/index.d.ts @@ -0,0 +1 @@ +// Dummy file, required by tsc diff --git a/app/index.ts b/app/index.ts index 49474b4e..5fba4d7e 100644 --- a/app/index.ts +++ b/app/index.ts @@ -1,5 +1,6 @@ // Print diagnostic information for a few arguments instead of running Hyper. if (['--help', '-v', '--version'].includes(process.argv[1])) { + // eslint-disable-next-line @typescript-eslint/no-var-requires const {version} = require('./package'); const configLocation = process.platform === 'win32' ? `${process.env.userprofile}\\.hyper.js` : '~/.hyper.js'; //eslint-disable-next-line no-console @@ -25,6 +26,7 @@ const checkSquirrel = () => { // handle startup squirrel events if (process.platform === 'win32') { + // eslint-disable-next-line @typescript-eslint/no-var-requires const systemContextMenu = require('./system-context-menu'); switch (process.argv[1]) { @@ -53,16 +55,38 @@ import {gitDescribe} from 'git-describe'; import isDev from 'electron-is-dev'; import * as config from './config'; +// Hack - this declararion doesn't work when put into ./ext-modules.d.ts for some reason so it's in this file for the time being +declare module 'electron' { + interface App { + config: typeof import('./config'); + plugins: typeof import('./plugins'); + getWindows: () => Set; + getLastFocusedWindow: () => BrowserWindow | null; + windowCallback: (win: BrowserWindow) => void; + createWindow: (fn?: (win: BrowserWindow) => void, options?: Record) => BrowserWindow; + setVersion: (version: string) => void; + } + + type Server = import('./rpc').Server; + interface BrowserWindow { + uid: string; + sessions: Map; + focusTime: number; + clean: () => void; + rpc: Server; + } +} + // set up config config.setup(); import * as plugins from './plugins'; import {installCLI} from './utils/cli-install'; import * as AppMenu from './menus/menu'; -import Window from './ui/window'; +import {newWindow} from './ui/window'; import * as windowUtils from './utils/window-utils'; -const windowSet = new Set([]); +const windowSet = new Set([]); // expose to plugins app.config = config; @@ -89,7 +113,7 @@ if (isDev) { console.log('running in dev mode'); // Override default appVersion which is set from package.json - gitDescribe({customArguments: ['--tags']}, (error, gitInfo) => { + gitDescribe({customArguments: ['--tags']}, (error: any, gitInfo: any) => { if (!error) { app.setVersion(gitInfo.raw); } @@ -103,16 +127,30 @@ const url = `file://${resolve(isDev ? __dirname : app.getAppPath(), 'index.html' //eslint-disable-next-line no-console console.log('electron will open', url); +function installDevExtensions(isDev_: boolean) { + if (!isDev_) { + return Promise.resolve([]); + } + // eslint-disable-next-line @typescript-eslint/no-var-requires + const installer = require('electron-devtools-installer') as typeof import('electron-devtools-installer'); + + const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'] as const; + const forceDownload = Boolean(process.env.UPGRADE_EXTENSIONS); + + return Promise.all(extensions.map(name => installer.default(installer[name], forceDownload))); +} + app.on('ready', () => installDevExtensions(isDev) .then(() => { - function createWindow(fn, options = {}) { + function createWindow(fn?: (win: BrowserWindow) => void, options: Record = {}) { const cfg = plugins.getDecoratedConfig(); const winSet = config.getWin(); let [startX, startY] = winSet.position; const [width, height] = options.size ? options.size : cfg.windowSize || winSet.size; + // eslint-disable-next-line @typescript-eslint/no-var-requires const {screen} = require('electron'); const winPos = options.position; @@ -150,7 +188,7 @@ app.on('ready', () => [startX, startY] = config.windowDefaults.windowPosition; } - const hwin = new Window({width, height, x: startX, y: startY}, cfg, fn); + const hwin = newWindow({width, height, x: startX, y: startY}, cfg, fn); windowSet.add(hwin); hwin.loadURL(url); @@ -229,7 +267,7 @@ app.on('ready', () => app.on('open-file', (event, path) => { const lastWindow = app.getLastFocusedWindow(); - const callback = win => win.rpc.emit('open file', {path}); + const callback = (win: BrowserWindow) => win.rpc.emit('open file', {path}); if (lastWindow) { callback(lastWindow); } else if (!lastWindow && {}.hasOwnProperty.call(app, 'createWindow')) { @@ -243,7 +281,7 @@ app.on('open-file', (event, path) => { app.on('open-url', (event, sshUrl) => { const lastWindow = app.getLastFocusedWindow(); - const callback = win => win.rpc.emit('open ssh', sshUrl); + const callback = (win: BrowserWindow) => win.rpc.emit('open ssh', sshUrl); if (lastWindow) { callback(lastWindow); } else if (!lastWindow && {}.hasOwnProperty.call(app, 'createWindow')) { @@ -254,15 +292,3 @@ app.on('open-url', (event, sshUrl) => { app.windowCallback = callback; } }); - -function installDevExtensions(isDev_) { - if (!isDev_) { - return Promise.resolve(); - } - const installer = require('electron-devtools-installer'); - - const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']; - const forceDownload = Boolean(process.env.UPGRADE_EXTENSIONS); - - return Promise.all(extensions.map(name => installer.default(installer[name], forceDownload))); -} diff --git a/app/menus/menu.ts b/app/menus/menu.ts index 5eb7b820..0a21d144 100644 --- a/app/menus/menu.ts +++ b/app/menus/menu.ts @@ -1,5 +1,5 @@ // Packages -import {app, dialog, Menu} from 'electron'; +import {app, dialog, Menu, BrowserWindow} from 'electron'; // Utilities import {getConfig} from '../config'; @@ -18,13 +18,16 @@ import {getRendererTypes} from '../utils/renderer-utils'; const appName = app.getName(); const appVersion = app.getVersion(); -let menu_ = []; +let menu_: Menu; -export const createMenu = (createWindow, getLoadedPluginVersions) => { +export const createMenu = ( + createWindow: (fn?: (win: BrowserWindow) => void, options?: Record) => BrowserWindow, + getLoadedPluginVersions: () => {name: string; version: string}[] +) => { const config = getConfig(); // We take only first shortcut in array for each command const allCommandKeys = getDecoratedKeymaps(); - const commandKeys = Object.keys(allCommandKeys).reduce((result, command) => { + const commandKeys = Object.keys(allCommandKeys).reduce((result: Record, command) => { result[command] = allCommandKeys[command][0]; return result; }, {}); @@ -40,7 +43,7 @@ export const createMenu = (createWindow, getLoadedPluginVersions) => { const pluginList = loadedPlugins.length === 0 ? 'none' : loadedPlugins.map(plugin => `\n ${plugin.name} (${plugin.version})`); - const rendererCounts = Object.values(getRendererTypes()).reduce((acc, type) => { + const rendererCounts = Object.values(getRendererTypes()).reduce((acc: Record, type) => { acc[type] = acc[type] ? acc[type] + 1 : 1; return acc; }, {}); @@ -53,7 +56,7 @@ export const createMenu = (createWindow, getLoadedPluginVersions) => { message: `${appName} ${appVersion} (${updateChannel})`, detail: `Renderers: ${renderers}\nPlugins: ${pluginList}\n\nCreated by Guillermo Rauch\nCopyright © 2019 ZEIT, Inc.`, buttons: [], - icon + icon: icon as any }); }; const menu = [ @@ -69,7 +72,7 @@ export const createMenu = (createWindow, getLoadedPluginVersions) => { return menu; }; -export const buildMenu = template => { +export const buildMenu = (template: Electron.MenuItemConstructorOptions[]): Electron.Menu => { menu_ = Menu.buildFromTemplate(template); return menu_; }; diff --git a/app/rpc.ts b/app/rpc.ts index e964d141..5f9f39b4 100644 --- a/app/rpc.ts +++ b/app/rpc.ts @@ -1,9 +1,12 @@ import {EventEmitter} from 'events'; -import {ipcMain} from 'electron'; +import {ipcMain, BrowserWindow} from 'electron'; import uuid from 'uuid'; -class Server extends EventEmitter { - constructor(win) { +export class Server extends EventEmitter { + destroyed = false; + win: BrowserWindow; + id!: string; + constructor(win: BrowserWindow) { super(); this.win = win; this.ipcListener = this.ipcListener.bind(this); @@ -29,11 +32,11 @@ class Server extends EventEmitter { return this.win.webContents; } - ipcListener(event, {ev, data}) { + ipcListener(event: any, {ev, data}: {ev: string; data: any}) { super.emit(ev, data); } - emit(ch, data) { + emit(ch: string, data: any): any { // This check is needed because data-batching can cause extra data to be // emitted after the window has already closed if (!this.win.isDestroyed()) { @@ -53,6 +56,6 @@ class Server extends EventEmitter { } } -export default win => { +export default (win: BrowserWindow) => { return new Server(win); }; diff --git a/app/ui/window.ts b/app/ui/window.ts index 9f830f67..99d45011 100644 --- a/app/ui/window.ts +++ b/app/ui/window.ts @@ -1,4 +1,4 @@ -import {app, BrowserWindow, shell, Menu} from 'electron'; +import {app, BrowserWindow, shell, Menu, BrowserWindowConstructorOptions} from 'electron'; import {isAbsolute} from 'path'; import {parse as parseUrl} from 'url'; import uuid from 'uuid'; @@ -16,301 +16,303 @@ import {execCommand} from '../commands'; import {setRendererType, unsetRendererType} from '../utils/renderer-utils'; import {decorateSessionOptions, decorateSessionClass} from '../plugins'; -export default class Window { - constructor(options_, cfg, fn) { - const classOpts = Object.assign({uid: uuid.v4()}); - app.plugins.decorateWindowClass(classOpts); - this.uid = classOpts.uid; +export function newWindow( + options_: BrowserWindowConstructorOptions, + cfg: any, + fn?: (win: BrowserWindow) => void +): BrowserWindow { + const classOpts = Object.assign({uid: uuid.v4()}); + app.plugins.decorateWindowClass(classOpts); - app.plugins.onWindowClass(this); - - const winOpts = Object.assign( - { - 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: process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev, - acceptFirstMouse: true, - webPreferences: { - nodeIntegration: true, - navigateOnDragDrop: true - } - }, - options_ - ); - - const window = new BrowserWindow(app.plugins.getDecoratedBrowserOptions(winOpts)); - 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 - let workingDirectory = cfgDir; - if (process.argv[1] && isAbsolute(process.argv[1])) { - workingDirectory = process.argv[1]; - } 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'); + const winOpts = Object.assign( + { + 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: process.env.HYPER_DEBUG || process.env.HYPERTERM_DEBUG || isDev, + acceptFirstMouse: true, + webPreferences: { + nodeIntegration: true, + navigateOnDragDrop: true } + }, + options_ + ); - // update background color if necessary - updateBackgroundColor(); + const window = new BrowserWindow(app.plugins.getDecoratedBrowserOptions(winOpts)); + window.uid = classOpts.uid; - cfg = cfg_; - }); + app.plugins.onWindowClass(window); + window.uid = classOpts.uid; - rpc.on('init', () => { - window.show(); - updateBackgroundColor(); + const rpc = createRPC(window); + const sessions = new Map(); - // 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', {}); - } + const updateBackgroundColor = () => { + const cfg_ = app.plugins.getDecoratedConfig(); + window.setBackgroundColor(toElectronBackgroundColor(cfg_.backgroundColor || '#000')); + }; - // 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'); - } - }); - - function createSession(extraOptions = {}) { - const uid = uuid.v4(); - - // 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) - }, - extraOptions, - {uid} - ); - const options = decorateSessionOptions(defaultOptions); - const DecoratedSession = decorateSessionClass(Session); - const session = new DecoratedSession(options); - sessions.set(uid, session); - return {session, options}; - } - - 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 - }); - - session.on('data', data => { - 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}) => { - 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}) => { - 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({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']) { - 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(); - }); - 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(); - } - }); - - // 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); - - rpc.emit('session data send', {data: path, escaped: true}); - } else if (protocol === 'http:' || protocol === 'https:') { - event.preventDefault(); - rpc.emit('session data send', {data: 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); - } - }); - - // 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 => { - 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; + // set working directory + let workingDirectory = cfgDir; + if (process.argv[1] && isAbsolute(process.argv[1])) { + workingDirectory = process.argv[1]; + } 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); + 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'); + } + }); + + function createSession(extraOptions = {}) { + const uid = uuid.v4(); + + // 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) + }, + extraOptions, + {uid} + ); + const options = decorateSessionOptions(defaultOptions); + const DecoratedSession = decorateSessionClass(Session); + const session = new DecoratedSession(options); + sessions.set(uid, session); + return {session, options}; + } + + 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 + }); + + session.on('data', (data: any) => { + 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}) => { + 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}) => { + 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({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(); + }); + 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(); + } + }); + + // 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); + + rpc.emit('session data send', {data: path, escaped: true}); + } else if (protocol === 'http:' || protocol === 'https:') { + event.preventDefault(); + rpc.emit('session data send', {data: 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); + } + }); + + // 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; } diff --git a/package.json b/package.json index 44d57f67..75a935e8 100644 --- a/package.json +++ b/package.json @@ -297,6 +297,7 @@ "@types/args": "3.0.0", "@types/color": "3.0.0", "@types/columnify": "^1.5.0", + "@types/electron-devtools-installer": "2.2.0", "@types/mousetrap": "^1.6.3", "@types/node": "^12.12.18", "@types/pify": "3.0.2", diff --git a/webpack.config.js b/webpack.config.js index b4de0bb2..d245d55e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,7 +13,7 @@ module.exports = [ resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'] }, - entry: './app/index.js', + entry: './app/index.ts', output: { path: path.join(__dirname, 'target'), filename: 'ignore_this.js' diff --git a/yarn.lock b/yarn.lock index db6b0b74..88ab3f96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -620,6 +620,11 @@ resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== +"@types/electron-devtools-installer@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@types/electron-devtools-installer/-/electron-devtools-installer-2.2.0.tgz#32ee4ebbe99b3daf9847a6d2097dc00b5de94f10" + integrity sha512-HJNxpaOXuykCK4rQ6FOMxAA0NLFYsf7FiPFGmab0iQmtVBHSAfxzy3MRFpLTTDDWbV0yD2YsHOQvdu8yCqtCfw== + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"