This commit is contained in:
crusader
2018-08-16 19:49:37 +09:00
parent 648efb7a7a
commit a9f514b9fb
20 changed files with 1802 additions and 298 deletions

View File

@@ -1,282 +1,293 @@
// import { BrowserWindow, ipcMain, Menu, app, dialog } from 'electron';
// import { encodePathAsUrl } from '../lib/path';
// import { registerWindowStateChangedEvents } from '../lib/window-state';
// import { MenuEvent } from './menu';
// import { URLActionType } from '../lib/parse-app-url';
// import { ILaunchStats } from '../lib/stats';
// import { menuFromElectronMenu } from '../models/app-menu';
// import { now } from './now';
// import * as path from 'path';
import { BrowserWindow, ipcMain, Menu, app, dialog } from 'electron';
import * as path from 'path';
import * as URL from 'url';
import { EventEmitter } from 'events';
// let windowStateKeeper: any | null = null;
import { encodePathAsUrl } from '@overflow/core/path';
import { registerWindowStateChangedEvents } from '@overflow/core/window-state';
import { URLActionType } from '@overflow/core/parse-app-url';
import { now } from '@overflow/core/now';
// export class AppWindow {
// private window: Electron.BrowserWindow;
// private emitter = new Emitter();
import { MenuEvent } from './menu';
// private _loadTime: number | null = null;
// private _rendererReadyTime: number | null = null;
let windowStateKeeper: any | null = null;
// private minWidth = 960;
// private minHeight = 660;
export class AppWindow {
private window: Electron.BrowserWindow;
private emitter = new EventEmitter();
// public constructor() {
// if (!windowStateKeeper) {
// // `electron-window-state` requires Electron's `screen` module, which can
// // only be required after the app has emitted `ready`. So require it
// // lazily.
// windowStateKeeper = require('electron-window-state');
// }
private _loadTime: number | null = null;
private _rendererReadyTime: number | null = null;
// const savedWindowState = windowStateKeeper({
// defaultWidth: this.minWidth,
// defaultHeight: this.minHeight,
// });
private minWidth = 960;
private minHeight = 660;
// const windowOptions: Electron.BrowserWindowConstructorOptions = {
// x: savedWindowState.x,
// y: savedWindowState.y,
// width: savedWindowState.width,
// height: savedWindowState.height,
// minWidth: this.minWidth,
// minHeight: this.minHeight,
// show: false,
// // This fixes subpixel aliasing on Windows
// // See https://github.com/atom/atom/commit/683bef5b9d133cb194b476938c77cc07fd05b972
// backgroundColor: '#fff',
// webPreferences: {
// // Disable auxclick event
// // See https://developers.google.com/web/updates/2016/10/auxclick
// disableBlinkFeatures: 'Auxclick',
// // Enable, among other things, the ResizeObserver
// experimentalFeatures: true,
// },
// acceptFirstMouse: true,
// };
public constructor() {
if (!windowStateKeeper) {
// `electron-window-state` requires Electron's `screen` module, which can
// only be required after the app has emitted `ready`. So require it
// lazily.
windowStateKeeper = require('electron-window-state');
}
// if (__DARWIN__) {
// windowOptions.titleBarStyle = 'hidden';
// } else if (__WIN32__) {
// windowOptions.frame = false;
// } else if (__LINUX__) {
// windowOptions.icon = path.join(__dirname, 'static', 'icon-logo.png');
// }
const savedWindowState = windowStateKeeper({
defaultWidth: this.minWidth,
defaultHeight: this.minHeight,
});
// this.window = new BrowserWindow(windowOptions);
// savedWindowState.manage(this.window);
const windowOptions: Electron.BrowserWindowConstructorOptions = {
x: savedWindowState.x,
y: savedWindowState.y,
width: savedWindowState.width,
height: savedWindowState.height,
minWidth: this.minWidth,
minHeight: this.minHeight,
show: true,
// This fixes subpixel aliasing on Windows
// See https://github.com/atom/atom/commit/683bef5b9d133cb194b476938c77cc07fd05b972
backgroundColor: '#fff',
webPreferences: {
// Disable auxclick event
// See https://developers.google.com/web/updates/2016/10/auxclick
disableBlinkFeatures: 'Auxclick',
// Enable, among other things, the ResizeObserver
experimentalFeatures: true,
},
acceptFirstMouse: true,
};
// let quitting = false;
// app.on('before-quit', () => {
// quitting = true;
// });
if (__DARWIN__) {
windowOptions.titleBarStyle = 'hidden';
} else if (__WIN32__) {
windowOptions.frame = false;
} else if (__LINUX__) {
// windowOptions.icon = path.join(__dirname, 'static', 'icon-logo.png');
}
// ipcMain.on('will-quit', (event: Electron.IpcMessageEvent) => {
// quitting = true;
// event.returnValue = true;
// });
console.log(windowOptions);
// // on macOS, when the user closes the window we really just hide it. This
// // lets us activate quickly and keep all our interesting logic in the
// // renderer.
// if (__DARWIN__) {
// this.window.on('close', e => {
// if (!quitting) {
// e.preventDefault();
// Menu.sendActionToFirstResponder('hide:');
// }
// });
// }
// }
this.window = new BrowserWindow(windowOptions);
savedWindowState.manage(this.window);
// public load() {
// let startLoad = 0;
// // We only listen for the first of the loading events to avoid a bug in
// // Electron/Chromium where they can sometimes fire more than once. See
// // See
// // https://github.com/desktop/desktop/pull/513#issuecomment-253028277. This
// // shouldn't really matter as in production builds loading _should_ only
// // happen once.
// this.window.webContents.once('did-start-loading', () => {
// this._rendererReadyTime = null;
// this._loadTime = null;
let quitting = false;
app.on('before-quit', () => {
quitting = true;
});
// startLoad = now();
// });
ipcMain.on('will-quit', (event: Electron.IpcMessageEvent) => {
quitting = true;
event.returnValue = true;
});
// this.window.webContents.once('did-finish-load', () => {
// if (process.env.NODE_ENV === 'development') {
// this.window.webContents.openDevTools();
// }
// on macOS, when the user closes the window we really just hide it. This
// lets us activate quickly and keep all our interesting logic in the
// renderer.
if (__DARWIN__) {
this.window.on('close', e => {
if (!quitting) {
e.preventDefault();
Menu.sendActionToFirstResponder('hide:');
}
});
}
}
// this._loadTime = now() - startLoad;
public load() {
let startLoad = 0;
// We only listen for the first of the loading events to avoid a bug in
// Electron/Chromium where they can sometimes fire more than once. See
// See
// https://github.com/desktop/desktop/pull/513#issuecomment-253028277. This
// shouldn't really matter as in production builds loading _should_ only
// happen once.
this.window.webContents.once('did-start-loading', () => {
this._rendererReadyTime = null;
this._loadTime = null;
// this.maybeEmitDidLoad();
// });
startLoad = now();
});
// this.window.webContents.on('did-finish-load', () => {
// this.window.webContents.setVisualZoomLevelLimits(1, 1);
// });
this.window.webContents.once('did-finish-load', () => {
if (process.env.NODE_ENV === 'development') {
this.window.webContents.openDevTools();
}
// this.window.webContents.on('did-fail-load', () => {
// this.window.webContents.openDevTools();
// this.window.show();
// });
this._loadTime = now() - startLoad;
// // TODO: This should be scoped by the window.
// ipcMain.once(
// 'renderer-ready',
// (event: Electron.IpcMessageEvent, readyTime: number) => {
// this._rendererReadyTime = readyTime;
this.maybeEmitDidLoad();
});
// this.maybeEmitDidLoad();
// }
// );
this.window.webContents.on('did-finish-load', () => {
this.window.webContents.setVisualZoomLevelLimits(1, 1);
});
// this.window.on('focus', () => this.window.webContents.send('focus'));
// this.window.on('blur', () => this.window.webContents.send('blur'));
this.window.webContents.on('did-fail-load', () => {
this.window.webContents.openDevTools();
this.window.show();
});
// registerWindowStateChangedEvents(this.window);
// this.window.loadURL(encodePathAsUrl(__dirname, 'index.html'));
// }
// TODO: This should be scoped by the window.
ipcMain.once(
'renderer-ready',
(event: Electron.IpcMessageEvent, readyTime: number) => {
this._rendererReadyTime = readyTime;
// /**
// * Emit the `onDidLoad` event if the page has loaded and the renderer has
// * signalled that it's ready.
// */
// private maybeEmitDidLoad() {
// if (!this.rendererLoaded) {
// return;
// }
this.maybeEmitDidLoad();
}
);
// this.emitter.emit('did-load', null);
// }
this.window.on('focus', () => this.window.webContents.send('focus'));
this.window.on('blur', () => this.window.webContents.send('blur'));
// /** Is the page loaded and has the renderer signalled it's ready? */
// private get rendererLoaded(): boolean {
// return !!this.loadTime && !!this.rendererReadyTime;
// }
registerWindowStateChangedEvents(this.window);
// this.window.loadURL(encodePathAsUrl(__dirname, 'index.html'));
// public onClose(fn: () => void) {
// this.window.on('closed', fn);
// }
const indexUrl = URL.format({
pathname: path.join('//localhost:4200'),
protocol: 'http:',
slashes: true
});
this.window.loadURL(indexUrl);
}
// /**
// * Register a function to call when the window is done loading. At that point
// * the page has loaded and the renderer has signalled that it is ready.
// */
// public onDidLoad(fn: () => void): Disposable {
// return this.emitter.on('did-load', fn);
// }
/**
* Emit the `onDidLoad` event if the page has loaded and the renderer has
* signalled that it's ready.
*/
private maybeEmitDidLoad() {
if (!this.rendererLoaded) {
return;
}
// public isMinimized() {
// return this.window.isMinimized();
// }
this.emitter.emit('did-load', null);
}
// /** Is the window currently visible? */
// public isVisible() {
// return this.window.isVisible();
// }
/** Is the page loaded and has the renderer signalled it's ready? */
private get rendererLoaded(): boolean {
return !!this.loadTime && !!this.rendererReadyTime;
}
// public restore() {
// this.window.restore();
// }
public onClose(fn: () => void) {
this.window.on('closed', fn);
}
// public focus() {
// this.window.focus();
// }
/**
* Register a function to call when the window is done loading. At that point
* the page has loaded and the renderer has signalled that it is ready.
*/
public onDidLoad(fn: () => void): EventEmitter {
return this.emitter.on('did-load', fn);
}
// /** Show the window. */
// public show() {
// this.window.show();
// }
public isMinimized() {
return this.window.isMinimized();
}
// /** Send the menu event to the renderer. */
// public sendMenuEvent(name: MenuEvent) {
// this.show();
/** Is the window currently visible? */
public isVisible() {
return this.window.isVisible();
}
// this.window.webContents.send('menu-event', { name });
// }
public restore() {
this.window.restore();
}
// /** Send the URL action to the renderer. */
// public sendURLAction(action: URLActionType) {
// this.show();
public focus() {
this.window.focus();
}
// this.window.webContents.send('url-action', { action });
// }
/** Show the window. */
public show() {
this.window.show();
}
// /** Send the app launch timing stats to the renderer. */
// public sendLaunchTimingStats(stats: ILaunchStats) {
// this.window.webContents.send('launch-timing-stats', { stats });
// }
/** Send the menu event to the renderer. */
public sendMenuEvent(name: MenuEvent) {
this.show();
// /** Send the app menu to the renderer. */
// public sendAppMenu() {
// const appMenu = Menu.getApplicationMenu();
// if (appMenu) {
// const menu = menuFromElectronMenu(appMenu);
// this.window.webContents.send('app-menu', { menu });
// }
// }
this.window.webContents.send('menu-event', { name });
}
// /** Send a certificate error to the renderer. */
// public sendCertificateError(
// certificate: Electron.Certificate,
// error: string,
// url: string
// ) {
// this.window.webContents.send('certificate-error', {
// certificate,
// error,
// url,
// });
// }
/** Send the URL action to the renderer. */
public sendURLAction(action: URLActionType) {
this.show();
// public showCertificateTrustDialog(
// certificate: Electron.Certificate,
// message: string
// ) {
// // The Electron type definitions don't include `showCertificateTrustDialog`
// // yet.
// const d = dialog as any;
// d.showCertificateTrustDialog(
// this.window,
// { certificate, message },
// () => { }
// );
// }
this.window.webContents.send('url-action', { action });
}
// /** Report the exception to the renderer. */
// public sendException(error: Error) {
// // `Error` can't be JSONified so it doesn't transport nicely over IPC. So
// // we'll just manually copy the properties we care about.
// const friendlyError = {
// stack: error.stack,
// message: error.message,
// name: error.name,
// };
// this.window.webContents.send('main-process-exception', friendlyError);
// }
/** Send the app launch timing stats to the renderer. */
// public sendLaunchTimingStats(stats: ILaunchStats) {
// this.window.webContents.send('launch-timing-stats', { stats });
// }
// /**
// * Get the time (in milliseconds) spent loading the page.
// *
// * This will be `null` until `onDidLoad` is called.
// */
// public get loadTime(): number | null {
// return this._loadTime;
// }
/** Send the app menu to the renderer. */
// public sendAppMenu() {
// const appMenu = Menu.getApplicationMenu();
// if (appMenu) {
// const menu = menuFromElectronMenu(appMenu);
// this.window.webContents.send('app-menu', { menu });
// }
// }
// /**
// * Get the time (in milliseconds) elapsed from the renderer being loaded to it
// * signaling it was ready.
// *
// * This will be `null` until `onDidLoad` is called.
// */
// public get rendererReadyTime(): number | null {
// return this._rendererReadyTime;
// }
/** Send a certificate error to the renderer. */
public sendCertificateError(
certificate: Electron.Certificate,
error: string,
url: string
) {
this.window.webContents.send('certificate-error', {
certificate,
error,
url,
});
}
// public destroy() {
// this.window.destroy();
// }
// }
public showCertificateTrustDialog(
certificate: Electron.Certificate,
message: string
) {
// The Electron type definitions don't include `showCertificateTrustDialog`
// yet.
const d = dialog as any;
d.showCertificateTrustDialog(
this.window,
{ certificate, message },
() => { }
);
}
/** Report the exception to the renderer. */
public sendException(error: Error) {
// `Error` can't be JSONified so it doesn't transport nicely over IPC. So
// we'll just manually copy the properties we care about.
const friendlyError = {
stack: error.stack,
message: error.message,
name: error.name,
};
this.window.webContents.send('main-process-exception', friendlyError);
}
/**
* Get the time (in milliseconds) spent loading the page.
*
* This will be `null` until `onDidLoad` is called.
*/
public get loadTime(): number | null {
return this._loadTime;
}
/**
* Get the time (in milliseconds) elapsed from the renderer being loaded to it
* signaling it was ready.
*
* This will be `null` until `onDidLoad` is called.
*/
public get rendererReadyTime(): number | null {
return this._rendererReadyTime;
}
public destroy() {
this.window.destroy();
}
}

View File

@@ -1,74 +1,461 @@
import { app, BrowserWindow, ipcMain, dialog } from 'electron';
import * as path from 'path';
import * as url from 'url';
import { app, Menu, ipcMain, BrowserWindow, shell } from 'electron';
import * as Fs from 'fs';
// const indexUrl = url.format({
// pathname: path.join(__dirname, 'index.html'),
// protocol: 'file:',
// slashes: true
// });
import { shellNeedsPatching, updateEnvironmentForProcess } from '@overflow/core/shell';
import { parseAppURL } from '@overflow/core/parse-app-url';
import {
enableSourceMaps,
withSourceMappedStack,
} from '@overflow/core/source-map-support';
import { now } from '@overflow/core/now';
import { IMenuItem } from '@overflow/core/menu-item';
const indexUrl = url.format({
pathname: path.join('//localhost:4200'),
protocol: 'http:',
slashes: true
import { AppWindow } from './app-window';
import { handleSquirrelEvent } from './squirrel-updater';
import { openDirectorySafe } from './shell';
enableSourceMaps();
let mainWindow: AppWindow | null = null;
const launchTime = now();
let preventQuit = false;
let readyTime: number | null = null;
type OnDidLoadFn = (window: AppWindow) => void;
/** See the `onDidLoad` function. */
const onDidLoadFns: Array<OnDidLoadFn> | null = [];
function handleUncaughtException(error: Error) {
preventQuit = true;
if (mainWindow) {
mainWindow.destroy();
mainWindow = null;
}
const isLaunchError = !mainWindow;
// showUncaughtException(isLaunchError, error);
}
process.on('uncaughtException', (error: Error) => {
error = withSourceMappedStack(error);
// reportError(error);
handleUncaughtException(error);
});
let handlingSquirrelEvent = false;
if (__WIN32__ && process.argv.length > 1) {
const arg = process.argv[1];
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win;
const promise = handleSquirrelEvent(arg);
if (promise) {
handlingSquirrelEvent = true;
promise
.catch(e => {
log.error(`Failed handling Squirrel event: ${arg}`, e);
})
.then(() => {
app.quit();
});
} else {
handlePossibleProtocolLauncherArgs(process.argv);
}
}
function createWindow() {
// Create the browser window.
win = new BrowserWindow({ width: 800, height: 600 });
// and load the index.html of the app.
win.loadURL(indexUrl);
// Open the DevTools.
win.webContents.openDevTools();
// Emitted when the window is closed.
win.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
win = null;
function handleAppURL(url: string) {
log.info('Processing protocol url');
const action = parseAppURL(url);
onDidLoad(window => {
// This manual focus call _shouldn't_ be necessary, but is for Chrome on
// macOS. See https://github.com/desktop/desktop/issues/973.
window.focus();
window.sendURLAction(action);
});
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow);
let isDuplicateInstance = false;
// If we're handling a Squirrel event we don't want to enforce single instance.
// We want to let the updated instance launch and do its work. It will then quit
// once it's done.
if (!handlingSquirrelEvent) {
isDuplicateInstance = app.makeSingleInstance((args, workingDirectory) => {
// Someone tried to run a second instance, we should focus our window.
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
if (!mainWindow.isVisible()) {
mainWindow.show();
}
mainWindow.focus();
}
handlePossibleProtocolLauncherArgs(args);
});
if (isDuplicateInstance) {
app.quit();
}
}
if (shellNeedsPatching(process)) {
updateEnvironmentForProcess();
}
app.on('will-finish-launching', () => {
// macOS only
app.on('open-url', (event, url) => {
event.preventDefault();
handleAppURL(url);
});
});
/**
* Attempt to detect and handle any protocol handler arguments passed
* either via the command line directly to the current process or through
* IPC from a duplicate instance (see makeSingleInstance)
*
* @param args Essentially process.argv, i.e. the first element is the exec
* path
*/
function handlePossibleProtocolLauncherArgs(args: ReadonlyArray<string>) {
log.info(`Received possible protocol arguments: ${args.length}`);
if (__WIN32__) {
// We register our protocol handler callback on Windows as
// [executable path] --protocol-launcher -- "%1" meaning that any
// url data comes after we've stopped processing arguments. We check
// for that exact scenario here before doing any processing. If there's
// more than 4 args because of a malformed url then we bail out.
if (
args.length === 4 &&
args[1] === '--protocol-launcher' &&
args[2] === '--'
) {
handleAppURL(args[3]);
}
} else if (args.length > 1) {
handleAppURL(args[1]);
}
}
/**
* Wrapper around app.setAsDefaultProtocolClient that adds our
* custom prefix command line switches on Windows that prevents
* command line argument parsing after the `--`.
*/
function setAsDefaultProtocolClient(protocol: string) {
if (__WIN32__) {
app.setAsDefaultProtocolClient(protocol, process.execPath, [
'--protocol-launcher',
'--',
]);
} else {
app.setAsDefaultProtocolClient(protocol);
}
}
// if (process.env.GITHUB_DESKTOP_DISABLE_HARDWARE_ACCELERATION) {
// log.info(
// `GITHUB_DESKTOP_DISABLE_HARDWARE_ACCELERATION environment variable set, disabling hardware acceleration`
// );
// app.disableHardwareAcceleration();
// }
app.on('ready', () => {
if (isDuplicateInstance || handlingSquirrelEvent) {
return;
}
readyTime = now() - launchTime;
// setAsDefaultProtocolClient('x-github-client');
// if (__DEV__) {
// setAsDefaultProtocolClient('x-github-desktop-dev-auth');
// } else {
// setAsDefaultProtocolClient('x-github-desktop-auth');
// }
// // Also support Desktop Classic's protocols.
// if (__DARWIN__) {
// setAsDefaultProtocolClient('github-mac');
// } else if (__WIN32__) {
// setAsDefaultProtocolClient('github-windows');
// }
createWindow();
// let menu = buildDefaultMenu();
// Menu.setApplicationMenu(menu);
// ipcMain.on(
// 'update-preferred-app-menu-item-labels',
// (
// event: Electron.IpcMessageEvent,
// labels: { editor?: string; pullRequestLabel?: string; shell: string }
// ) => {
// menu = buildDefaultMenu(
// labels.editor,
// labels.shell,
// labels.pullRequestLabel
// );
// Menu.setApplicationMenu(menu);
// if (mainWindow) {
// mainWindow.sendAppMenu();
// }
// }
// );
// ipcMain.on('menu-event', (event: Electron.IpcMessageEvent, args: any[]) => {
// const { name }: { name: MenuEvent } = event as any;
// if (mainWindow) {
// mainWindow.sendMenuEvent(name);
// }
// });
/**
* An event sent by the renderer asking that the menu item with the given id
* is executed (ie clicked).
*/
// ipcMain.on(
// 'execute-menu-item',
// (event: Electron.IpcMessageEvent, { id }: { id: string }) => {
// const menuItem = findMenuItemByID(menu, id);
// if (menuItem) {
// const window = BrowserWindow.fromWebContents(event.sender);
// const fakeEvent = { preventDefault: () => { }, sender: event.sender };
// menuItem.click(fakeEvent, window, event.sender);
// }
// }
// );
// ipcMain.on(
// 'update-menu-state',
// (
// event: Electron.IpcMessageEvent,
// items: Array<{ id: string; state: IMenuItemState }>
// ) => {
// let sendMenuChangedEvent = false;
// for (const item of items) {
// const { id, state } = item;
// const menuItem = findMenuItemByID(menu, id);
// if (menuItem) {
// // Only send the updated app menu when the state actually changes
// // or we might end up introducing a never ending loop between
// // the renderer and the main process
// if (
// state.enabled !== undefined &&
// menuItem.enabled !== state.enabled
// ) {
// menuItem.enabled = state.enabled;
// sendMenuChangedEvent = true;
// }
// } else {
// fatalError(`Unknown menu id: ${id}`);
// }
// }
// if (sendMenuChangedEvent && mainWindow) {
// mainWindow.sendAppMenu();
// }
// }
// );
// ipcMain.on(
// 'show-contextual-menu',
// (event: Electron.IpcMessageEvent, items: ReadonlyArray<IMenuItem>) => {
// const menu = buildContextMenu(items, ix =>
// event.sender.send('contextual-menu-action', ix)
// );
// const window = BrowserWindow.fromWebContents(event.sender);
// menu.popup({ window });
// }
// );
/**
* An event sent by the renderer asking for a copy of the current
* application menu.
*/
// ipcMain.on('get-app-menu', () => {
// if (mainWindow) {
// mainWindow.sendAppMenu();
// }
// });
// ipcMain.on(
// 'show-certificate-trust-dialog',
// (
// event: Electron.IpcMessageEvent,
// {
// certificate,
// message,
// }: { certificate: Electron.Certificate; message: string }
// ) => {
// // This API is only implemented for macOS and Windows right now.
// if (__DARWIN__ || __WIN32__) {
// onDidLoad(window => {
// window.showCertificateTrustDialog(certificate, message);
// });
// }
// }
// );
// ipcMain.on(
// 'log',
// (event: Electron.IpcMessageEvent, level: LogLevel, message: string) => {
// writeLog(level, message);
// }
// );
// ipcMain.on(
// 'uncaught-exception',
// (event: Electron.IpcMessageEvent, error: Error) => {
// handleUncaughtException(error);
// }
// );
// ipcMain.on(
// 'send-error-report',
// (
// event: Electron.IpcMessageEvent,
// { error, extra }: { error: Error; extra: { [key: string]: string } }
// ) => {
// reportError(error, extra);
// }
// );
// ipcMain.on(
// 'open-external',
// (event: Electron.IpcMessageEvent, { path }: { path: string }) => {
// const pathLowerCase = path.toLowerCase();
// if (
// pathLowerCase.startsWith('http://') ||
// pathLowerCase.startsWith('https://')
// ) {
// log.info(`opening in browser: ${path}`);
// }
// const result = shell.openExternal(path);
// event.sender.send('open-external-result', { result });
// }
// );
// ipcMain.on(
// 'show-item-in-folder',
// (event: Electron.IpcMessageEvent, { path }: { path: string }) => {
// Fs.stat(path, (err, stats) => {
// if (err) {
// log.error(`Unable to find file at '${path}'`, err);
// return;
// }
// if (stats.isDirectory()) {
// openDirectorySafe(path);
// } else {
// shell.showItemInFolder(path);
// }
// });
// }
// );
});
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
ipcMain.on('show-dialog', (event, arg) => {
dialog.showMessageBox(win, {
type: 'info',
buttons: ['OK'],
title: 'Native Dialog',
message: 'I\'m a native dialog!',
detail: 'It\'s my pleasure to make your life better.'
onDidLoad(window => {
window.show();
});
});
app.on('web-contents-created', (event, contents) => {
contents.on('new-window', (_event, url) => {
// Prevent links or window.open from opening new windows
_event.preventDefault();
log.warn(`Prevented new window to: ${url}`);
});
});
// app.on(
// 'certificate-error',
// (event, webContents, url, error, certificate, callback) => {
// callback(false);
// onDidLoad(window => {
// window.sendCertificateError(certificate, error, url);
// });
// }
// );
function createWindow() {
const window = new AppWindow();
if (__DEV__) {
const {
default: installExtension,
} = require('electron-devtools-installer');
require('electron-debug')({ showDevTools: true });
const ChromeLens = {
id: 'idikgljglpfilbhaboonnpnnincjhjkd',
electron: '>=1.2.1',
};
const extensions = [ChromeLens];
for (const extension of extensions) {
try {
installExtension(extension);
} catch (e) { }
}
}
window.onClose(() => {
mainWindow = null;
if (!__DARWIN__ && !preventQuit) {
app.quit();
}
});
window.onDidLoad(() => {
window.show();
// window.sendLaunchTimingStats({
// mainReadyTime: readyTime!,
// loadTime: window.loadTime!,
// rendererReadyTime: window.rendererReadyTime!,
// });
// const fns = onDidLoadFns!;
// onDidLoadFns = null;
// for (const fn of fns) {
// fn(window);
// }
});
window.load();
mainWindow = window;
}
/**
* Register a function to be called once the window has been loaded. If the
* window has already been loaded, the function will be called immediately.
*/
function onDidLoad(fn: OnDidLoadFn) {
if (onDidLoadFns) {
onDidLoadFns.push(fn);
} else {
if (mainWindow) {
fn(mainWindow);
}
}
}

View File

@@ -0,0 +1,2 @@
export * from './menu-event';
export * from './menu-ids';

View File

@@ -0,0 +1,4 @@
export type MenuEvent =
| 'show-about'
| 'open-external-editor'
| 'select-all';

View File

@@ -0,0 +1,5 @@
export type MenuIDs =
| 'preferences'
| 'open-in-shell'
| 'open-external-editor'
| 'about';

View File

@@ -1,11 +0,0 @@
/**
* Get the time from some arbitrary fixed starting point. The time will not be
* based on clock time.
*
* Ideally we'd just use `performance.now` but that's a browser API and not
* available in our Plain Old Node main process environment.
*/
export function now(): number {
const time = process.hrtime();
return time[0] * 1000 + time[1] / 1000000;
}

25
src/electron/shell.ts Normal file
View File

@@ -0,0 +1,25 @@
import * as Url from 'url';
import { shell } from 'electron';
/**
* Wraps the inbuilt shell.openItem path to address a focus issue that affects macOS.
*
* When opening a folder in Finder, the window will appear behind the application
* window, which may confuse users. As a workaround, we will fallback to using
* shell.openExternal for macOS until it can be fixed upstream.
*
* @param path directory to open
*/
export function openDirectorySafe(path: string) {
if (__DARWIN__) {
const directoryURL = Url.format({
pathname: path,
protocol: 'file:',
slashes: true,
});
shell.openExternal(directoryURL);
} else {
shell.openItem(path);
}
}

View File

@@ -0,0 +1,156 @@
import * as Path from 'path';
import * as Os from 'os';
import { pathExists, ensureDir, writeFile } from 'fs-extra';
import { spawn, getPathSegments, setPathSegments } from '@overflow/core/process/win32';
const appFolder = Path.resolve(process.execPath, '..');
const rootAppDir = Path.resolve(appFolder, '..');
const updateDotExe = Path.resolve(Path.join(rootAppDir, 'Update.exe'));
const exeName = Path.basename(process.execPath);
// A lot of this code was cargo-culted from our Atom comrades:
// https://github.com/atom/atom/blob/7c9f39e3f1d05ee423e0093e6b83f042ce11c90a/src/main-process/squirrel-update.coffee.
/**
* Handle Squirrel.Windows app lifecycle events.
*
* Returns a promise which will resolve when the work is done.
*/
export function handleSquirrelEvent(eventName: string): Promise<void> | null {
switch (eventName) {
case '--squirrel-install':
return handleInstalled();
case '--squirrel-updated':
return handleUpdated();
case '--squirrel-uninstall':
return handleUninstall();
case '--squirrel-obsolete':
return Promise.resolve();
}
return null;
}
async function handleInstalled(): Promise<void> {
await createShortcut(['StartMenu', 'Desktop']);
await installCLI();
}
async function handleUpdated(): Promise<void> {
await updateShortcut();
await installCLI();
}
async function installCLI(): Promise<void> {
const binPath = getBinPath();
await ensureDir(binPath);
await writeBatchScriptCLITrampoline(binPath);
await writeShellScriptCLITrampoline(binPath);
const paths = await getPathSegments();
if (paths.indexOf(binPath) < 0) {
await setPathSegments([...paths, binPath]);
}
}
/**
* Get the path for the `bin` directory which exists in our `AppData` but
* outside path which includes the installed app version.
*/
function getBinPath(): string {
return Path.resolve(process.execPath, '../../bin');
}
function resolveVersionedPath(binPath: string, relativePath: string): string {
const _appFolder = Path.resolve(process.execPath, '..');
return Path.relative(binPath, Path.join(_appFolder, relativePath));
}
/**
* Here's the problem: our app's path contains its version number. So each time
* we update, the path to our app changes. So it's Real Hard to add our path
* directly to `Path`. We'd have to detect and remove stale entries, etc.
*
* So instead, we write a trampoline out to a fixed path, still inside our
* `AppData` directory but outside the version-specific path. That trampoline
* just launches the current version's CLI tool. Then, whenever we update, we
* rewrite the trampoline to point to the new, version-specific path. Bingo
* bango Bob's your uncle.
*/
function writeBatchScriptCLITrampoline(binPath: string): Promise<void> {
const versionedPath = resolveVersionedPath(
binPath,
'resources/app/static/github.bat'
);
const trampoline = `@echo off\n"%~dp0\\${versionedPath}" %*`;
const trampolinePath = Path.join(binPath, 'github.bat');
return writeFile(trampolinePath, trampoline);
}
function writeShellScriptCLITrampoline(binPath: string): Promise<void> {
const versionedPath = resolveVersionedPath(
binPath,
'resources/app/static/github.sh'
);
const trampoline = `#!/usr/bin/env bash
DIR="$( cd "$( dirname "\$\{BASH_SOURCE[0]\}" )" && pwd )"
sh "$DIR/${versionedPath}" "$@"`;
const trampolinePath = Path.join(binPath, 'github');
return writeFile(trampolinePath, trampoline, { encoding: 'utf8', mode: 755 });
}
/** Spawn the Squirrel.Windows `Update.exe` with a command. */
async function spawnSquirrelUpdate(
commands: ReadonlyArray<string>
): Promise<void> {
await spawn(updateDotExe, commands);
}
type ShortcutLocations = ReadonlyArray<'StartMenu' | 'Desktop'>;
function createShortcut(locations: ShortcutLocations): Promise<void> {
return spawnSquirrelUpdate([
'--createShortcut',
exeName,
'-l',
locations.join(','),
]);
}
async function handleUninstall(): Promise<void> {
await removeShortcut();
const paths = await getPathSegments();
const binPath = getBinPath();
const pathsWithoutBinPath = paths.filter(p => p !== binPath);
return setPathSegments(pathsWithoutBinPath);
}
function removeShortcut(): Promise<void> {
return spawnSquirrelUpdate(['--removeShortcut', exeName]);
}
async function updateShortcut(): Promise<void> {
const homeDirectory = Os.homedir();
if (homeDirectory) {
const desktopShortcutPath = Path.join(
homeDirectory,
'Desktop',
'GitHub Desktop.lnk'
);
const exists = await pathExists(desktopShortcutPath);
const locations: ShortcutLocations = exists
? ['StartMenu', 'Desktop']
: ['StartMenu'];
return createShortcut(locations);
} else {
return createShortcut(['StartMenu', 'Desktop']);
}
}

256
src/globals.d.ts vendored Normal file
View File

@@ -0,0 +1,256 @@
/* eslint-disable typescript/interface-name-prefix */
/** Is the app running in dev mode? */
declare const __DEV__: boolean
/** The OAuth client id the app should use */
declare const __OAUTH_CLIENT_ID__: string | undefined
/** The OAuth secret the app should use. */
declare const __OAUTH_SECRET__: string | undefined
/** Is the app being built to run on Darwin? */
declare const __DARWIN__: boolean
/** Is the app being built to run on Win32? */
declare const __WIN32__: boolean
/** Is the app being built to run on Linux? */
declare const __LINUX__: boolean
/**
* The commit id of the repository HEAD at build time.
* Represented as a 40 character SHA-1 hexadecimal digest string.
*/
declare const __SHA__: string
/** The channel for which the release was created. */
declare const __RELEASE_CHANNEL__:
| 'production'
| 'beta'
| 'test'
| 'development'
declare const __CLI_COMMANDS__: ReadonlyArray<string>
/** The URL for Squirrel's updates. */
declare const __UPDATES_URL__: string
/**
* The currently executing process kind, this is specific to desktop
* and identifies the processes that we have.
*/
declare const __PROCESS_KIND__:
| 'main'
| 'ui'
| 'crash'
| 'askpass'
| 'highlighter'
/**
* The DOMHighResTimeStamp type is a double and is used to store a time value.
*
* The value could be a discrete point in time or the difference in time between
* two discrete points in time. The unit is milliseconds and should be accurate
* to 5 µs (microseconds). However, if the browser is unable to provide a time
* value accurate to 5 microseconds (due, for example, to hardware or software
* constraints), the browser can represent the value as a time in milliseconds
* accurate to a millisecond.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp
*/
declare type DOMHighResTimeStamp = number
/**
* The IdleDeadline interface is used as the data type of the input parameter to
* idle callbacks established by calling Window.requestIdleCallback(). It offers
* a method, timeRemaining(), which lets you determine how much longer the user
* agent estimates it will remain idle and a property, didTimeout, which lets
* you determine if your callback is executing because its timeout duration
* expired.
*
* https://developer.mozilla.org/en-US/docs/Web/API/IdleDeadline
*/
interface IdleDeadline {
readonly didTimeout: boolean
readonly timeRemaining: () => DOMHighResTimeStamp
}
/**
* Contains optional configuration parameters for the requestIdleCallback
* function.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
*/
interface IdleCallbackOptions {
/**
* If timeout is specified and has a positive value, and the callback has not
* already been called by the time timeout milliseconds have passed, the
* timeout will be called during the next idle period, even if doing so risks
* causing a negative performance impact..
*/
readonly timeout: number
}
/**
* The window.requestIdleCallback() method queues a function to be called during
* a browser's idle periods. This enables developers to perform background and
* low priority work on the main event loop, without impacting latency-critical
* events such as animation and input response. Functions are generally called
* in first-in-first-out order; however, callbacks which have a timeout
* specified may be called out-of-order if necessary in order to run them before
* the timeout elapses.
*
* See https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
*
* @param options Contains optional configuration parameters. Currently only one
* property is defined:
* timeout:
*/
declare function requestIdleCallback(
fn: (deadline: IdleDeadline) => void,
options?: IdleCallbackOptions
): number
interface IDesktopLogger {
/**
* Writes a log message at the 'error' level.
*
* The error will be persisted to disk as long as the disk transport is
* configured to pass along log messages at this level. For more details
* about the on-disk transport, see log.ts in the main process.
*
* If used from a renderer the log message will also be appended to the
* devtools console.
*
* @param message The text to write to the log file
* @param error An optional error instance that will be formatted to
* include the stack trace (if one is available) and
* then appended to the log message.
*/
error(message: string, error?: Error): void
/**
* Writes a log message at the 'warn' level.
*
* The error will be persisted to disk as long as the disk transport is
* configured to pass along log messages at this level. For more details
* about the on-disk transport, see log.ts in the main process.
*
* If used from a renderer the log message will also be appended to the
* devtools console.
*
* @param message The text to write to the log file
* @param error An optional error instance that will be formatted to
* include the stack trace (if one is available) and
* then appended to the log message.
*/
warn(message: string, error?: Error): void
/**
* Writes a log message at the 'info' level.
*
* The error will be persisted to disk as long as the disk transport is
* configured to pass along log messages at this level. For more details
* about the on-disk transport, see log.ts in the main process.
*
* If used from a renderer the log message will also be appended to the
* devtools console.
*
* @param message The text to write to the log file
* @param error An optional error instance that will be formatted to
* include the stack trace (if one is available) and
* then appended to the log message.
*/
info(message: string, error?: Error): void
/**
* Writes a log message at the 'debug' level.
*
* The error will be persisted to disk as long as the disk transport is
* configured to pass along log messages at this level. For more details
* about the on-disk transport, see log.ts in the main process.
*
* If used from a renderer the log message will also be appended to the
* devtools console.
*
* @param message The text to write to the log file
* @param error An optional error instance that will be formatted to
* include the stack trace (if one is available) and
* then appended to the log message.
*/
debug(message: string, error?: Error): void
}
declare const log: IDesktopLogger
// these changes should be pushed into the Electron declarations
declare namespace NodeJS {
interface Process extends EventEmitter {
once(event: 'uncaughtException', listener: (error: Error) => void): this
on(event: 'uncaughtException', listener: (error: Error) => void): this
removeListener(event: 'exit', listener: Function): this
once(event: 'exit', listener: Function): this
}
}
declare namespace Electron {
interface MenuItem {
readonly accelerator?: Electron.Accelerator
readonly submenu?: Electron.Menu
readonly role?: string
readonly type: 'normal' | 'separator' | 'submenu' | 'checkbox' | 'radio'
}
interface RequestOptions {
readonly method: string
readonly url: string
readonly headers: any
}
type AppleActionOnDoubleClickPref = 'Maximize' | 'Minimize' | 'None'
interface SystemPreferences {
getUserDefault(
key: 'AppleActionOnDoubleClick',
type: 'string'
): AppleActionOnDoubleClickPref
}
interface WebviewTag extends HTMLElement {
// Copied from https://github.com/electron/electron-typescript-definitions/pull/81
// until we can upgrade to a version of Electron which includes the fix.
addEventListener<K extends keyof HTMLElementEventMap>(
type: K,
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
useCapture?: boolean
): void
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
useCapture?: boolean
): void
removeEventListener<K extends keyof HTMLElementEventMap>(
type: K,
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any,
useCapture?: boolean
): void
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
useCapture?: boolean
): void
}
}
// https://wicg.github.io/ResizeObserver/#resizeobserverentry
interface IResizeObserverEntry {
readonly target: HTMLElement
readonly contentRect: ClientRect
}
declare class ResizeObserver {
public constructor(cb: (entries: ReadonlyArray<IResizeObserverEntry>) => void)
public disconnect(): void
public observe(e: HTMLElement): void
}