Skip to content

Commit 0336e8d

Browse files
authored
feat: align desktop settings with workspace design (#984)
* feat: align desktop settings with workspace design * fix: address settings review feedback
1 parent 8e871b6 commit 0336e8d

14 files changed

Lines changed: 1345 additions & 517 deletions

File tree

apps/desktop/main/index.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import { execFileSync } from "node:child_process";
2+
import { existsSync } from "node:fs";
23
import { dirname, join, resolve } from "node:path";
34
import { fileURLToPath } from "node:url";
45
import * as Sentry from "@sentry/electron/main";
56
import {
67
BrowserWindow,
78
Menu,
89
type MenuItemConstructorOptions,
10+
Tray,
911
app,
1012
crashReporter,
1113
dialog,
1214
globalShortcut,
15+
nativeImage,
1316
nativeTheme,
1417
powerMonitor,
1518
powerSaveBlocker,
@@ -62,8 +65,15 @@ import {
6265
getDefaultPlistDir,
6366
getLogDir,
6467
installLaunchdQuitHandler,
68+
runTeardownAndExit,
6569
teardownLaunchdServices,
6670
} from "./services";
71+
import {
72+
type DesktopShellPreferences,
73+
applyDesktopShellPreferencesOnStartup,
74+
getDesktopShellPreferences,
75+
setDesktopShellPreferencesRuntimeHandler,
76+
} from "./services/desktop-shell-preferences";
6777
import { isLaunchdBootstrapEnabled } from "./services/launchd-bootstrap";
6878
import { ProxyManager } from "./services/proxy-manager";
6979
import {
@@ -277,6 +287,10 @@ if (sentryDsn) {
277287
}
278288

279289
let mainWindow: BrowserWindow | null = null;
290+
let residentTray: Tray | null = null;
291+
let launchdQuitOptsForResidentEntry:
292+
| Parameters<typeof installLaunchdQuitHandler>[0]
293+
| null = null;
280294
let diagnosticsReporter: DesktopDiagnosticsReporter | null = null;
281295

282296
/** True if this is the x86_64 build running under Rosetta 2 on Apple Silicon. */
@@ -946,18 +960,147 @@ function focusMainWindow(): void {
946960
mainWindow.focus();
947961
}
948962

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+
9491090
app.on("second-instance", () => {
9501091
if (!mainWindow || mainWindow.isDestroyed()) {
9511092
createMainWindow();
9521093
return;
9531094
}
9541095

1096+
showMainWindowFromResidentEntry();
9551097
focusMainWindow();
9561098
});
9571099

9581100
function createMainWindow(): BrowserWindow {
9591101
logLaunchTimeline("main window creation requested");
9601102
const isMacOS = process.platform === "darwin";
1103+
const shellPreferences = getDesktopShellPreferences();
9611104
const window = new BrowserWindow({
9621105
width: 1280,
9631106
height: 720,
@@ -986,6 +1129,10 @@ function createMainWindow(): BrowserWindow {
9861129
},
9871130
});
9881131

1132+
if (process.platform === "win32") {
1133+
window.setSkipTaskbar(!shellPreferences.showInDock);
1134+
}
1135+
9891136
// Disable sandbox for webviews so preload scripts have access to Node.js APIs
9901137
// (needed for contextBridge/ipcRenderer in ESM-built preloads)
9911138
window.webContents.on(
@@ -1108,6 +1255,17 @@ function createMainWindow(): BrowserWindow {
11081255
}
11091256
});
11101257

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+
11111269
// During first install / post-update, show the window IMMEDIATELY with a
11121270
// white background — before loadFile, before React, before anything.
11131271
// This eliminates the 10-20s blank screen while the Electron main process
@@ -1284,6 +1442,10 @@ app.whenReady().then(async () => {
12841442
status: "ok",
12851443
detail: app.getVersion(),
12861444
});
1445+
setDesktopShellPreferencesRuntimeHandler((preferences) => {
1446+
applyResidentEntryPreferences(preferences);
1447+
});
1448+
applyDesktopShellPreferencesOnStartup();
12871449
registerIpcHandlers(
12881450
orchestrator,
12891451
runtimeConfig,
@@ -1380,6 +1542,7 @@ app.whenReady().then(async () => {
13801542
};
13811543
installLaunchdQuitHandler(quitOpts);
13821544
setQuitHandlerOpts(quitOpts);
1545+
launchdQuitOptsForResidentEntry = quitOpts;
13831546
}
13841547

13851548
if (app.isPackaged && runtimeConfig.updates.autoUpdateEnabled) {
@@ -1426,6 +1589,9 @@ app.whenReady().then(async () => {
14261589

14271590
app.on("window-all-closed", () => {
14281591
if (process.platform !== "darwin") {
1592+
if (shouldUseResidentEntry(getDesktopShellPreferences())) {
1593+
return;
1594+
}
14291595
app.quit();
14301596
}
14311597
});

apps/desktop/main/ipc.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ import type { DesktopRuntimeConfig } from "../shared/runtime-config";
1717
import type { DesktopDiagnosticsReporter } from "./desktop-diagnostics";
1818
import { exportDiagnostics } from "./diagnostics-export";
1919
import type { RuntimeOrchestrator } from "./runtime/daemon-supervisor";
20+
import {
21+
getDesktopShellPreferences,
22+
updateDesktopShellPreferences,
23+
} from "./services/desktop-shell-preferences";
2024
import {
2125
type QuitHandlerOptions,
2226
runTeardownAndExit,
@@ -425,6 +429,16 @@ export function registerIpcHandlers(
425429
);
426430
}
427431

432+
case "desktop:get-shell-preferences": {
433+
return getDesktopShellPreferences();
434+
}
435+
436+
case "desktop:update-shell-preferences": {
437+
const typedPayload =
438+
payload as HostInvokePayloadMap["desktop:update-shell-preferences"];
439+
return updateDesktopShellPreferences(typedPayload);
440+
}
441+
428442
case "desktop:get-rewards-status": {
429443
return fetchControllerJson<
430444
HostInvokeResultMap["desktop:get-rewards-status"]

0 commit comments

Comments
 (0)