|
1 | 1 | import { execFileSync } from "node:child_process"; |
| 2 | +import { existsSync } from "node:fs"; |
2 | 3 | import { dirname, join, resolve } from "node:path"; |
3 | 4 | import { fileURLToPath } from "node:url"; |
4 | 5 | import * as Sentry from "@sentry/electron/main"; |
5 | 6 | import { |
6 | 7 | BrowserWindow, |
7 | 8 | Menu, |
8 | 9 | type MenuItemConstructorOptions, |
| 10 | + Tray, |
9 | 11 | app, |
10 | 12 | crashReporter, |
11 | 13 | dialog, |
12 | 14 | globalShortcut, |
| 15 | + nativeImage, |
13 | 16 | nativeTheme, |
14 | 17 | powerMonitor, |
15 | 18 | powerSaveBlocker, |
@@ -62,8 +65,15 @@ import { |
62 | 65 | getDefaultPlistDir, |
63 | 66 | getLogDir, |
64 | 67 | installLaunchdQuitHandler, |
| 68 | + runTeardownAndExit, |
65 | 69 | teardownLaunchdServices, |
66 | 70 | } from "./services"; |
| 71 | +import { |
| 72 | + type DesktopShellPreferences, |
| 73 | + applyDesktopShellPreferencesOnStartup, |
| 74 | + getDesktopShellPreferences, |
| 75 | + setDesktopShellPreferencesRuntimeHandler, |
| 76 | +} from "./services/desktop-shell-preferences"; |
67 | 77 | import { isLaunchdBootstrapEnabled } from "./services/launchd-bootstrap"; |
68 | 78 | import { ProxyManager } from "./services/proxy-manager"; |
69 | 79 | import { |
@@ -277,6 +287,10 @@ if (sentryDsn) { |
277 | 287 | } |
278 | 288 |
|
279 | 289 | let mainWindow: BrowserWindow | null = null; |
| 290 | +let residentTray: Tray | null = null; |
| 291 | +let launchdQuitOptsForResidentEntry: |
| 292 | + | Parameters<typeof installLaunchdQuitHandler>[0] |
| 293 | + | null = null; |
280 | 294 | let diagnosticsReporter: DesktopDiagnosticsReporter | null = null; |
281 | 295 |
|
282 | 296 | /** True if this is the x86_64 build running under Rosetta 2 on Apple Silicon. */ |
@@ -946,18 +960,147 @@ function focusMainWindow(): void { |
946 | 960 | mainWindow.focus(); |
947 | 961 | } |
948 | 962 |
|
| 963 | +function shouldUseResidentEntry(preferences: DesktopShellPreferences): boolean { |
| 964 | + return !preferences.showInDock; |
| 965 | +} |
| 966 | + |
| 967 | +function resolveTrayIconPath(): string | null { |
| 968 | + const candidate = resolve( |
| 969 | + app.isPackaged ? process.resourcesPath : getDesktopAppRoot(), |
| 970 | + "build", |
| 971 | + process.platform === "win32" ? "icon.ico" : "icon.png", |
| 972 | + ); |
| 973 | + |
| 974 | + return existsSync(candidate) ? candidate : null; |
| 975 | +} |
| 976 | + |
| 977 | +function hideMainWindowToBackground(): void { |
| 978 | + if (!mainWindow || mainWindow.isDestroyed()) { |
| 979 | + return; |
| 980 | + } |
| 981 | + |
| 982 | + mainWindow.hide(); |
| 983 | +} |
| 984 | + |
| 985 | +function showMainWindowFromResidentEntry(): void { |
| 986 | + const preferences = getDesktopShellPreferences(); |
| 987 | + |
| 988 | + if (process.platform === "darwin" && preferences.showInDock) { |
| 989 | + void app.dock?.show(); |
| 990 | + } |
| 991 | + |
| 992 | + if (!mainWindow || mainWindow.isDestroyed()) { |
| 993 | + createMainWindow(); |
| 994 | + return; |
| 995 | + } |
| 996 | + |
| 997 | + if (!mainWindow.isVisible()) { |
| 998 | + mainWindow.show(); |
| 999 | + } |
| 1000 | + |
| 1001 | + focusMainWindow(); |
| 1002 | +} |
| 1003 | + |
| 1004 | +function destroyResidentTray(): void { |
| 1005 | + residentTray?.destroy(); |
| 1006 | + residentTray = null; |
| 1007 | +} |
| 1008 | + |
| 1009 | +function ensureResidentTray(): void { |
| 1010 | + if (residentTray) { |
| 1011 | + return; |
| 1012 | + } |
| 1013 | + |
| 1014 | + const trayIconPath = resolveTrayIconPath(); |
| 1015 | + if (!trayIconPath) { |
| 1016 | + return; |
| 1017 | + } |
| 1018 | + |
| 1019 | + const trayIcon = nativeImage.createFromPath(trayIconPath); |
| 1020 | + if (trayIcon.isEmpty()) { |
| 1021 | + return; |
| 1022 | + } |
| 1023 | + |
| 1024 | + if (process.platform === "darwin") { |
| 1025 | + trayIcon.setTemplateImage(true); |
| 1026 | + } |
| 1027 | + |
| 1028 | + residentTray = new Tray(trayIcon); |
| 1029 | + residentTray.setToolTip("nexu"); |
| 1030 | + residentTray.setContextMenu( |
| 1031 | + Menu.buildFromTemplate([ |
| 1032 | + { |
| 1033 | + label: "Open nexu", |
| 1034 | + click: () => { |
| 1035 | + showMainWindowFromResidentEntry(); |
| 1036 | + }, |
| 1037 | + }, |
| 1038 | + { |
| 1039 | + label: "Quit", |
| 1040 | + click: () => { |
| 1041 | + if (app.isPackaged && launchdQuitOptsForResidentEntry) { |
| 1042 | + void runTeardownAndExit( |
| 1043 | + launchdQuitOptsForResidentEntry, |
| 1044 | + "tray-quit", |
| 1045 | + ); |
| 1046 | + return; |
| 1047 | + } |
| 1048 | + |
| 1049 | + (app as unknown as Record<string, unknown>).__nexuForceQuit = true; |
| 1050 | + app.quit(); |
| 1051 | + }, |
| 1052 | + }, |
| 1053 | + ]), |
| 1054 | + ); |
| 1055 | + residentTray.on("click", () => { |
| 1056 | + showMainWindowFromResidentEntry(); |
| 1057 | + }); |
| 1058 | +} |
| 1059 | + |
| 1060 | +function applyResidentEntryPreferences( |
| 1061 | + preferences: DesktopShellPreferences, |
| 1062 | +): void { |
| 1063 | + if (process.platform === "darwin") { |
| 1064 | + app.setActivationPolicy(preferences.showInDock ? "regular" : "accessory"); |
| 1065 | + } |
| 1066 | + |
| 1067 | + if (process.platform === "win32" && mainWindow && !mainWindow.isDestroyed()) { |
| 1068 | + mainWindow.setSkipTaskbar(!preferences.showInDock); |
| 1069 | + } |
| 1070 | + |
| 1071 | + if (shouldUseResidentEntry(preferences)) { |
| 1072 | + ensureResidentTray(); |
| 1073 | + } else { |
| 1074 | + destroyResidentTray(); |
| 1075 | + } |
| 1076 | +} |
| 1077 | + |
| 1078 | +function shouldHideOnWindowClose(): boolean { |
| 1079 | + if (!app.isPackaged) { |
| 1080 | + return false; |
| 1081 | + } |
| 1082 | + |
| 1083 | + if (process.platform === "darwin") { |
| 1084 | + return true; |
| 1085 | + } |
| 1086 | + |
| 1087 | + return shouldUseResidentEntry(getDesktopShellPreferences()); |
| 1088 | +} |
| 1089 | + |
949 | 1090 | app.on("second-instance", () => { |
950 | 1091 | if (!mainWindow || mainWindow.isDestroyed()) { |
951 | 1092 | createMainWindow(); |
952 | 1093 | return; |
953 | 1094 | } |
954 | 1095 |
|
| 1096 | + showMainWindowFromResidentEntry(); |
955 | 1097 | focusMainWindow(); |
956 | 1098 | }); |
957 | 1099 |
|
958 | 1100 | function createMainWindow(): BrowserWindow { |
959 | 1101 | logLaunchTimeline("main window creation requested"); |
960 | 1102 | const isMacOS = process.platform === "darwin"; |
| 1103 | + const shellPreferences = getDesktopShellPreferences(); |
961 | 1104 | const window = new BrowserWindow({ |
962 | 1105 | width: 1280, |
963 | 1106 | height: 720, |
@@ -986,6 +1129,10 @@ function createMainWindow(): BrowserWindow { |
986 | 1129 | }, |
987 | 1130 | }); |
988 | 1131 |
|
| 1132 | + if (process.platform === "win32") { |
| 1133 | + window.setSkipTaskbar(!shellPreferences.showInDock); |
| 1134 | + } |
| 1135 | + |
989 | 1136 | // Disable sandbox for webviews so preload scripts have access to Node.js APIs |
990 | 1137 | // (needed for contextBridge/ipcRenderer in ESM-built preloads) |
991 | 1138 | window.webContents.on( |
@@ -1108,6 +1255,17 @@ function createMainWindow(): BrowserWindow { |
1108 | 1255 | } |
1109 | 1256 | }); |
1110 | 1257 |
|
| 1258 | + window.on("close", (event) => { |
| 1259 | + if ((app as unknown as Record<string, unknown>).__nexuForceQuit) { |
| 1260 | + return; |
| 1261 | + } |
| 1262 | + |
| 1263 | + if (!launchdResult && shouldHideOnWindowClose()) { |
| 1264 | + event.preventDefault(); |
| 1265 | + hideMainWindowToBackground(); |
| 1266 | + } |
| 1267 | + }); |
| 1268 | + |
1111 | 1269 | // During first install / post-update, show the window IMMEDIATELY with a |
1112 | 1270 | // white background — before loadFile, before React, before anything. |
1113 | 1271 | // This eliminates the 10-20s blank screen while the Electron main process |
@@ -1284,6 +1442,10 @@ app.whenReady().then(async () => { |
1284 | 1442 | status: "ok", |
1285 | 1443 | detail: app.getVersion(), |
1286 | 1444 | }); |
| 1445 | + setDesktopShellPreferencesRuntimeHandler((preferences) => { |
| 1446 | + applyResidentEntryPreferences(preferences); |
| 1447 | + }); |
| 1448 | + applyDesktopShellPreferencesOnStartup(); |
1287 | 1449 | registerIpcHandlers( |
1288 | 1450 | orchestrator, |
1289 | 1451 | runtimeConfig, |
@@ -1380,6 +1542,7 @@ app.whenReady().then(async () => { |
1380 | 1542 | }; |
1381 | 1543 | installLaunchdQuitHandler(quitOpts); |
1382 | 1544 | setQuitHandlerOpts(quitOpts); |
| 1545 | + launchdQuitOptsForResidentEntry = quitOpts; |
1383 | 1546 | } |
1384 | 1547 |
|
1385 | 1548 | if (app.isPackaged && runtimeConfig.updates.autoUpdateEnabled) { |
@@ -1426,6 +1589,9 @@ app.whenReady().then(async () => { |
1426 | 1589 |
|
1427 | 1590 | app.on("window-all-closed", () => { |
1428 | 1591 | if (process.platform !== "darwin") { |
| 1592 | + if (shouldUseResidentEntry(getDesktopShellPreferences())) { |
| 1593 | + return; |
| 1594 | + } |
1429 | 1595 | app.quit(); |
1430 | 1596 | } |
1431 | 1597 | }); |
|
0 commit comments