cncjs
1import fs from 'node:fs';2import path from 'node:path';3import {4BrowserWindow,5Menu,6app,7ipcMain,8powerSaveBlocker,9screen,10shell,11} from 'electron';12import Store from 'electron-store';13import chalk from 'chalk';14import mkdirp from 'mkdirp';15import {16createApplicationMenuTemplate,17inputMenuTemplate,18selectionMenuTemplate,19} from './electron-app/menu-template';20import launchServer from './server-cli';21import pkg from './package.json';22
23let mainWindow = null;24let powerId = 0;25const store = new Store();26
27// https://github.com/electron/electron/blob/master/docs/api/app.md#apprequestsingleinstancelock
28const gotSingleInstanceLock = app.requestSingleInstanceLock();29const shouldQuitImmediately = !gotSingleInstanceLock;30if (shouldQuitImmediately) {31app.quit();32process.exit(0);33}
34
35// Create the user data directory if it does not exist
36const userDataPath = app.getPath('userData');37mkdirp.sync(userDataPath);38
39function getBrowserWindowOptions() {40const defaultOptions = {41width: 1440,42height: 900,43minHeight: 708,44minWidth: 1024,45show: false,46title: `${pkg.name} ${pkg.version}`,47
48// useContentSize boolean (optional) - The width and height would be used as web page's size, which means the actual window's size will include window frame's size and be slightly larger. Default is false.49useContentSize: true,50
51// webPreferences Object (optional) - Settings of web page's features.52webPreferences: {53// https://www.electronjs.org/docs/latest/breaking-changes#default-changed-contextisolation-defaults-to-true54// require() cannot be used in the renderer process unless nodeIntegration is true and contextIsolation is false.55contextIsolation: false,56nodeIntegration: true,57}58};59
60// { x, y, width, height }61const lastOptions = store.get('bounds');62
63// Get display that most closely intersects the provided bounds64let windowOptions = {};65if (lastOptions) {66const display = screen.getDisplayMatching(lastOptions);67
68if (display.id === lastOptions.id) {69// Use last time options when using the same display70windowOptions = {71...windowOptions,72...lastOptions,73};74} else {75// Or center the window when using other display76const workArea = display.workArea;77
78// Calculate window size79const width = Math.max(Math.min(lastOptions.width, workArea.width), 360);80const height = Math.max(Math.min(lastOptions.height, workArea.height), 240);81const x = workArea.x + (workArea.width - width) / 2;82const y = workArea.y + (workArea.height - height) / 2;83
84windowOptions = {85id: display.id,86x,87y,88width,89height,90};91}92} else {93const display = screen.getPrimaryDisplay();94const { x, y, width } = display.workArea;95const nx = x + (width - 1440) / 2;96windowOptions = {97id: display.id,98x: nx,99y,100center: true,101};102}103
104return Object.assign({}, defaultOptions, windowOptions);105}
106
107const showMainWindow = async () => {108const browserWindowOptions = getBrowserWindowOptions();109const browserWindow = new BrowserWindow(browserWindowOptions);110mainWindow = browserWindow;111powerId = powerSaveBlocker.start('prevent-display-sleep');112
113const res = await launchServer();114const { address, port, mountPoints } = { ...res };115if (!(address && port)) {116console.error('Unable to start the server at ' + chalk.cyan(`http://${address}:${port}`));117return;118}119
120const applicationMenu = Menu.buildFromTemplate(createApplicationMenuTemplate({ address, port, mountPoints }));121const inputMenu = Menu.buildFromTemplate(inputMenuTemplate);122const selectionMenu = Menu.buildFromTemplate(selectionMenuTemplate);123Menu.setApplicationMenu(applicationMenu);124
125// https://www.electronjs.org/docs/latest/api/web-contents#contentssetwindowopenhandlerhandler126// https://github.com/electron/electron/pull/24517127mainWindow.webContents.setWindowOpenHandler(({ url }) => {128shell.openExternal(url);129return { action: 'deny' };130});131
132// https://github.com/electron/electron/blob/main/docs/api/web-contents.md#event-context-menu133// https://github.com/electron/electron/issues/4068#issuecomment-274159726134mainWindow.webContents.on('context-menu', (event, props) => {135const { selectionText, isEditable } = props;136if (isEditable) {137inputMenu.popup(mainWindow);138} else if (selectionText && String(selectionText).trim() !== '') {139selectionMenu.popup(mainWindow);140}141});142
143const webContentsSession = mainWindow.webContents.session;144webContentsSession.setProxy({ proxyRules: 'direct://' })145.then(() => {146const url = `http://${address}:${port}`;147mainWindow.loadURL(url);148})149.catch(err => {150console.log('err', err.message);151});152
153if (process.platform === 'win32') {154mainWindow.show();155} else {156mainWindow.on('ready-to-show', () => {157mainWindow.show();158});159}160
161// Save window size and position162mainWindow.on('close', (event) => {163const bounds = mainWindow.getBounds();164const display = screen.getDisplayMatching(bounds);165const options = {166id: display.id,167...bounds,168};169
170store.set('bounds', options);171mainWindow.webContents.send('save-and-close');172
173mainWindow = null;174});175
176// @see 'src/app/store/index.js'177ipcMain.handle('read-user-config', () => {178let content = '{}';179const configPath = path.join(userDataPath, 'cnc.json');180if (fs.existsSync(configPath)) {181content = fs.readFileSync(configPath, 'utf8');182}183return content;184});185
186// @see 'src/app/store/index.js'187ipcMain.handle('write-user-config', (event, content) => {188const configPath = path.join(userDataPath, 'cnc.json');189fs.writeFileSync(configPath, content ?? '{}');190});191};192
193// Increase V8 heap size of the main process in production
194if (process.arch === 'x64') {195const memoryLimit = 1024 * 4; // 4GB196app.commandLine.appendSwitch('--js-flags', `--max-old-space-size=${memoryLimit}`);197}
198
199// Ignore the GPU blacklist and use any available GPU
200app.commandLine.appendSwitch('ignore-gpu-blacklist');201
202if (process.platform === 'linux') {203// https://github.com/electron/electron/issues/18265204// Run this at early startup, before app.on('ready')205//206// TODO: Maybe we can only disable --disable-setuid-sandbox207// reference changes: https://github.com/microsoft/vscode/pull/122909/files208app.commandLine.appendSwitch('--no-sandbox');209}
210
211/**
212* https://www.electronjs.org/docs/latest/api/app#event-activate-macos
213*
214* Event: 'activate' [macOS]
215*
216* Returns:
217* - `event` Event
218* - `hasVisibleWindows` boolean
219*
220* Emitted when the application is activated. Various actions can trigger this event, such as launching the application for the first time, attempting to re-launch the application when it's already running, or clicking on the application's dock or taskbar icon.
221*/
222app.on('activate', async (event, hasVisibleWindows) => {223if (!mainWindow) {224await showMainWindow();225}226});227
228/**
229* https://www.electronjs.org/docs/latest/api/app#event-window-all-closed
230*
231* Event: 'window-all-closed'
232*
233* Emitted when all windows have been closed.
234*
235* If you do not subscribe to this event and all windows are closed, the default behavior is to quit the app; however, if you subscribe, you control whether the app quits or not. If the user pressed `Cmd + Q`, or the developer called `app.quit()`, Electron will first try to close all the windows and then emit the `will-quit` event, and in this case the `window-all-closed` event would not be emitted.
236*/
237app.on('window-all-closed', () => {238powerSaveBlocker.stop(powerId);239
240app.quit();241});242
243/**
244* https://www.electronjs.org/docs/latest/api/app#event-second-instance
245*
246* Event: 'second-instance'
247*
248* Returns:
249* - `event` Event
250* - `argv` string[] - An array of the second instance's command line arguments
251* - `workingDirectory` string - The second instance's working directory
252* - `additionalData` unknown - A JSON object of additional data passed from the second instance
253*
254* This event will be emitted inside the primary instance of your application when a second instance has been executed and calls `app.requestSingleInstanceLock()`.
255*
256* `argv` is an Array of the second instance's command line arguments, and `workingDirectory` is its current working directory. Usually applications respond to this by making their primary window focused and non-minimized.
257*
258* Note: If the second instance is started by a different user than the first, the `argv` array will not include the arguments.
259*
260* This event is guaranteed to be emitted after the ready event of app gets emitted.
261*/
262app.on('second-instance', (event, argv, workingDirectory, additionalData) => {263if (mainWindow) {264if (mainWindow.isMinimized()) {265mainWindow.restore();266}267mainWindow.focus();268}269});270
271/**
272* Method: app.whenReady()
273*
274* Returns Promise<void> - fulfilled when Electron is initialized. May be used as a convenient alternative to checking `app.isReady()` and subscribing to the `ready` event if the app is not ready yet.
275*/
276app.whenReady().then(showMainWindow);277