ing
This commit is contained in:
21
@overflow/core/menu-item.ts
Normal file
21
@overflow/core/menu-item.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface IMenuItem {
|
||||
/** The user-facing label. */
|
||||
readonly label?: string;
|
||||
|
||||
/** The action to invoke when the user selects the item. */
|
||||
readonly action?: () => void;
|
||||
|
||||
/** The type of item. */
|
||||
readonly type?: 'separator';
|
||||
|
||||
/** Is the menu item enabled? Defaults to true. */
|
||||
readonly enabled?: boolean;
|
||||
|
||||
/**
|
||||
* The predefined behavior of the menu item.
|
||||
*
|
||||
* When specified the click property will be ignored.
|
||||
* See https://electronjs.org/docs/api/menu-item#roles
|
||||
*/
|
||||
readonly role?: string;
|
||||
}
|
||||
11
@overflow/core/now.ts
Normal file
11
@overflow/core/now.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
145
@overflow/core/parse-app-url.ts
Normal file
145
@overflow/core/parse-app-url.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import * as URL from 'url';
|
||||
|
||||
export interface IOAuthAction {
|
||||
readonly name: 'oauth';
|
||||
readonly code: string;
|
||||
}
|
||||
|
||||
export interface IOpenRepositoryFromURLAction {
|
||||
readonly name: 'open-repository-from-url';
|
||||
|
||||
/** the remote repository location associated with the "Open in Desktop" action */
|
||||
readonly url: string;
|
||||
|
||||
/** the optional branch name which should be checked out. use the default branch otherwise. */
|
||||
readonly branch: string | null;
|
||||
|
||||
/** the pull request number, if pull request originates from a fork of the repository */
|
||||
readonly pr: string | null;
|
||||
|
||||
/** the file to open after cloning the repository */
|
||||
readonly filepath: string | null;
|
||||
}
|
||||
|
||||
export interface IOpenRepositoryFromPathAction {
|
||||
readonly name: 'open-repository-from-path';
|
||||
|
||||
/** The local path to open. */
|
||||
readonly path: string;
|
||||
}
|
||||
|
||||
export interface IUnknownAction {
|
||||
readonly name: 'unknown';
|
||||
readonly url: string;
|
||||
}
|
||||
|
||||
export type URLActionType =
|
||||
| IOAuthAction
|
||||
| IOpenRepositoryFromURLAction
|
||||
| IOpenRepositoryFromPathAction
|
||||
| IUnknownAction;
|
||||
|
||||
// eslint-disable-next-line typescript/interface-name-prefix
|
||||
interface ParsedUrlQueryWithUndefined {
|
||||
// `undefined` is added here to ensure we handle the missing querystring key
|
||||
// See https://github.com/Microsoft/TypeScript/issues/13778 for discussion about
|
||||
// why this isn't supported natively in TypeScript
|
||||
[key: string]: string | string[] | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the URL to find a given key in the querystring text.
|
||||
*
|
||||
* @param url The source URL containing querystring key-value pairs
|
||||
* @param key The key to look for in the querystring
|
||||
*/
|
||||
function getQueryStringValue(
|
||||
query: ParsedUrlQueryWithUndefined,
|
||||
key: string
|
||||
): string | null {
|
||||
const value = query[key];
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value[0];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseAppURL(url: string): URLActionType {
|
||||
const parsedURL = URL.parse(url, true);
|
||||
const hostname = parsedURL.hostname;
|
||||
const unknown: IUnknownAction = { name: 'unknown', url };
|
||||
if (!hostname) {
|
||||
return unknown;
|
||||
}
|
||||
|
||||
const query = parsedURL.query;
|
||||
|
||||
const actionName = hostname.toLowerCase();
|
||||
if (actionName === 'oauth') {
|
||||
const code = getQueryStringValue(query, 'code');
|
||||
if (code != null) {
|
||||
return { name: 'oauth', code };
|
||||
} else {
|
||||
return unknown;
|
||||
}
|
||||
}
|
||||
|
||||
// we require something resembling a URL first
|
||||
// - bail out if it's not defined
|
||||
// - bail out if you only have `/`
|
||||
const pathName = parsedURL.pathname;
|
||||
if (!pathName || pathName.length <= 1) {
|
||||
return unknown;
|
||||
}
|
||||
|
||||
// Trim the trailing / from the URL
|
||||
const parsedPath = pathName.substr(1);
|
||||
|
||||
if (actionName === 'openrepo') {
|
||||
const probablyAURL = parsedPath;
|
||||
|
||||
// suffix the remote URL with `.git`, for backwards compatibility
|
||||
const _url = `${probablyAURL}.git`;
|
||||
|
||||
const pr = getQueryStringValue(query, 'pr');
|
||||
const branch = getQueryStringValue(query, 'branch');
|
||||
const filepath = getQueryStringValue(query, 'filepath');
|
||||
|
||||
if (pr != null) {
|
||||
if (!/^\d+$/.test(pr)) {
|
||||
return unknown;
|
||||
}
|
||||
|
||||
// we also expect the branch for a forked PR to be a given ref format
|
||||
if (branch != null && !/^pr\/\d+$/.test(branch)) {
|
||||
return unknown;
|
||||
}
|
||||
}
|
||||
|
||||
// if (branch != null && testForInvalidChars(branch)) {
|
||||
// return unknown;
|
||||
// }
|
||||
|
||||
return {
|
||||
name: 'open-repository-from-url',
|
||||
url: _url,
|
||||
branch,
|
||||
pr,
|
||||
filepath,
|
||||
};
|
||||
}
|
||||
|
||||
if (actionName === 'openlocalrepo') {
|
||||
return {
|
||||
name: 'open-repository-from-path',
|
||||
path: decodeURIComponent(parsedPath),
|
||||
};
|
||||
}
|
||||
|
||||
return unknown;
|
||||
}
|
||||
12
@overflow/core/path.ts
Normal file
12
@overflow/core/path.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import * as Path from 'path';
|
||||
import fileUrl = require('file-url');
|
||||
|
||||
/**
|
||||
* Resolve and encode the path information into a URL.
|
||||
*
|
||||
* @param pathSegments array of path segments to resolve
|
||||
*/
|
||||
export function encodePathAsUrl(...pathSegments: string[]): string {
|
||||
const path = Path.resolve(...pathSegments);
|
||||
return fileUrl(path);
|
||||
}
|
||||
90
@overflow/core/process/win32.ts
Normal file
90
@overflow/core/process/win32.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { spawn as spawnInternal } from 'child_process';
|
||||
import * as Path from 'path';
|
||||
|
||||
/** Get the path segments in the user's `Path`. */
|
||||
export async function getPathSegments(): Promise<ReadonlyArray<string>> {
|
||||
let powershellPath: string;
|
||||
const systemRoot = process.env.SystemRoot;
|
||||
if (systemRoot != null) {
|
||||
const system32Path = Path.join(systemRoot, 'System32');
|
||||
powershellPath = Path.join(
|
||||
system32Path,
|
||||
'WindowsPowerShell',
|
||||
'v1.0',
|
||||
'powershell.exe'
|
||||
);
|
||||
} else {
|
||||
powershellPath = 'powershell.exe';
|
||||
}
|
||||
|
||||
const args = [
|
||||
'-noprofile',
|
||||
'-ExecutionPolicy',
|
||||
'RemoteSigned',
|
||||
'-command',
|
||||
// Set encoding and execute the command, capture the output, and return it
|
||||
// via .NET's console in order to have consistent UTF-8 encoding.
|
||||
// See http://stackoverflow.com/questions/22349139/utf-8-output-from-powershell
|
||||
// to address https://github.com/atom/atom/issues/5063
|
||||
`
|
||||
[Console]::OutputEncoding=[System.Text.Encoding]::UTF8
|
||||
$output=[environment]::GetEnvironmentVariable('Path', 'User')
|
||||
[Console]::WriteLine($output)
|
||||
`,
|
||||
];
|
||||
|
||||
const stdout = await spawn(powershellPath, args);
|
||||
const pathOutput = stdout.replace(/^\s+|\s+$/g, '');
|
||||
return pathOutput.split(/;+/).filter(segment => segment.length);
|
||||
}
|
||||
|
||||
/** Set the user's `Path`. */
|
||||
export async function setPathSegments(
|
||||
paths: ReadonlyArray<string>
|
||||
): Promise<void> {
|
||||
let setxPath: string;
|
||||
const systemRoot = process.env['SystemRoot'];
|
||||
if (systemRoot) {
|
||||
const system32Path = Path.join(systemRoot, 'System32');
|
||||
setxPath = Path.join(system32Path, 'setx.exe');
|
||||
} else {
|
||||
setxPath = 'setx.exe';
|
||||
}
|
||||
|
||||
await spawn(setxPath, ['Path', paths.join(';')]);
|
||||
}
|
||||
|
||||
/** Spawn a command with arguments and capture its output. */
|
||||
export function spawn(
|
||||
command: string,
|
||||
args: ReadonlyArray<string>
|
||||
): Promise<string> {
|
||||
try {
|
||||
const child = spawnInternal(command, args as string[]);
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
let stdout = '';
|
||||
child.stdout.on('data', data => {
|
||||
stdout += data;
|
||||
});
|
||||
|
||||
child.on('close', code => {
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
} else {
|
||||
reject(new Error(`Command "${command} ${args}" failed: "${stdout}"`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err: Error) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// This is necessary if using Powershell 2 on Windows 7 to get the events
|
||||
// to raise.
|
||||
// See http://stackoverflow.com/questions/9155289/calling-powershell-from-nodejs
|
||||
child.stdin.end();
|
||||
});
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
167
@overflow/core/shell.ts
Normal file
167
@overflow/core/shell.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
/* eslint-disable no-sync */
|
||||
|
||||
import * as ChildProcess from 'child_process';
|
||||
import * as os from 'os';
|
||||
|
||||
interface IndexLookup {
|
||||
[propName: string]: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The names of any env vars that we shouldn't copy from the shell environment.
|
||||
*/
|
||||
const BlacklistedNames = new Set(['LOCAL_GIT_DIRECTORY']);
|
||||
|
||||
/**
|
||||
* Inspect whether the current process needs to be patched to get important
|
||||
* environment variables for Desktop to work and integrate with other tools
|
||||
* the user may invoke as part of their workflow.
|
||||
*
|
||||
* This is only applied to macOS installations due to how the application
|
||||
* is launched.
|
||||
*
|
||||
* @param process The process to inspect.
|
||||
*/
|
||||
export function shellNeedsPatching(process: NodeJS.Process): boolean {
|
||||
return __DARWIN__ && !process.env.PWD;
|
||||
}
|
||||
|
||||
interface ShellResult {
|
||||
stdout: string;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a dump of the user's configured shell environment.
|
||||
*
|
||||
* @returns the output of the `env` command or `null` if there was an error.
|
||||
*/
|
||||
async function getRawShellEnv(): Promise<string | null> {
|
||||
const shell = getUserShell();
|
||||
|
||||
const promise = new Promise<ShellResult>(resolve => {
|
||||
let child: ChildProcess.ChildProcess | null = null;
|
||||
let _error: Error | null = null;
|
||||
let _stdout = '';
|
||||
let done = false;
|
||||
|
||||
// ensure we clean up eventually, in case things go bad
|
||||
const cleanup = () => {
|
||||
if (!done && child) {
|
||||
child.kill();
|
||||
done = true;
|
||||
}
|
||||
};
|
||||
process.once('exit', cleanup);
|
||||
setTimeout(() => {
|
||||
cleanup();
|
||||
}, 5000);
|
||||
|
||||
const options = {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'pipe', process.stderr],
|
||||
};
|
||||
|
||||
child = ChildProcess.spawn(shell, ['-ilc', 'command env'], options);
|
||||
|
||||
const buffers: Array<Buffer> = [];
|
||||
|
||||
child.on('error', (e: Error) => {
|
||||
done = true;
|
||||
_error = e;
|
||||
});
|
||||
|
||||
child.stdout.on('data', (data: Buffer) => {
|
||||
buffers.push(data);
|
||||
});
|
||||
|
||||
child.on('close', (code: number, signal) => {
|
||||
done = true;
|
||||
process.removeListener('exit', cleanup);
|
||||
if (buffers.length) {
|
||||
_stdout = Buffer.concat(buffers).toString('utf8');
|
||||
}
|
||||
|
||||
resolve({ stdout: _stdout, error });
|
||||
});
|
||||
});
|
||||
|
||||
const { stdout, error } = await promise;
|
||||
|
||||
if (error) {
|
||||
// just swallow the error and move on with everything
|
||||
return null;
|
||||
}
|
||||
|
||||
return stdout;
|
||||
}
|
||||
|
||||
function getUserShell() {
|
||||
if (process.env.SHELL) {
|
||||
return process.env.SHELL;
|
||||
}
|
||||
|
||||
return '/bin/bash';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the environment variables from the user's current shell and update the
|
||||
* current environment.
|
||||
*
|
||||
* @param updateEnvironment a callback to fire if a valid environment is found
|
||||
*/
|
||||
async function getEnvironmentFromShell(
|
||||
updateEnvironment: (env: IndexLookup) => void
|
||||
): Promise<void> {
|
||||
if (__WIN32__) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shellEnvText = await getRawShellEnv();
|
||||
if (!shellEnvText) {
|
||||
return;
|
||||
}
|
||||
|
||||
const env: IndexLookup = {};
|
||||
|
||||
for (const line of shellEnvText.split(os.EOL)) {
|
||||
if (line.includes('=')) {
|
||||
const components = line.split('=');
|
||||
if (components.length === 2) {
|
||||
env[components[0]] = components[1];
|
||||
} else {
|
||||
const k = components.shift();
|
||||
const v = components.join('=');
|
||||
if (k) {
|
||||
env[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateEnvironment(env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply new environment variables to the current process, ignoring
|
||||
* Node-specific environment variables which need to be preserved.
|
||||
*
|
||||
* @param env The new environment variables from the user's shell.
|
||||
*/
|
||||
function mergeEnvironmentVariables(env: IndexLookup) {
|
||||
for (const key in env) {
|
||||
if (BlacklistedNames.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
process.env[key] = env[key];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current process's environment variables using environment
|
||||
* variables from the user's shell, if they can be retrieved successfully.
|
||||
*/
|
||||
export function updateEnvironmentForProcess(): Promise<void> {
|
||||
return getEnvironmentFromShell(mergeEnvironmentVariables);
|
||||
}
|
||||
137
@overflow/core/source-map-support.ts
Normal file
137
@overflow/core/source-map-support.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import * as Path from 'path';
|
||||
import * as Fs from 'fs';
|
||||
|
||||
const fileUriToPath: (uri: string) => string = require('file-uri-to-path');
|
||||
const sourceMapSupport = require('source-map-support');
|
||||
|
||||
/**
|
||||
* This array tells the source map logic which files that we can expect to
|
||||
* be able to resolve a source map for and they should reflect the chunks
|
||||
* entry names from our webpack config.
|
||||
*
|
||||
* Note that we explicitly don't enable source maps for the crash process
|
||||
* since it's possible that the error which caused us to spawn the crash
|
||||
* process was related to source maps.
|
||||
*/
|
||||
const knownFilesWithSourceMap = ['renderer.js', 'main.js'];
|
||||
|
||||
function retrieveSourceMap(source: string) {
|
||||
// This is a happy path in case we know for certain that we won't be
|
||||
// able to resolve a source map for the given location.
|
||||
if (!knownFilesWithSourceMap.some(file => source.endsWith(file))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We get a file uri when we're inside a renderer, convert to a path
|
||||
if (source.startsWith('file://')) {
|
||||
source = fileUriToPath(source);
|
||||
}
|
||||
|
||||
// We store our source maps right next to the bundle
|
||||
const path = `${source}.map`;
|
||||
|
||||
if (__DEV__ && path.startsWith('http://')) {
|
||||
try {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', path, false);
|
||||
xhr.send(null);
|
||||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||||
return { url: Path.basename(path), map: xhr.responseText };
|
||||
}
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// We don't have an option here, see
|
||||
// https://github.com/v8/v8/wiki/Stack-Trace-API#customizing-stack-traces
|
||||
// This happens on-demand when someone accesses the stack
|
||||
// property on an error object and has to be synchronous :/
|
||||
// eslint-disable-next-line no-sync
|
||||
if (!Fs.existsSync(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line no-sync
|
||||
const map = Fs.readFileSync(path, 'utf8');
|
||||
return { url: Path.basename(path), map };
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** A map from errors to their stack frames. */
|
||||
const stackFrameMap = new WeakMap<Error, ReadonlyArray<any>>();
|
||||
|
||||
/**
|
||||
* The `prepareStackTrace` that comes from the `source-map-support` module.
|
||||
* We'll use this when the user explicitly wants the stack source mapped.
|
||||
*/
|
||||
let prepareStackTraceWithSourceMap: (
|
||||
error: Error,
|
||||
frames: ReadonlyArray<any>
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* Capture the error's stack frames and return a standard, un-source mapped
|
||||
* stack trace.
|
||||
*/
|
||||
function prepareStackTrace(error: Error, frames: ReadonlyArray<any>) {
|
||||
stackFrameMap.set(error, frames);
|
||||
|
||||
// Ideally we'd use the default `Error.prepareStackTrace` here but it's
|
||||
// undefined so V8 must doing something fancy. Instead we'll do a decent
|
||||
// impression.
|
||||
return error + frames.map(frame => `\n at ${frame}`).join('');
|
||||
}
|
||||
|
||||
/** Enable source map support in the current process. */
|
||||
export function enableSourceMaps() {
|
||||
sourceMapSupport.install({
|
||||
environment: 'node',
|
||||
handleUncaughtExceptions: false,
|
||||
retrieveSourceMap,
|
||||
});
|
||||
|
||||
const AnyError = Error as any;
|
||||
// We want to keep `source-map-support`s `prepareStackTrace` around to use
|
||||
// later, but our cheaper `prepareStackTrace` should be the default.
|
||||
prepareStackTraceWithSourceMap = AnyError.prepareStackTrace;
|
||||
AnyError.prepareStackTrace = prepareStackTrace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a copy of the error with a source-mapped stack trace. If it couldn't
|
||||
* perform the source mapping, it'll use the original error stack.
|
||||
*/
|
||||
export function withSourceMappedStack(error: Error): Error {
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: sourceMappedStackTrace(error),
|
||||
};
|
||||
}
|
||||
|
||||
/** Get the source mapped stack trace for the error. */
|
||||
function sourceMappedStackTrace(error: Error): string | undefined {
|
||||
let frames = stackFrameMap.get(error);
|
||||
|
||||
if (!frames) {
|
||||
// At this point there's no guarantee that anyone has actually retrieved the
|
||||
// stack on this error which means that our custom prepareStackTrace handler
|
||||
// hasn't run and as a result of that we don't have the native frames stored
|
||||
// in our weak map. In order to get around that we'll eagerly access the
|
||||
// stack, forcing our handler to run which should ensure that the native
|
||||
// frames are stored in our weak map.
|
||||
(error.stack || '').toString();
|
||||
frames = stackFrameMap.get(error);
|
||||
}
|
||||
|
||||
if (!frames) {
|
||||
return error.stack;
|
||||
}
|
||||
|
||||
return prepareStackTraceWithSourceMap(error, frames);
|
||||
}
|
||||
60
@overflow/core/window-state.ts
Normal file
60
@overflow/core/window-state.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
// The name of the ipc channel over which state changes are communicated.
|
||||
export const windowStateChannelName = 'window-state-changed';
|
||||
|
||||
export type WindowState =
|
||||
| 'minimized'
|
||||
| 'normal'
|
||||
| 'maximized'
|
||||
| 'full-screen'
|
||||
| 'hidden';
|
||||
|
||||
export function getWindowState(window: Electron.BrowserWindow): WindowState {
|
||||
if (window.isFullScreen()) {
|
||||
return 'full-screen';
|
||||
} else if (window.isMaximized()) {
|
||||
return 'maximized';
|
||||
} else if (window.isMinimized()) {
|
||||
return 'minimized';
|
||||
} else if (!window.isVisible()) {
|
||||
return 'hidden';
|
||||
} else {
|
||||
return 'normal';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers event handlers for all window state transition events and
|
||||
* forwards those to the renderer process for a given window.
|
||||
*/
|
||||
export function registerWindowStateChangedEvents(
|
||||
window: Electron.BrowserWindow
|
||||
) {
|
||||
window.on('enter-full-screen', () =>
|
||||
sendWindowStateEvent(window, 'full-screen')
|
||||
);
|
||||
|
||||
// So this is a bit of a hack. If we call window.isFullScreen directly after
|
||||
// receiving the leave-full-screen event it'll return true which isn't what
|
||||
// we're after. So we'll say that we're transitioning to 'normal' even though
|
||||
// we might be maximized. This works because electron will emit a 'maximized'
|
||||
// event after 'leave-full-screen' if the state prior to full-screen was maximized.
|
||||
window.on('leave-full-screen', () => sendWindowStateEvent(window, 'normal'));
|
||||
|
||||
window.on('maximize', () => sendWindowStateEvent(window, 'maximized'));
|
||||
window.on('minimize', () => sendWindowStateEvent(window, 'minimized'));
|
||||
window.on('unmaximize', () => sendWindowStateEvent(window, 'normal'));
|
||||
window.on('restore', () => sendWindowStateEvent(window, 'normal'));
|
||||
window.on('hide', () => sendWindowStateEvent(window, 'hidden'));
|
||||
window.on('show', () => sendWindowStateEvent(window, 'normal'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Short hand convenience function for sending a window state change event
|
||||
* over the window-state-changed channel to the render process.
|
||||
*/
|
||||
function sendWindowStateEvent(
|
||||
window: Electron.BrowserWindow,
|
||||
state: WindowState
|
||||
) {
|
||||
window.webContents.send(windowStateChannelName, state);
|
||||
}
|
||||
Reference in New Issue
Block a user