require('hazardous'); const path = require('path'); const { BrowserWindow, ipcMain, app, clipboard, Tray, Menu, dialog, Notification, nativeImage } = require('electron'); const { spawn, spawnSync } = require('child_process'); const fs = require('fs'); const https = require('https'); //app.disableHardwareAcceleration(); const bundledDotNetPath = path.join(process.resourcesPath, 'dotnet-runtime'); if (fs.existsSync(bundledDotNetPath)) { // Include bundled .NET runtime process.env.DOTNET_ROOT = bundledDotNetPath; process.env.PATH = `${bundledDotNetPath}:${process.env.PATH}`; } else if (process.platform === 'darwin') { const dotnetPath = path.join('/usr/local/share/dotnet'); const dotnetPathArm = path.join('/usr/local/share/dotnet/x64'); if (fs.existsSync(dotnetPathArm)) { process.env.DOTNET_ROOT = dotnetPathArm; process.env.PATH = `${dotnetPathArm}:${process.env.PATH}`; } else if (fs.existsSync(dotnetPath)) { process.env.DOTNET_ROOT = dotnetPath; process.env.PATH = `${dotnetPath}:${process.env.PATH}`; } } if (!isDotNetInstalled()) { app.whenReady().then(() => { dialog.showErrorBox( 'VRCX', 'Please install .NET 9.0 Runtime "dotnet-runtime-9.0" to run VRCX.' ); app.quit(); }); } const VRCX_URI_PREFIX = 'vrcx'; let isOverlayActive = false; let appIsQuitting = false; const rootDir = app.getAppPath(); let tray = null; let trayIcon = null; let trayIconNotify = null; // Get launch arguments let appImagePath = process.env.APPIMAGE; const args = process.argv.slice(1); const noInstall = args.includes('--no-install'); const x11 = args.includes('--x11'); const noDesktop = args.includes('--no-desktop'); const startup = args.includes('--startup'); const debug = args.includes('--hot-reload'); const noUpdater = args.includes('--no-updater') || fs.existsSync(path.join(rootDir, '.no-updater')); if (process.defaultApp && process.platform !== 'win32') { if (process.argv.length >= 2) { app.setAsDefaultProtocolClient(VRCX_URI_PREFIX, process.execPath, [ path.resolve(process.argv[1]) ]); } else { app.setAsDefaultProtocolClient(VRCX_URI_PREFIX); } } const version = getVersion(); const homePath = getHomePath(); tryRelaunchWithArgs(args); tryCopyFromWinePrefix(); const userDataPath = getElectronUserDataPath(); console.log('Electron userData path:', userDataPath); if (!fs.existsSync(userDataPath)) { fs.mkdirSync(userDataPath, { recursive: true }); } app.setPath('userData', userDataPath); const armPath = path.join(rootDir, 'build/Electron/VRCX-Electron-arm64.cjs'); if (process.arch === 'arm64' && fs.existsSync(armPath)) { require(armPath); } else { require(path.join(rootDir, 'build/Electron/VRCX-Electron.cjs')); } const InteropApi = require('./InteropApi'); const interopApi = new InteropApi(); const OVERLAY_WRIST_FRAME_WIDTH = 512; const OVERLAY_WRIST_FRAME_HEIGHT = 512; const OVERLAY_HMD_FRAME_WIDTH = 1024; const OVERLAY_HMD_FRAME_HEIGHT = 1024; const OVERLAY_SHARED_HEIGHT = OVERLAY_WRIST_FRAME_HEIGHT + OVERLAY_HMD_FRAME_HEIGHT; const OVERLAY_SHARED_WIDTH = Math.max( OVERLAY_WRIST_FRAME_WIDTH, OVERLAY_HMD_FRAME_WIDTH ); const OVERLAY_FRAME_SIZE = OVERLAY_SHARED_WIDTH * OVERLAY_SHARED_HEIGHT * 4; const OVERLAY_SHM_PATH = '/dev/shm/vrcx_overlay'; function createOverlayWindowShm() { fs.writeFileSync(OVERLAY_SHM_PATH, Buffer.alloc(OVERLAY_FRAME_SIZE + 1)); } interopApi.getDotNetObject('ProgramElectron').PreInit(version, args); interopApi.getDotNetObject('VRCXStorage').Load(); interopApi.getDotNetObject('ProgramElectron').Init(); interopApi.getDotNetObject('SQLite').Init(); interopApi.getDotNetObject('AppApiElectron').Init(); interopApi.getDotNetObject('Discord').Init(); interopApi.getDotNetObject('WebApi').Init(); interopApi.getDotNetObject('LogWatcher').Init(); interopApi.getDotNetObject('SystemMonitorElectron').Init(); interopApi.getDotNetObject('AppApiVrElectron').Init(); ipcMain.handle('callDotNetMethod', (event, className, methodName, args) => { return interopApi.callMethod(className, methodName, args); }); /** @type {BrowserWindow} */ let mainWindow = undefined; const VRCXStorage = interopApi.getDotNetObject('VRCXStorage'); const hasAskedToMoveAppImage = VRCXStorage.Get('VRCX_HasAskedToMoveAppImage') === 'true'; function getCloseToTray() { if (process.platform === 'darwin') { return true; } return VRCXStorage.Get('VRCX_CloseToTray') === 'true'; } const gotTheLock = app.requestSingleInstanceLock(); const strip_vrcx_prefix_regex = new RegExp('^' + VRCX_URI_PREFIX + '://'); if (!gotTheLock) { console.log('Another instance is already running. Exiting.'); app.quit(); } else { app.on('second-instance', (event, commandLine, workingDirectory) => { if (mainWindow && commandLine.length >= 2) { try { mainWindow.webContents.send( 'launch-command', commandLine .pop() .trim() .replace(strip_vrcx_prefix_regex, '') ); } catch (err) { console.error('Error processing second-instance command:', err); } } }); app.on('open-url', (event, url) => { if (mainWindow && url) { mainWindow.webContents.send( 'launch-command', url.replace(strip_vrcx_prefix_regex, '') ); } }); } ipcMain.handle('dialog:openFile', async () => { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openFile'], filters: [{ name: 'Images', extensions: ['png'] }] }); if (!result.canceled && result.filePaths.length > 0) { return result.filePaths[0]; } return null; }); ipcMain.handle('dialog:openDirectory', async () => { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory'] }); if (!result.canceled && result.filePaths.length > 0) { return result.filePaths[0]; } return null; }); ipcMain.handle('notification:showNotification', (event, title, body, icon) => { const notification = { title, body, icon }; new Notification(notification).show(); }); ipcMain.handle('app:restart', () => { if (process.platform === 'linux') { const options = { execPath: process.execPath, args: process.argv.slice(1) }; if (appImagePath) { options.execPath = appImagePath; if (!x11 && !options.args.includes('--appimage-extract-and-run')) { options.args.unshift('--appimage-extract-and-run'); } } app.relaunch(options); destroyTray(); app.exit(0); } else { app.relaunch(); app.quit(); } }); ipcMain.handle('app:getOverlayWindow', () => { if (overlayWindow && overlayWindow.webContents) { return ( !overlayWindow.webContents.isLoading() && overlayWindow.webContents.isPainting() ); } return false; }); ipcMain.handle( 'app:updateVr', (event, active, hmdOverlay, wristOverlay, menuButton, overlayHand) => { if (!active || (!hmdOverlay && !wristOverlay)) { disposeOverlay(); return; } if (active && !overlayWindow) { try { createOverlayWindowOffscreen(); } catch (err) { console.error('Error creating overlay windows:', err); } } } ); ipcMain.handle('app:getArch', () => { return process.arch.toString(); }); ipcMain.handle('app:getClipboardText', () => { return clipboard.readText(); }); ipcMain.handle('app:getNoUpdater', () => { return noUpdater; }); ipcMain.handle('app:setTrayIconNotification', (event, notify) => { setTrayIconNotification(notify); }); function tryRelaunchWithArgs(args) { if ( process.platform !== 'linux' || x11 || args.includes('--ozone-platform-hint=auto') ) { return; } const fullArgs = ['--ozone-platform-hint=auto', ...args]; let execPath = process.execPath; if (appImagePath) { execPath = appImagePath; fullArgs.unshift('--appimage-extract-and-run'); } console.log('Relaunching with args:', fullArgs); const child = spawn(execPath, fullArgs, { detached: true, stdio: 'inherit' }); child.unref(); destroyTray(); app.exit(0); } function createWindow() { app.commandLine.appendSwitch('enable-speech-dispatcher'); const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0; const y = parseInt(VRCXStorage.Get('VRCX_LocationY')) || 0; const width = parseInt(VRCXStorage.Get('VRCX_SizeWidth')) || 1920; const height = parseInt(VRCXStorage.Get('VRCX_SizeHeight')) || 1080; const zoomLevel = parseFloat(VRCXStorage.Get('VRCX_ZoomLevel')) || 0; mainWindow = new BrowserWindow({ x, y, width, height, icon: path.join(rootDir, 'images/VRCX.png'), autoHideMenuBar: true, titleBarStyle: 'hiddenInset', webPreferences: { preload: path.join(__dirname, 'preload.js') } }); applyWindowState(); const indexPath = path.join(rootDir, 'build/html/index.html'); mainWindow.loadFile(indexPath); if (debug) { mainWindow.loadURL('http://localhost:9000/index.html'); mainWindow.webContents.openDevTools(); } // add proxy config, doesn't work, thanks electron // const proxy = VRCXStorage.Get('VRCX_Proxy'); // if (proxy) { // session.setProxy( // { proxyRules: proxy.replaceAll('://', '=') }, // function () { // mainWindow.loadFile(indexPath); // } // ); // session.setProxy({ // proxyRules: proxy.replaceAll('://', '=') // }); // } // Open the DevTools. // mainWindow.webContents.openDevTools() mainWindow.webContents.on('did-finish-load', () => { mainWindow.webContents.setZoomLevel(zoomLevel); }); mainWindow.webContents.on('before-input-event', (event, input) => { if (input.control && input.key === '=') { mainWindow.webContents.setZoomLevel( mainWindow.webContents.getZoomLevel() + 1 ); } if (input.control && input.key === '-') { mainWindow.webContents.setZoomLevel( mainWindow.webContents.getZoomLevel() - 1 ); } }); mainWindow.webContents.on('zoom-changed', (event, zoomDirection) => { let currentZoom = mainWindow.webContents.getZoomLevel(); if (zoomDirection === 'in') { mainWindow.webContents.setZoomLevel(++currentZoom); } else { mainWindow.webContents.setZoomLevel(--currentZoom); } VRCXStorage.Set('VRCX_ZoomLevel', currentZoom.toString()); }); mainWindow.webContents.setVisualZoomLevelLimits(1, 5); mainWindow.on('close', (event) => { if (getCloseToTray() && !appIsQuitting) { event.preventDefault(); mainWindow.hide(); } else { app.quit(); } }); mainWindow.on('resize', () => { const [width, height] = mainWindow .getSize() .map((size) => size.toString()); mainWindow.webContents.send('setWindowSize', { width, height }); }); mainWindow.on('move', () => { const [x, y] = mainWindow .getPosition() .map((coord) => coord.toString()); mainWindow.webContents.send('setWindowPosition', { x, y }); }); mainWindow.on('maximize', () => { mainWindow.webContents.send('setWindowState', '2'); }); mainWindow.on('minimize', () => { mainWindow.webContents.send('setWindowState', '1'); }); mainWindow.on('unmaximize', () => { mainWindow.webContents.send('setWindowState', '0'); }); mainWindow.on('restore', () => { mainWindow.webContents.send('setWindowState', '0'); }); mainWindow.on('focus', () => { mainWindow.webContents.send('onBrowserFocus'); }); } let overlayWindow = undefined; function createOverlayWindowOffscreen() { if (process.platform !== 'linux') { console.error('Offscreen overlay is only supported on Linux.'); return; } isOverlayActive = true; if (!fs.existsSync(OVERLAY_SHM_PATH)) { createOverlayWindowShm(); } const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0; const y = parseInt(VRCXStorage.Get('VRCX_LocationY')) || 0; const width = OVERLAY_SHARED_WIDTH; const height = OVERLAY_SHARED_HEIGHT; overlayWindow = new BrowserWindow({ x, y, width, height, icon: path.join(rootDir, 'images/VRCX.png'), autoHideMenuBar: true, transparent: true, frame: false, show: false, webPreferences: { partition: 'vrcx-vr-overlay', offscreen: true, preload: path.join(__dirname, 'preload.js') } }); overlayWindow.webContents.setFrameRate(48); let fileUrl = `file://${path.join(rootDir, 'build/html/vr.html')}`; if (debug) { fileUrl = 'http://localhost:9000/vr.html'; } overlayWindow.loadURL(fileUrl, { userAgent: version }); // Use paint event for offscreen rendering overlayWindow.webContents.on('paint', (event, dirty, image) => { const buffer = image.toBitmap(); //console.log('Captured frame via paint event, size:', buffer.length); writeOverlayFrame(buffer); }); } function writeOverlayFrame(imageBuffer) { try { const fd = fs.openSync(OVERLAY_SHM_PATH, 'r+'); const buffer = Buffer.alloc(OVERLAY_FRAME_SIZE + 1); buffer[0] = 0; // not ready imageBuffer.copy(buffer, 1, 0, OVERLAY_FRAME_SIZE); buffer[0] = 1; // ready fs.writeSync(fd, buffer); fs.closeSync(fd); //console.log('Wrote frame to shared memory'); } catch (err) { console.error('Error writing frame to shared memory:', err); } } function destroyTray() { if (tray) { tray.destroy(); tray = null; } } function createTray() { if (process.platform === 'darwin') { const image = nativeImage.createFromPath( path.join(rootDir, 'images/VRCX.png') ); trayIcon = image.resize({ width: 16, height: 16 }); const imageNotify = nativeImage.createFromPath( path.join(rootDir, 'images/VRCX_notify.png') ); trayIconNotify = imageNotify.resize({ width: 16, height: 16 }); } else if (process.platform === 'linux') { const image = nativeImage.createFromPath( path.join(rootDir, 'images/VRCX.png') ); trayIcon = image.resize({ width: 64, height: 64 }); const imageNotify = nativeImage.createFromPath( path.join(rootDir, 'images/VRCX_notify.png') ); trayIconNotify = imageNotify.resize({ width: 64, height: 64 }); } else { trayIcon = path.join(rootDir, 'images/VRCX.ico'); trayIconNotify = path.join(rootDir, 'images/VRCX_notify.ico'); } tray = new Tray(trayIcon); const contextMenu = Menu.buildFromTemplate([ { label: 'Open', type: 'normal', click: function () { mainWindow.show(); } }, { label: 'DevTools', type: 'normal', click: function () { mainWindow.webContents.openDevTools(); } }, { label: 'Quit VRCX', type: 'normal', click: function () { appIsQuitting = true; app.quit(); } } ]); tray.setToolTip('VRCX'); tray.setContextMenu(contextMenu); tray.on('click', () => { mainWindow.show(); }); } function setTrayIconNotification(notify) { if (tray) { tray.setImage(notify ? trayIconNotify : trayIcon); } } async function installVRCX() { console.log('Home path:', homePath); console.log('AppImage path:', appImagePath); if (!appImagePath) { console.error('AppImage path is not available!'); return; } if (noInstall) { interopApi.getDotNetObject('Update').Init(appImagePath); console.log('Skipping installation.'); return; } // rename AppImage to VRCX.AppImage const currentName = path.basename(appImagePath); const expectedName = 'VRCX.AppImage'; if (currentName !== expectedName) { const newPath = path.join(path.dirname(appImagePath), expectedName); try { // remove existing VRCX.AppImage if (fs.existsSync(newPath)) { fs.unlinkSync(newPath); } fs.renameSync(appImagePath, newPath); console.log('AppImage renamed to:', newPath); appImagePath = newPath; } catch (err) { console.error(`Error renaming AppImage ${newPath}`, err); dialog.showErrorBox('VRCX', `Failed to rename AppImage ${newPath}`); return; } } // ask to move AppImage to ~/Applications const appImageHomePath = `${homePath}/Applications/VRCX.AppImage`; if (!hasAskedToMoveAppImage && appImagePath !== appImageHomePath) { const result = dialog.showMessageBoxSync(mainWindow, { type: 'question', title: 'VRCX', message: 'Do you want to install VRCX?', detail: 'VRCX will be moved to your ~/Applications folder.', buttons: ['No', 'Yes'] }); if (result === 0) { console.log('Cancel AppImage move to ~/Applications'); // don't ask again VRCXStorage.Set('VRCX_HasAskedToMoveAppImage', 'true'); VRCXStorage.Save(); } if (result === 1) { console.log('Moving AppImage to ~/Applications'); try { const applicationsPath = path.join(homePath, 'Applications'); // create ~/Applications if it doesn't exist if (!fs.existsSync(applicationsPath)) { fs.mkdirSync(applicationsPath); } // remove existing VRCX.AppImage if (fs.existsSync(appImageHomePath)) { fs.unlinkSync(appImageHomePath); } fs.renameSync(appImagePath, appImageHomePath); appImagePath = appImageHomePath; console.log('AppImage moved to:', appImageHomePath); } catch (err) { console.error(`Error moving AppImage ${appImageHomePath}`, err); dialog.showErrorBox( 'VRCX', `Failed to move AppImage ${appImageHomePath}` ); return; } } } // inform .NET side about AppImage path interopApi.getDotNetObject('Update').Init(appImagePath); await createDesktopFile(); } async function createDesktopFile() { if (noDesktop) { console.log('Skipping desktop file creation.'); return; } // Download the icon and save it to the target directory const iconPath = path.join(homePath, '.local/share/icons/VRCX.png'); if (!fs.existsSync(iconPath) || fs.statSync(iconPath).size === 0) { const iconDir = path.dirname(iconPath); if (!fs.existsSync(iconDir)) { fs.mkdirSync(iconDir, { recursive: true }); } const iconUrl = 'https://raw.githubusercontent.com/vrcx-team/VRCX/master/images/VRCX.png'; await downloadIcon(iconUrl, iconPath) .then(() => { console.log('Icon downloaded and saved to:', iconPath); }) .catch((err) => { console.error('Error downloading icon:', err); dialog.showErrorBox('VRCX', 'Failed to download the icon.'); }); } // Create the desktop file const desktopFilePath = path.join( homePath, '.local/share/applications/VRCX.desktop' ); const dotDesktop = { Name: 'VRCX', Version: version, Comment: 'Friendship management tool for VRChat', Exec: `${appImagePath} --ozone-platform-hint=auto %U`, Icon: 'VRCX', Type: 'Application', Categories: 'Network;InstantMessaging;Game;', Terminal: 'false', StartupWMClass: 'VRCX', MimeType: 'x-scheme-handler/vrcx;' }; const desktopFile = '[Desktop Entry]\n' + Object.entries(dotDesktop) .map(([key, value]) => `${key}=${value}`) .join('\n'); try { // Create the applications directory if it doesn't exist const desktopDir = path.dirname(desktopFilePath); if (!fs.existsSync(desktopDir)) { fs.mkdirSync(desktopDir, { recursive: true }); } // Create/update the desktop file when needed let existingDesktopFile = ''; if (fs.existsSync(desktopFilePath)) { existingDesktopFile = fs.readFileSync(desktopFilePath, 'utf8'); } if (existingDesktopFile !== desktopFile) { fs.writeFileSync(desktopFilePath, desktopFile); console.log('Desktop file created at:', desktopFilePath); const result = spawnSync( 'xdg-mime', ['default', 'VRCX.desktop', 'x-scheme-handler/vrcx'], { encoding: 'utf-8' } ); if (result.error) { console.error('Error setting MIME type:', result.error); } else { console.log('MIME type set x-scheme-handler/vrcx'); } } } catch (err) { console.error('Error creating desktop file:', err); dialog.showErrorBox('VRCX', 'Failed to create desktop entry.'); return; } } function downloadIcon(url, targetPath) { return new Promise((resolve, reject) => { const file = fs.createWriteStream(targetPath); https .get(url, (response) => { if (response.statusCode !== 200) { reject( new Error( `Failed to download icon, status code: ${response.statusCode}` ) ); return; } response.pipe(file); file.on('finish', () => { file.close(resolve); }); }) .on('error', (err) => { fs.unlink(targetPath, () => reject(err)); // Delete the file if error occurs }); }); } function getElectronUserDataPath() { const electronUserData = 'ElectronUserData'; if (process.platform === 'win32') { return path.join(getVRCXPath(), electronUserData); } if (process.platform === 'darwin') { return path.join( process.env.HOME, 'Library/Caches/VRCX', electronUserData ); } // Linux or other let cacheHome = process.env.XDG_CACHE_HOME; if (!cacheHome) { cacheHome = path.join(process.env.HOME, '.cache'); } return path.join(cacheHome, 'VRCX', electronUserData); } function getVRCXPath() { if (process.platform === 'win32') { return path.join(process.env.APPDATA, 'VRCX'); } else if (process.platform === 'darwin') { return path.join(process.env.HOME, 'Library/Application Support/VRCX'); } // Linux or other let configHome = process.env.XDG_CONFIG_HOME; if (!configHome) { configHome = path.join(process.env.HOME, '.config'); } return path.join(configHome, 'VRCX'); } function getHomePath() { const relativeHomePath = path.join(app.getPath('home')); try { const absoluteHomePath = fs.realpathSync(relativeHomePath); return absoluteHomePath; } catch (err) { console.error('Error resolving absolute home path:', err); return relativeHomePath; } } function getVersion() { try { const versionFile = fs .readFileSync(path.join(rootDir, 'Version'), 'utf8') .trim(); // look for trailing git hash "-22bcd96" to indicate nightly build const version = versionFile.split('-'); console.log('Version:', versionFile); if (version.length > 0 && version[version.length - 1].length == 7) { return `VRCX (Linux) Nightly ${versionFile}`; } else { return `VRCX (Linux) ${versionFile}`; } } catch (err) { console.error('Error reading Version:', err); return 'VRCX (Linux) Nightly Build'; } } function isDotNetInstalled() { let dotnetPath; if (process.env.DOTNET_ROOT) { dotnetPath = path.join(process.env.DOTNET_ROOT, 'dotnet'); if (!fs.existsSync(dotnetPath)) { // fallback to command dotnetPath = 'dotnet'; } } else { // fallback to command dotnetPath = 'dotnet'; } console.log('Checking for .NET installation at:', dotnetPath); // Fallback to system .NET runtime const result = spawnSync(dotnetPath, ['--list-runtimes'], { encoding: 'utf-8' }); if (result.error) { console.error('Error checking .NET runtimes:', result.error); return false; } return result.stdout?.includes('.NETCore.App 9.0'); } function tryCopyFromWinePrefix() { try { if (!fs.existsSync(getVRCXPath())) { // try copy from old wine path const userName = process.env.USER || process.env.USERNAME; const oldPath = path.join( homePath, '.local/share/vrcx/drive_c/users', userName, 'AppData/Roaming/VRCX' ); const newPath = getVRCXPath(); if (fs.existsSync(oldPath)) { fs.mkdirSync(newPath, { recursive: true }); const files = fs.readdirSync(oldPath); for (const file of files) { const oldFilePath = path.join(oldPath, file); const newFilePath = path.join(newPath, file); if (fs.lstatSync(oldFilePath).isDirectory()) { continue; } fs.copyFileSync(oldFilePath, newFilePath); } } } } catch (err) { console.error('Error copying from wine prefix:', err); dialog.showErrorBox( 'VRCX', 'Failed to copy database from wine prefix.' ); } } function applyWindowState() { if (VRCXStorage.Get('VRCX_StartAsMinimizedState') === 'true' && startup) { if (getCloseToTray()) { mainWindow.hide(); return; } mainWindow.minimize(); return; } const windowState = parseInt(VRCXStorage.Get('VRCX_WindowState')) || -1; switch (windowState) { case -1: break; case 0: mainWindow.restore(); break; case 1: mainWindow.minimize(); break; case 2: mainWindow.maximize(); break; } } app.whenReady().then(() => { createWindow(); createTray(); installVRCX(); app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } else { // Ensure main window shows when clicking Dock icon (critical for macOS) if (mainWindow && !mainWindow.isVisible()) { mainWindow.show(); } } }); }); function disposeOverlay() { if (!isOverlayActive) { return; } if (overlayWindow && !overlayWindow.isDestroyed()) { overlayWindow.close(); } overlayWindow = undefined; isOverlayActive = false; if (fs.existsSync(OVERLAY_SHM_PATH)) { fs.unlinkSync(OVERLAY_SHM_PATH); } } app.on('before-quit', function () { // Mark it as a quitting state to make macOS Dock's "Quit" action take effect. appIsQuitting = true; disposeOverlay(); destroyTray(); }); app.on('window-all-closed', function () { disposeOverlay(); if (process.platform !== 'darwin') { app.quit(); } });