Skip to content

Commit 4777956

Browse files
juliusmarmingeJulius Marmingecodexclaude
authored
refactor: resolve host process state through Effect (#2959)
Co-authored-by: Julius Marminge <julius@mac.lan> Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a23b833 commit 4777956

87 files changed

Lines changed: 2180 additions & 2124 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/desktop/scripts/dev-electron.mjs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { spawn, spawnSync } from "node:child_process";
22
import { watch } from "node:fs";
3+
import * as NodeOS from "node:os";
34
import { join } from "node:path";
45

56
import {
@@ -33,6 +34,8 @@ const forcedShutdownTimeoutMs = 1_500;
3334
const restartDebounceMs = 120;
3435
const childTreeGracePeriodMs = 1_200;
3536
const remoteDebuggingPort = process.env.T3CODE_DESKTOP_REMOTE_DEBUGGING_PORT?.trim();
37+
// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone dev script has no Effect runtime.
38+
const hostPlatform = NodeOS.platform();
3639

3740
await waitForResources({
3841
baseDir: desktopDir,
@@ -57,15 +60,15 @@ const expectedExits = new WeakSet();
5760
const watchers = [];
5861

5962
function killChildTreeByPid(pid, signal) {
60-
if (process.platform === "win32" || typeof pid !== "number") {
63+
if (hostPlatform === "win32" || typeof pid !== "number") {
6164
return;
6265
}
6366

6467
spawnSync("pkill", [`-${signal}`, "-P", String(pid)], { stdio: "ignore" });
6568
}
6669

6770
function cleanupStaleDevApps() {
68-
if (process.platform === "win32") {
71+
if (hostPlatform === "win32") {
6972
return;
7073
}
7174

@@ -194,7 +197,7 @@ function startWatchers() {
194197
}
195198

196199
function killChildTree(signal) {
197-
if (process.platform === "win32") {
200+
if (hostPlatform === "win32") {
198201
return;
199202
}
200203

apps/desktop/scripts/electron-launcher.mjs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
writeFileSync,
1515
} from "node:fs";
1616
import { createRequire } from "node:module";
17+
import * as NodeOS from "node:os";
1718
import { basename, dirname, join, resolve } from "node:path";
1819
import { fileURLToPath } from "node:url";
1920
import { ensureElectronRuntime } from "./ensure-electron-runtime.mjs";
@@ -33,6 +34,8 @@ const APP_PROTOCOL_SCHEMES = isDevelopment ? ["t3code-dev"] : ["t3code"];
3334
const LAUNCHER_VERSION = 11;
3435
const defaultIconPath = join(desktopDir, "resources", "icon.icns");
3536
const developmentMacIconPngPath = join(repoRoot, "assets", "dev", "blueprint-macos-1024.png");
37+
// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone launcher script has no Effect runtime.
38+
const hostPlatform = NodeOS.platform();
3639

3740
function resolveDevelopmentProtocolCallbackPort() {
3841
const configuredPort = Number.parseInt(process.env.T3CODE_PORT ?? "", 10);
@@ -312,7 +315,7 @@ function buildMacLauncher(electronBinaryPath) {
312315
}
313316

314317
function isLinuxSetuidSandboxConfigured(electronBinaryPath) {
315-
if (process.platform !== "linux") {
318+
if (hostPlatform !== "linux") {
316319
return true;
317320
}
318321

@@ -342,7 +345,7 @@ export function resolveElectronPath() {
342345
const require = createRequire(import.meta.url);
343346
const electronBinaryPath = require("electron");
344347

345-
if (process.platform !== "darwin") {
348+
if (hostPlatform !== "darwin") {
346349
return electronBinaryPath;
347350
}
348351

@@ -358,7 +361,7 @@ export function resolveElectronLaunchCommand(args = []) {
358361
}
359362

360363
export function resolveDevProtocolClient() {
361-
if (process.platform !== "darwin" || !isDevelopment) {
364+
if (hostPlatform !== "darwin" || !isDevelopment) {
362365
return null;
363366
}
364367

apps/desktop/scripts/ensure-electron-runtime.mjs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
22
import { createRequire } from "node:module";
3-
import { tmpdir } from "node:os";
3+
import { arch, platform, tmpdir } from "node:os";
44
import { dirname, join } from "node:path";
55
import { spawnSync } from "node:child_process";
66

77
const require = createRequire(import.meta.url);
8+
// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone repair script has no Effect runtime.
9+
const hostPlatform = platform();
10+
// oxlint-disable-next-line t3code/no-global-process-runtime -- Standalone repair script has no Effect runtime.
11+
const hostArch = arch();
812

913
function getPlatformPath() {
10-
switch (process.platform) {
14+
switch (hostPlatform) {
1115
case "darwin":
1216
return "Electron.app/Contents/MacOS/Electron";
1317
case "freebsd":
@@ -17,12 +21,12 @@ function getPlatformPath() {
1721
case "win32":
1822
return "electron.exe";
1923
default:
20-
throw new Error(`Electron builds are not available on platform: ${process.platform}`);
24+
throw new Error(`Electron builds are not available on platform: ${hostPlatform}`);
2125
}
2226
}
2327

2428
function ensureExecutable(filePath) {
25-
if (process.platform !== "win32") {
29+
if (hostPlatform !== "win32") {
2630
chmodSync(filePath, 0o755);
2731
}
2832
}
@@ -39,7 +43,7 @@ function repairPathFile(electronDir, platformPath) {
3943
function getRequiredRuntimePaths(electronDir, platformPath) {
4044
const paths = [join(electronDir, "dist", platformPath)];
4145

42-
if (process.platform === "darwin") {
46+
if (hostPlatform === "darwin") {
4347
paths.push(
4448
join(electronDir, "dist", "Electron.app", "Contents", "Info.plist"),
4549
join(
@@ -58,7 +62,7 @@ function getRequiredRuntimePaths(electronDir, platformPath) {
5862
}
5963

6064
function isMachO(filePath) {
61-
if (process.platform !== "darwin") {
65+
if (hostPlatform !== "darwin") {
6266
return true;
6367
}
6468

@@ -76,7 +80,7 @@ function missingRuntimePaths(electronDir, platformPath) {
7680
}
7781

7882
function invalidRuntimePaths(electronDir, platformPath) {
79-
if (process.platform !== "darwin") {
83+
if (hostPlatform !== "darwin") {
8084
return [];
8185
}
8286

@@ -111,16 +115,16 @@ function runChecked(command, args) {
111115

112116
function installElectronRuntime(electronDir, version) {
113117
const tempDir = mkdtempSync(join(tmpdir(), "t3-electron-"));
114-
const zipPath = join(tempDir, `electron-v${version}-${process.platform}-${process.arch}.zip`);
118+
const zipPath = join(tempDir, `electron-v${version}-${hostPlatform}-${hostArch}.zip`);
115119

116120
try {
117121
runChecked("curl", [
118122
"-fsSL",
119-
`https://github.com/electron/electron/releases/download/v${version}/electron-v${version}-${process.platform}-${process.arch}.zip`,
123+
`https://github.com/electron/electron/releases/download/v${version}/electron-v${version}-${hostPlatform}-${hostArch}.zip`,
120124
"-o",
121125
zipPath,
122126
]);
123-
if (process.platform === "darwin") {
127+
if (hostPlatform === "darwin") {
124128
runChecked("ditto", ["-x", "-k", zipPath, join(electronDir, "dist")]);
125129
} else {
126130
runChecked("python3", [

apps/desktop/src/app/DesktopAssets.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ const resolveIconPath = Effect.fn("desktop.assets.resolveIconPath")(function* (
4949
> {
5050
const fileSystem = yield* FileSystem.FileSystem;
5151
const environment = yield* DesktopEnvironment.DesktopEnvironment;
52-
if (environment.isDevelopment && process.platform === "darwin" && ext === "png") {
52+
if (environment.isDevelopment && environment.platform === "darwin" && ext === "png") {
5353
const developmentDockIconPath = environment.developmentDockIconPath;
5454
const developmentDockIconExists = yield* fileSystem
5555
.exists(developmentDockIconPath)

apps/desktop/src/electron/ElectronMenu.ts

Lines changed: 99 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as Layer from "effect/Layer";
55
import * as Option from "effect/Option";
66

77
import * as Electron from "electron";
8+
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
89

910
export interface ElectronMenuPosition {
1011
readonly x: number;
@@ -79,109 +80,113 @@ const normalizePosition = (
7980
({ x, y }) => Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0,
8081
).pipe(Option.map(({ x, y }) => ({ x: Math.floor(x), y: Math.floor(y) })));
8182

82-
export const layer = Layer.sync(ElectronMenu, () => {
83-
let destructiveMenuIconCache: Option.Option<Electron.NativeImage> | undefined;
83+
export const layer = Layer.effect(
84+
ElectronMenu,
85+
Effect.gen(function* () {
86+
const platform = yield* HostProcessPlatform;
87+
let destructiveMenuIconCache: Option.Option<Electron.NativeImage> | undefined;
8488

85-
const getDestructiveMenuIcon = (): Option.Option<Electron.NativeImage> => {
86-
if (process.platform !== "darwin") {
87-
return Option.none();
88-
}
89-
if (destructiveMenuIconCache !== undefined) {
90-
return destructiveMenuIconCache;
91-
}
92-
93-
try {
94-
const icon = Electron.nativeImage.createFromNamedImage("trash").resize({
95-
width: 12,
96-
height: 12,
97-
});
98-
destructiveMenuIconCache = icon.isEmpty() ? Option.none() : Option.some(icon);
99-
} catch {
100-
destructiveMenuIconCache = Option.none();
101-
}
102-
103-
return destructiveMenuIconCache;
104-
};
105-
106-
const buildTemplate = (
107-
entries: readonly ContextMenuItem[],
108-
complete: (selectedItemId: Option.Option<string>) => void,
109-
): Electron.MenuItemConstructorOptions[] => {
110-
const template: Electron.MenuItemConstructorOptions[] = [];
111-
let hasInsertedDestructiveSeparator = false;
112-
113-
for (const item of entries) {
114-
if (item.destructive && !hasInsertedDestructiveSeparator && template.length > 0) {
115-
template.push({ type: "separator" });
116-
hasInsertedDestructiveSeparator = true;
89+
const getDestructiveMenuIcon = (): Option.Option<Electron.NativeImage> => {
90+
if (platform !== "darwin") {
91+
return Option.none();
11792
}
118-
119-
const itemOption: Electron.MenuItemConstructorOptions = {
120-
label: item.label,
121-
enabled: !item.disabled,
122-
};
123-
if (item.children && item.children.length > 0) {
124-
itemOption.submenu = buildTemplate(item.children, complete);
125-
} else {
126-
itemOption.click = () => complete(Option.some(item.id));
93+
if (destructiveMenuIconCache !== undefined) {
94+
return destructiveMenuIconCache;
12795
}
128-
if (item.destructive && (!item.children || item.children.length === 0)) {
129-
const destructiveIcon = getDestructiveMenuIcon();
130-
if (Option.isSome(destructiveIcon)) {
131-
itemOption.icon = destructiveIcon.value;
132-
}
96+
97+
try {
98+
const icon = Electron.nativeImage.createFromNamedImage("trash").resize({
99+
width: 12,
100+
height: 12,
101+
});
102+
destructiveMenuIconCache = icon.isEmpty() ? Option.none() : Option.some(icon);
103+
} catch {
104+
destructiveMenuIconCache = Option.none();
133105
}
134106

135-
template.push(itemOption);
136-
}
107+
return destructiveMenuIconCache;
108+
};
137109

138-
return template;
139-
};
140-
141-
return ElectronMenu.of({
142-
setApplicationMenu: (template) =>
143-
Effect.sync(() => {
144-
Electron.Menu.setApplicationMenu(Electron.Menu.buildFromTemplate([...template]));
145-
}),
146-
popupTemplate: (input) =>
147-
Effect.sync(() => {
148-
if (input.template.length === 0) {
149-
return;
110+
const buildTemplate = (
111+
entries: readonly ContextMenuItem[],
112+
complete: (selectedItemId: Option.Option<string>) => void,
113+
): Electron.MenuItemConstructorOptions[] => {
114+
const template: Electron.MenuItemConstructorOptions[] = [];
115+
let hasInsertedDestructiveSeparator = false;
116+
117+
for (const item of entries) {
118+
if (item.destructive && !hasInsertedDestructiveSeparator && template.length > 0) {
119+
template.push({ type: "separator" });
120+
hasInsertedDestructiveSeparator = true;
150121
}
151-
Electron.Menu.buildFromTemplate([...input.template]).popup({ window: input.window });
152-
}),
153-
showContextMenu: (input) =>
154-
Effect.callback<Option.Option<string>>((resume) => {
155-
const normalizedItems = normalizeContextMenuItems(input.items);
156-
if (normalizedItems.length === 0) {
157-
resume(Effect.succeed(Option.none()));
158-
return;
122+
123+
const itemOption: Electron.MenuItemConstructorOptions = {
124+
label: item.label,
125+
enabled: !item.disabled,
126+
};
127+
if (item.children && item.children.length > 0) {
128+
itemOption.submenu = buildTemplate(item.children, complete);
129+
} else {
130+
itemOption.click = () => complete(Option.some(item.id));
159131
}
132+
if (item.destructive && (!item.children || item.children.length === 0)) {
133+
const destructiveIcon = getDestructiveMenuIcon();
134+
if (Option.isSome(destructiveIcon)) {
135+
itemOption.icon = destructiveIcon.value;
136+
}
137+
}
138+
139+
template.push(itemOption);
140+
}
160141

161-
let completed = false;
162-
const complete = (selectedItemId: Option.Option<string>) => {
163-
if (completed) {
142+
return template;
143+
};
144+
145+
return ElectronMenu.of({
146+
setApplicationMenu: (template) =>
147+
Effect.sync(() => {
148+
Electron.Menu.setApplicationMenu(Electron.Menu.buildFromTemplate([...template]));
149+
}),
150+
popupTemplate: (input) =>
151+
Effect.sync(() => {
152+
if (input.template.length === 0) {
153+
return;
154+
}
155+
Electron.Menu.buildFromTemplate([...input.template]).popup({ window: input.window });
156+
}),
157+
showContextMenu: (input) =>
158+
Effect.callback<Option.Option<string>>((resume) => {
159+
const normalizedItems = normalizeContextMenuItems(input.items);
160+
if (normalizedItems.length === 0) {
161+
resume(Effect.succeed(Option.none()));
164162
return;
165163
}
166-
completed = true;
167-
resume(Effect.succeed(selectedItemId));
168-
};
169164

170-
const menu = Electron.Menu.buildFromTemplate(buildTemplate(normalizedItems, complete));
171-
const popupPosition = normalizePosition(input.position);
172-
const popupOptions = Option.match(popupPosition, {
173-
onNone: (): Electron.PopupOptions => ({
174-
window: input.window,
175-
callback: () => complete(Option.none()),
176-
}),
177-
onSome: (position): Electron.PopupOptions => ({
178-
window: input.window,
179-
x: position.x,
180-
y: position.y,
181-
callback: () => complete(Option.none()),
182-
}),
183-
});
184-
menu.popup(popupOptions);
185-
}),
186-
});
187-
});
165+
let completed = false;
166+
const complete = (selectedItemId: Option.Option<string>) => {
167+
if (completed) {
168+
return;
169+
}
170+
completed = true;
171+
resume(Effect.succeed(selectedItemId));
172+
};
173+
174+
const menu = Electron.Menu.buildFromTemplate(buildTemplate(normalizedItems, complete));
175+
const popupPosition = normalizePosition(input.position);
176+
const popupOptions = Option.match(popupPosition, {
177+
onNone: (): Electron.PopupOptions => ({
178+
window: input.window,
179+
callback: () => complete(Option.none()),
180+
}),
181+
onSome: (position): Electron.PopupOptions => ({
182+
window: input.window,
183+
x: position.x,
184+
y: position.y,
185+
callback: () => complete(Option.none()),
186+
}),
187+
});
188+
menu.popup(popupOptions);
189+
}),
190+
});
191+
}),
192+
);

0 commit comments

Comments
 (0)