From 16296a27da4b3315e0ad872a5268cc6d950e83dd Mon Sep 17 00:00:00 2001 From: Jonathan van Rij Date: Tue, 16 Jun 2026 13:35:28 +0200 Subject: [PATCH] multiple instances --- src/main/index.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/index.ts b/src/main/index.ts index 1c6ce04..645f976 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,5 +1,6 @@ import { join, resolve, sep } from 'node:path' import { readFileSync, statSync } from 'node:fs' +import { spawn } from 'node:child_process' import { app, dialog, shell, BrowserWindow, ipcMain, Menu } from 'electron' import { watch, type FSWatcher } from 'chokidar' import { addRecentProject, getName, getRecentProjects, getRoot, openDialog, setRoot } from './project' @@ -27,6 +28,32 @@ function broadcast(channel: string): void { for (const w of BrowserWindow.getAllWindows()) w.webContents.send(channel) } +/** + * Each Helder window is one project, and one project owns global main-process + * state (root + FS/git/config watchers + PTYs). To run several projects beside + * each other we therefore spawn a *separate, detached* Helder process per window + * rather than opening a second BrowserWindow in this process. + * + * Spawning `process.execPath` directly also sidesteps macOS LaunchServices, which + * otherwise just re-activates the running instance when the bundle is launched + * again from Finder/Dock. `HELDER_PROJECT` points the child at a project (the + * child's `resolveInitialRoot` reads it); with no path it lands on the launcher. + */ +function spawnInstance(projectPath?: string): void { + const env = { ...process.env } + if (projectPath && safeIsDir(projectPath)) env.HELDER_PROJECT = resolve(projectPath) + else delete env.HELDER_PROJECT + // Packaged: execPath IS the app, no app path needed. Dev: replay our own argv + // (the electron-vite main entry) so the child loads the same app. + const args = app.isPackaged ? [] : process.argv.slice(1) + const child = spawn(process.execPath, args, { detached: true, stdio: 'ignore', env }) + child.unref() +} + +function safeIsDir(p: string): boolean { + try { return statSync(p).isDirectory() } catch { return false } +} + function startConfigWatcher(): void { if (configWatcher) { configWatcher.close(); configWatcher = null } const root = getRoot() @@ -96,6 +123,11 @@ function buildAppMenu(): Menu { { label: 'File', submenu: [ + { + label: 'New Window', + accelerator: 'CmdOrCtrl+Shift+N', + click: () => spawnInstance(), + }, { label: 'Open Folder…', accelerator: 'CmdOrCtrl+O',