import { spawn, type ChildProcess } from "node:child_process";import { randomBytes } from "node:crypto";import fs from "node:fs";import net from "node:net";import os from "node:os";import path from "node:path"; import { app, BrowserWindow, dialog, ipcMain, Menu, type MenuItemConstructorOptions, protocol, shell,} from "electron"; import { showDesktopConfirmDialog } from "./confirmDialog";import { fixPath } from "./fixPath"; fixPath(); const PICK_FOLDER_CHANNEL = "desktop:pick-folder";const CONFIRM_CHANNEL = "desktop:confirm";const CONTEXT_MENU_CHANNEL = "desktop:context-menu";const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";const MENU_ACTION_CHANNEL = "desktop:menu-action";const STATE_DIR = path.join(os.homedir(), ".t3", "userdata");const DESKTOP_SCHEME = "t3";const ROOT_DIR = path.resolve(__dirname, "../../..");const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";const APP_USER_MODEL_ID = "com.t3tools.t3code"; let mainWindow: BrowserWindow | null = null;let backendProcess: ChildProcess | null = null;let backendPort = 0;let backendAuthToken = "";let backendWsUrl = "";let restartAttempt = 0;let restartTimer: ReturnType<typeof setTimeout> | null = null;let isQuitting = false;let desktopProtocolRegistered = false; protocol.registerSchemesAsPrivileged([ { scheme: DESKTOP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, }, },]); function resolveAppRoot(): string { if (!app.isPackaged) { return ROOT_DIR; } return app.getAppPath();} function resolveBackendEntry(): string { return path.join(resolveAppRoot(), "apps/server/dist/index.mjs");} function resolveBackendCwd(): string { if (!app.isPackaged) { return resolveAppRoot(); } return os.homedir();} function resolveDesktopStaticDir(): string | null { const appRoot = resolveAppRoot(); const candidates = [ path.join(appRoot, "apps/server/dist/client"), path.join(appRoot, "apps/web/dist"), ]; for (const candidate of candidates) { if (fs.existsSync(path.join(candidate, "index.html"))) { return candidate; } } return null;} function resolveDesktopStaticPath(staticRoot: string, requestUrl: string): string { const url = new URL(requestUrl); const rawPath = decodeURIComponent(url.pathname); const normalizedPath = path.posix.normalize(rawPath).replace(/^\/+/, ""); if (normalizedPath.includes("..")) { return path.join(staticRoot, "index.html"); } const requestedPath = normalizedPath.length > 0 ? normalizedPath : "index.html"; const resolvedPath = path.join(staticRoot, requestedPath); if (path.extname(resolvedPath)) { return resolvedPath; } const nestedIndex = path.join(resolvedPath, "index.html"); if (fs.existsSync(nestedIndex)) { return nestedIndex; } return path.join(staticRoot, "index.html");} function isStaticAssetRequest(requestUrl: string): boolean { try { const url = new URL(requestUrl); return path.extname(url.pathname).length > 0; } catch { return false; }} function registerDesktopProtocol(): void { if (isDevelopment || desktopProtocolRegistered) return; const staticRoot = resolveDesktopStaticDir(); if (!staticRoot) { throw new Error( "Desktop static bundle missing. Build apps/server (with bundled client) first.", ); } const staticRootResolved = path.resolve(staticRoot); const staticRootPrefix = `${staticRootResolved}${path.sep}`; const fallbackIndex = path.join(staticRootResolved, "index.html"); protocol.registerFileProtocol(DESKTOP_SCHEME, (request, callback) => { try { const candidate = resolveDesktopStaticPath(staticRootResolved, request.url); const resolvedCandidate = path.resolve(candidate); const isInRoot = resolvedCandidate === fallbackIndex || resolvedCandidate.startsWith(staticRootPrefix); const isAssetRequest = isStaticAssetRequest(request.url); if (!isInRoot || !fs.existsSync(resolvedCandidate)) { if (isAssetRequest) { callback({ error: -6 }); return; } callback({ path: fallbackIndex }); return; } callback({ path: resolvedCandidate }); } catch { callback({ path: fallbackIndex }); } }); desktopProtocolRegistered = true;} function dispatchMenuAction(action: string): void { const existingWindow = BrowserWindow.getFocusedWindow() ?? mainWindow ?? BrowserWindow.getAllWindows()[0]; const targetWindow = existingWindow ?? createWindow(); if (!existingWindow) { mainWindow = targetWindow; } const send = () => { if (targetWindow.isDestroyed()) return; targetWindow.webContents.send(MENU_ACTION_CHANNEL, action); if (!targetWindow.isVisible()) { targetWindow.show(); } targetWindow.focus(); }; if (targetWindow.webContents.isLoadingMainFrame()) { targetWindow.webContents.once("did-finish-load", send); return; } send();} function configureApplicationMenu(): void { const template: MenuItemConstructorOptions[] = []; if (process.platform === "darwin") { template.push({ label: app.name, submenu: [ { role: "about" }, { label: "Settings...", accelerator: "CmdOrCtrl+,", click: () => dispatchMenuAction("open-settings"), }, { type: "separator" }, { role: "services" }, { type: "separator" }, { role: "hide" }, { role: "hideOthers" }, { role: "unhide" }, { type: "separator" }, { role: "quit" }, ], }); } template.push( { label: "File", submenu: [ ...(process.platform === "darwin" ? [] : [ { label: "Settings...", accelerator: "CmdOrCtrl+,", click: () => dispatchMenuAction("open-settings"), }, { type: "separator" as const }, ]), { role: process.platform === "darwin" ? "close" : "quit" }, ], }, { role: "editMenu" }, { role: "viewMenu" }, { role: "windowMenu" }, ); Menu.setApplicationMenu(Menu.buildFromTemplate(template));} function resolveResourcePath(fileName: string): string | null { const candidates = [ path.join(__dirname, "../resources", fileName), path.join(process.resourcesPath, "resources", fileName), path.join(process.resourcesPath, fileName), ]; for (const candidate of candidates) { if (fs.existsSync(candidate)) { return candidate; } } return null;} function resolveIconPath(ext: "ico" | "icns" | "png"): string | null { return resolveResourcePath(`icon.${ext}`);} function configureAppIdentity(): void { app.setName(APP_DISPLAY_NAME); app.setAboutPanelOptions({ applicationName: APP_DISPLAY_NAME, }); if (process.platform === "win32") { app.setAppUserModelId(APP_USER_MODEL_ID); } if (process.platform === "darwin" && app.dock) { const iconPath = resolveIconPath("png"); if (iconPath) { app.dock.setIcon(iconPath); } }} async function reserveLoopbackPort(): Promise<number> { return new Promise((resolve, reject) => { const probe = net.createServer(); probe.listen(0, "127.0.0.1", () => { const address = probe.address(); const port = typeof address === "object" && address !== null ? address.port : 0; probe.close(() => { if (port > 0) { resolve(port); return; } reject(new Error("Failed to reserve backend port")); }); }); probe.on("error", reject); });} function backendEnv(): NodeJS.ProcessEnv { return { ...process.env, T3CODE_MODE: "desktop", T3CODE_NO_BROWSER: "1", T3CODE_PORT: String(backendPort), T3CODE_STATE_DIR: STATE_DIR, T3CODE_AUTH_TOKEN: backendAuthToken, };} function scheduleBackendRestart(reason: string): void { if (isQuitting || restartTimer) return; const delayMs = Math.min(500 * 2 ** restartAttempt, 10_000); restartAttempt += 1; console.error(`[desktop] backend exited unexpectedly (${reason}); restarting in ${delayMs}ms`); restartTimer = setTimeout(() => { restartTimer = null; startBackend(); }, delayMs);} function startBackend(): void { if (isQuitting || backendProcess) return; const backendEntry = resolveBackendEntry(); if (!fs.existsSync(backendEntry)) { scheduleBackendRestart(`missing server entry at ${backendEntry}`); return; } const child = spawn(process.execPath, [backendEntry], { cwd: resolveBackendCwd(), // In Electron main, process.execPath points to the Electron binary. // Run the child in Node mode so this backend process does not become a GUI app instance. env: { ...backendEnv(), ELECTRON_RUN_AS_NODE: "1", }, stdio: "inherit", }); backendProcess = child; child.once("spawn", () => { restartAttempt = 0; }); child.on("error", (error) => { if (backendProcess === child) { backendProcess = null; } scheduleBackendRestart(error.message); }); child.on("exit", (code, signal) => { if (backendProcess === child) { backendProcess = null; } if (isQuitting) return; const reason = `code=${code ?? "null"} signal=${signal ?? "null"}`; scheduleBackendRestart(reason); });} function stopBackend(): void { if (restartTimer) { clearTimeout(restartTimer); restartTimer = null; } const child = backendProcess; backendProcess = null; if (!child) return; if (child.exitCode === null && child.signalCode === null) { child.kill("SIGTERM"); setTimeout(() => { if (child.exitCode === null && child.signalCode === null) { child.kill("SIGKILL"); } }, 2_000).unref(); }} function registerIpcHandlers(): void { ipcMain.removeHandler(PICK_FOLDER_CHANNEL); ipcMain.handle(PICK_FOLDER_CHANNEL, async () => { const owner = BrowserWindow.getFocusedWindow() ?? mainWindow; const result = owner ? await dialog.showOpenDialog(owner, { properties: ["openDirectory", "createDirectory"], }) : await dialog.showOpenDialog({ properties: ["openDirectory", "createDirectory"], }); if (result.canceled) return null; return result.filePaths[0] ?? null; }); ipcMain.removeHandler(CONFIRM_CHANNEL); ipcMain.handle(CONFIRM_CHANNEL, async (_event, message: unknown) => { if (typeof message !== "string") { return false; } const owner = BrowserWindow.getFocusedWindow() ?? mainWindow; return showDesktopConfirmDialog(message, owner); }); ipcMain.removeHandler(CONTEXT_MENU_CHANNEL); ipcMain.handle(CONTEXT_MENU_CHANNEL, async (_event, items: { id: string; label: string }[]) => { const window = BrowserWindow.getFocusedWindow() ?? mainWindow; if (!window) return null; return new Promise<string | null>((resolve) => { const menu = Menu.buildFromTemplate( items.map((item) => ({ label: item.label, click: () => resolve(item.id), })), ); menu.popup({ window, callback: () => resolve(null), }); }); }); ipcMain.removeHandler(OPEN_EXTERNAL_CHANNEL); ipcMain.handle(OPEN_EXTERNAL_CHANNEL, async (_event, rawUrl: unknown) => { if (typeof rawUrl !== "string" || rawUrl.length === 0) { return false; } let parsedUrl: URL; try { parsedUrl = new URL(rawUrl); } catch { return false; } if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") { return false; } try { await shell.openExternal(parsedUrl.toString()); return true; } catch { return false; } });} function getIconOption(): { icon: string } | Record<string, never> { if (process.platform === "darwin") return {}; // macOS uses .icns from app bundle const ext = process.platform === "win32" ? "ico" : "png"; const iconPath = resolveIconPath(ext); return iconPath ? { icon: iconPath } : {};} function createWindow(): BrowserWindow { const window = new BrowserWindow({ width: 1100, height: 780, minWidth: 840, minHeight: 620, show: false, autoHideMenuBar: true, ...getIconOption(), title: APP_DISPLAY_NAME, titleBarStyle: "hiddenInset", trafficLightPosition: { x: 16, y: 18 }, webPreferences: { preload: path.join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false, sandbox: true, }, }); window.webContents.setWindowOpenHandler(() => ({ action: "deny" })); window.on("page-title-updated", (event) => { event.preventDefault(); window.setTitle(APP_DISPLAY_NAME); }); window.webContents.on("did-finish-load", () => { window.setTitle(APP_DISPLAY_NAME); }); window.once("ready-to-show", () => { window.show(); }); if (isDevelopment) { void window.loadURL(process.env.VITE_DEV_SERVER_URL as string); window.webContents.openDevTools({ mode: "detach" }); } else { void window.loadURL(`${DESKTOP_SCHEME}://app/index.html`); } window.on("closed", () => { if (mainWindow === window) { mainWindow = null; }Lines 1–500 of 557. Load more
import { spawn, type ChildProcess } from "node:child_process"; import { randomBytes } from "node:crypto"; import fs from "node:fs"; import net from "node:net"; import os from "node:os"; import path from "node:path"; import { app, BrowserWindow, dialog, ipcMain, Menu, type MenuItemConstructorOptions, protocol, shell, } from "electron"; import { showDesktopConfirmDialog } from "./confirmDialog"; import { fixPath } from "./fixPath"; fixPath(); const PICK_FOLDER_CHANNEL = "desktop:pick-folder"; const CONFIRM_CHANNEL = "desktop:confirm"; const CONTEXT_MENU_CHANNEL = "desktop:context-menu"; const OPEN_EXTERNAL_CHANNEL = "desktop:open-external"; const MENU_ACTION_CHANNEL = "desktop:menu-action"; const STATE_DIR = path.join(os.homedir(), ".t3", "userdata"); const DESKTOP_SCHEME = "t3"; const ROOT_DIR = path.resolve(__dirname, "../../.."); const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL); const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)"; const APP_USER_MODEL_ID = "com.t3tools.t3code"; let mainWindow: BrowserWindow | null = null; let backendProcess: ChildProcess | null = null; let backendPort = 0; let backendAuthToken = ""; let backendWsUrl = ""; let restartAttempt = 0; let restartTimer: ReturnType<typeof setTimeout> | null = null; let isQuitting = false; let desktopProtocolRegistered = false; protocol.registerSchemesAsPrivileged([ { scheme: DESKTOP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, }, }, ]); function resolveAppRoot(): string { if (!app.isPackaged) { return ROOT_DIR; } return app.getAppPath(); } function resolveBackendEntry(): string { return path.join(resolveAppRoot(), "apps/server/dist/index.mjs"); } function resolveBackendCwd(): string { if (!app.isPackaged) { return resolveAppRoot(); } return os.homedir(); } function resolveDesktopStaticDir(): string | null { const appRoot = resolveAppRoot(); const candidates = [ path.join(appRoot, "apps/server/dist/client"), path.join(appRoot, "apps/web/dist"), ]; for (const candidate of candidates) { if (fs.existsSync(path.join(candidate, "index.html"))) { return candidate; } } return null; } function resolveDesktopStaticPath(staticRoot: string, requestUrl: string): string { const url = new URL(requestUrl); const rawPath = decodeURIComponent(url.pathname); const normalizedPath = path.posix.normalize(rawPath).replace(/^\/+/, ""); if (normalizedPath.includes("..")) { return path.join(staticRoot, "index.html"); } const requestedPath = normalizedPath.length > 0 ? normalizedPath : "index.html"; const resolvedPath = path.join(staticRoot, requestedPath); if (path.extname(resolvedPath)) { return resolvedPath; } const nestedIndex = path.join(resolvedPath, "index.html"); if (fs.existsSync(nestedIndex)) { return nestedIndex; } return path.join(staticRoot, "index.html"); } function isStaticAssetRequest(requestUrl: string): boolean { try { const url = new URL(requestUrl); return path.extname(url.pathname).length > 0; } catch { return false; } } function registerDesktopProtocol(): void { if (isDevelopment || desktopProtocolRegistered) return; const staticRoot = resolveDesktopStaticDir(); if (!staticRoot) { throw new Error( "Desktop static bundle missing. Build apps/server (with bundled client) first.", ); } const staticRootResolved = path.resolve(staticRoot); const staticRootPrefix = `${staticRootResolved}${path.sep}`; const fallbackIndex = path.join(staticRootResolved, "index.html"); protocol.registerFileProtocol(DESKTOP_SCHEME, (request, callback) => { try { const candidate = resolveDesktopStaticPath(staticRootResolved, request.url); const resolvedCandidate = path.resolve(candidate); const isInRoot = resolvedCandidate === fallbackIndex || resolvedCandidate.startsWith(staticRootPrefix); const isAssetRequest = isStaticAssetRequest(request.url); if (!isInRoot || !fs.existsSync(resolvedCandidate)) { if (isAssetRequest) { callback({ error: -6 }); return; } callback({ path: fallbackIndex }); return; } callback({ path: resolvedCandidate }); } catch { callback({ path: fallbackIndex }); } }); desktopProtocolRegistered = true; } function dispatchMenuAction(action: string): void { const existingWindow = BrowserWindow.getFocusedWindow() ?? mainWindow ?? BrowserWindow.getAllWindows()[0]; const targetWindow = existingWindow ?? createWindow(); if (!existingWindow) { mainWindow = targetWindow; } const send = () => { if (targetWindow.isDestroyed()) return; targetWindow.webContents.send(MENU_ACTION_CHANNEL, action); if (!targetWindow.isVisible()) { targetWindow.show(); } targetWindow.focus(); }; if (targetWindow.webContents.isLoadingMainFrame()) { targetWindow.webContents.once("did-finish-load", send); return; } send(); } function configureApplicationMenu(): void { const template: MenuItemConstructorOptions[] = []; if (process.platform === "darwin") { template.push({ label: app.name, submenu: [ { role: "about" }, { label: "Settings...", accelerator: "CmdOrCtrl+,", click: () => dispatchMenuAction("open-settings"), }, { type: "separator" }, { role: "services" }, { type: "separator" }, { role: "hide" }, { role: "hideOthers" }, { role: "unhide" }, { type: "separator" }, { role: "quit" }, ], }); } template.push( { label: "File", submenu: [ ...(process.platform === "darwin" ? [] : [ { label: "Settings...", accelerator: "CmdOrCtrl+,", click: () => dispatchMenuAction("open-settings"), }, { type: "separator" as const }, ]), { role: process.platform === "darwin" ? "close" : "quit" }, ], }, { role: "editMenu" }, { role: "viewMenu" }, { role: "windowMenu" }, ); Menu.setApplicationMenu(Menu.buildFromTemplate(template)); } function resolveResourcePath(fileName: string): string | null { const candidates = [ path.join(__dirname, "../resources", fileName), path.join(process.resourcesPath, "resources", fileName), path.join(process.resourcesPath, fileName), ]; for (const candidate of candidates) { if (fs.existsSync(candidate)) { return candidate; } } return null; } function resolveIconPath(ext: "ico" | "icns" | "png"): string | null { return resolveResourcePath(`icon.${ext}`); } function configureAppIdentity(): void { app.setName(APP_DISPLAY_NAME); app.setAboutPanelOptions({ applicationName: APP_DISPLAY_NAME, }); if (process.platform === "win32") { app.setAppUserModelId(APP_USER_MODEL_ID); } if (process.platform === "darwin" && app.dock) { const iconPath = resolveIconPath("png"); if (iconPath) { app.dock.setIcon(iconPath); } } } async function reserveLoopbackPort(): Promise<number> { return new Promise((resolve, reject) => { const probe = net.createServer(); probe.listen(0, "127.0.0.1", () => { const address = probe.address(); const port = typeof address === "object" && address !== null ? address.port : 0; probe.close(() => { if (port > 0) { resolve(port); return; } reject(new Error("Failed to reserve backend port")); }); }); probe.on("error", reject); }); } function backendEnv(): NodeJS.ProcessEnv { return { ...process.env, T3CODE_MODE: "desktop", T3CODE_NO_BROWSER: "1", T3CODE_PORT: String(backendPort), T3CODE_STATE_DIR: STATE_DIR, T3CODE_AUTH_TOKEN: backendAuthToken, }; } function scheduleBackendRestart(reason: string): void { if (isQuitting || restartTimer) return; const delayMs = Math.min(500 * 2 ** restartAttempt, 10_000); restartAttempt += 1; console.error(`[desktop] backend exited unexpectedly (${reason}); restarting in ${delayMs}ms`); restartTimer = setTimeout(() => { restartTimer = null; startBackend(); }, delayMs); } function startBackend(): void { if (isQuitting || backendProcess) return; const backendEntry = resolveBackendEntry(); if (!fs.existsSync(backendEntry)) { scheduleBackendRestart(`missing server entry at ${backendEntry}`); return; } const child = spawn(process.execPath, [backendEntry], { cwd: resolveBackendCwd(), // In Electron main, process.execPath points to the Electron binary. // Run the child in Node mode so this backend process does not become a GUI app instance. env: { ...backendEnv(), ELECTRON_RUN_AS_NODE: "1", }, stdio: "inherit", }); backendProcess = child; child.once("spawn", () => { restartAttempt = 0; }); child.on("error", (error) => { if (backendProcess === child) { backendProcess = null; } scheduleBackendRestart(error.message); }); child.on("exit", (code, signal) => { if (backendProcess === child) { backendProcess = null; } if (isQuitting) return; const reason = `code=${code ?? "null"} signal=${signal ?? "null"}`; scheduleBackendRestart(reason); }); } function stopBackend(): void { if (restartTimer) { clearTimeout(restartTimer); restartTimer = null; } const child = backendProcess; backendProcess = null; if (!child) return; if (child.exitCode === null && child.signalCode === null) { child.kill("SIGTERM"); setTimeout(() => { if (child.exitCode === null && child.signalCode === null) { child.kill("SIGKILL"); } }, 2_000).unref(); } } function registerIpcHandlers(): void { ipcMain.removeHandler(PICK_FOLDER_CHANNEL); ipcMain.handle(PICK_FOLDER_CHANNEL, async () => { const owner = BrowserWindow.getFocusedWindow() ?? mainWindow; const result = owner ? await dialog.showOpenDialog(owner, { properties: ["openDirectory", "createDirectory"], }) : await dialog.showOpenDialog({ properties: ["openDirectory", "createDirectory"], }); if (result.canceled) return null; return result.filePaths[0] ?? null; }); ipcMain.removeHandler(CONFIRM_CHANNEL); ipcMain.handle(CONFIRM_CHANNEL, async (_event, message: unknown) => { if (typeof message !== "string") { return false; } const owner = BrowserWindow.getFocusedWindow() ?? mainWindow; return showDesktopConfirmDialog(message, owner); }); ipcMain.removeHandler(CONTEXT_MENU_CHANNEL); ipcMain.handle(CONTEXT_MENU_CHANNEL, async (_event, items: { id: string; label: string }[]) => { const window = BrowserWindow.getFocusedWindow() ?? mainWindow; if (!window) return null; return new Promise<string | null>((resolve) => { const menu = Menu.buildFromTemplate( items.map((item) => ({ label: item.label, click: () => resolve(item.id), })), ); menu.popup({ window, callback: () => resolve(null), }); }); }); ipcMain.removeHandler(OPEN_EXTERNAL_CHANNEL); ipcMain.handle(OPEN_EXTERNAL_CHANNEL, async (_event, rawUrl: unknown) => { if (typeof rawUrl !== "string" || rawUrl.length === 0) { return false; } let parsedUrl: URL; try { parsedUrl = new URL(rawUrl); } catch { return false; } if (parsedUrl.protocol !== "https:" && parsedUrl.protocol !== "http:") { return false; } try { await shell.openExternal(parsedUrl.toString()); return true; } catch { return false; } }); } function getIconOption(): { icon: string } | Record<string, never> { if (process.platform === "darwin") return {}; // macOS uses .icns from app bundle const ext = process.platform === "win32" ? "ico" : "png"; const iconPath = resolveIconPath(ext); return iconPath ? { icon: iconPath } : {}; } function createWindow(): BrowserWindow { const window = new BrowserWindow({ width: 1100, height: 780, minWidth: 840, minHeight: 620, show: false, autoHideMenuBar: true, ...getIconOption(), title: APP_DISPLAY_NAME, titleBarStyle: "hiddenInset", trafficLightPosition: { x: 16, y: 18 }, webPreferences: { preload: path.join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false, sandbox: true, }, }); window.webContents.setWindowOpenHandler(() => ({ action: "deny" })); window.on("page-title-updated", (event) => { event.preventDefault(); window.setTitle(APP_DISPLAY_NAME); }); window.webContents.on("did-finish-load", () => { window.setTitle(APP_DISPLAY_NAME); }); window.once("ready-to-show", () => { window.show(); }); if (isDevelopment) { void window.loadURL(process.env.VITE_DEV_SERVER_URL as string); window.webContents.openDevTools({ mode: "detach" }); } else { void window.loadURL(`${DESKTOP_SCHEME}://app/index.html`); } window.on("closed", () => { if (mainWindow === window) { mainWindow = null; }