authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 01:24:04-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 01:24:04-07:00
logb19fc1b87b22a3c923e60c2ef2a1dae336279924
tree409474b8aa0d7d1a750c0a7edf1174ac9237f11f
parent2fa37ec183e0b99ca2ea77efbcb615f11019da75
signature Signed by PGP key B5690EEEBB952194

[codex] refactor desktop Electron Effect services (#3178)

Co-authored-by: codex <codex@users.noreply.github.com>

21 files changed, 351 insertions(+), 353 deletions(-)

apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts-3
...@@ -237,7 +237,6 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -237,7 +237,6 @@ describe("DesktopConnectionCatalogStore", () => {
237 error,237 error,
238 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreDocumentDecodeError,238 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreDocumentDecodeError,
239 );239 );
240 assert.equal(error.operation, "decode-catalog-document");
241 assert.equal(error.catalogPath, catalogPath);240 assert.equal(error.catalogPath, catalogPath);
242 assert.exists(error.cause);241 assert.exists(error.cause);
243 assert.equal(yield* fileSystem.readFileString(catalogPath), "{not-json");242 assert.equal(yield* fileSystem.readFileString(catalogPath), "{not-json");
...@@ -272,7 +271,6 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -272,7 +271,6 @@ describe("DesktopConnectionCatalogStore", () => {
272 error,271 error,
273 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreReadError,272 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreReadError,
274 );273 );
275 assert.equal(error.operation, "read-catalog");
276 assert.equal(error.catalogPath, `${baseDir}/userdata/connection-catalog.json`);274 assert.equal(error.catalogPath, `${baseDir}/userdata/connection-catalog.json`);
277 assert.strictEqual(error.cause, permissionError);275 assert.strictEqual(error.cause, permissionError);
278 assert.equal(276 assert.equal(
...@@ -368,7 +366,6 @@ describe("DesktopConnectionCatalogStore", () => {...@@ -368,7 +366,6 @@ describe("DesktopConnectionCatalogStore", () => {
368 error,366 error,
369 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreDecodeError,367 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreDecodeError,
370 );368 );
371 assert.equal(error.operation, "decode-encrypted-catalog");
372 assert.equal(error.resource, "encryptedCatalog");369 assert.equal(error.resource, "encryptedCatalog");
373 assert.equal(error.catalogPath, catalogPath);370 assert.equal(error.catalogPath, catalogPath);
374 assert.exists(error.cause);371 assert.exists(error.cause);
apps/desktop/src/app/DesktopConnectionCatalogStore.ts+2-11
...@@ -92,7 +92,6 @@ const writeError = (...@@ -92,7 +92,6 @@ const writeError = (
92export class DesktopConnectionCatalogStoreDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDecodeError>()(92export class DesktopConnectionCatalogStoreDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDecodeError>()(
93 "DesktopConnectionCatalogStoreDecodeError",93 "DesktopConnectionCatalogStoreDecodeError",
94 {94 {
95 operation: Schema.Literal("decode-encrypted-catalog"),
96 resource: Schema.Literal("encryptedCatalog"),95 resource: Schema.Literal("encryptedCatalog"),
97 catalogPath: Schema.String,96 catalogPath: Schema.String,
98 cause: Schema.Defect(),97 cause: Schema.Defect(),
...@@ -106,7 +105,6 @@ export class DesktopConnectionCatalogStoreDecodeError extends Schema.TaggedError...@@ -106,7 +105,6 @@ export class DesktopConnectionCatalogStoreDecodeError extends Schema.TaggedError
106export class DesktopConnectionCatalogStoreReadError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreReadError>()(105export class DesktopConnectionCatalogStoreReadError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreReadError>()(
107 "DesktopConnectionCatalogStoreReadError",106 "DesktopConnectionCatalogStoreReadError",
108 {107 {
109 operation: Schema.Literal("read-catalog"),
110 catalogPath: Schema.String,108 catalogPath: Schema.String,
111 cause: Schema.Defect(),109 cause: Schema.Defect(),
112 },110 },
...@@ -119,7 +117,6 @@ export class DesktopConnectionCatalogStoreReadError extends Schema.TaggedErrorCl...@@ -119,7 +117,6 @@ export class DesktopConnectionCatalogStoreReadError extends Schema.TaggedErrorCl
119export class DesktopConnectionCatalogStoreDocumentDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDocumentDecodeError>()(117export class DesktopConnectionCatalogStoreDocumentDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDocumentDecodeError>()(
120 "DesktopConnectionCatalogStoreDocumentDecodeError",118 "DesktopConnectionCatalogStoreDocumentDecodeError",
121 {119 {
122 operation: Schema.Literal("decode-catalog-document"),
123 catalogPath: Schema.String,120 catalogPath: Schema.String,
124 cause: Schema.Defect(),121 cause: Schema.Defect(),
125 },122 },
...@@ -167,16 +164,13 @@ export class DesktopConnectionCatalogStore extends Context.Service<...@@ -167,16 +164,13 @@ export class DesktopConnectionCatalogStore extends Context.Service<
167 | DesktopConnectionCatalogStoreDocumentDecodeError164 | DesktopConnectionCatalogStoreDocumentDecodeError
168 | DesktopConnectionCatalogStoreDecodeError165 | DesktopConnectionCatalogStoreDecodeError
169 | DesktopConnectionCatalogStoreMigrationError166 | DesktopConnectionCatalogStoreMigrationError
170 | ElectronSafeStorage.ElectronSafeStorageAvailabilityError167 | ElectronSafeStorage.ElectronSafeStorageError
171 | ElectronSafeStorage.ElectronSafeStorageDecryptError
172 >;168 >;
173 readonly set: (169 readonly set: (
174 catalog: string,170 catalog: string,
175 ) => Effect.Effect<171 ) => Effect.Effect<
176 boolean,172 boolean,
177 | DesktopConnectionCatalogStoreWriteError173 DesktopConnectionCatalogStoreWriteError | ElectronSafeStorage.ElectronSafeStorageError
178 | ElectronSafeStorage.ElectronSafeStorageAvailabilityError
179 | ElectronSafeStorage.ElectronSafeStorageEncryptError
180 >;174 >;
181 readonly clear: Effect.Effect<void>;175 readonly clear: Effect.Effect<void>;
182 }176 }
...@@ -190,7 +184,6 @@ function decodeSecretBytes(...@@ -190,7 +184,6 @@ function decodeSecretBytes(
190 Effect.mapError(184 Effect.mapError(
191 (cause) =>185 (cause) =>
192 new DesktopConnectionCatalogStoreDecodeError({186 new DesktopConnectionCatalogStoreDecodeError({
193 operation: "decode-encrypted-catalog",
194 resource: "encryptedCatalog",187 resource: "encryptedCatalog",
195 catalogPath,188 catalogPath,
196 cause,189 cause,
...@@ -212,7 +205,6 @@ const readDocument = (...@@ -212,7 +205,6 @@ const readDocument = (
212 ? Effect.succeed<string | null>(null)205 ? Effect.succeed<string | null>(null)
213 : Effect.fail(206 : Effect.fail(
214 new DesktopConnectionCatalogStoreReadError({207 new DesktopConnectionCatalogStoreReadError({
215 operation: "read-catalog",
216 catalogPath,208 catalogPath,
217 cause: error,209 cause: error,
218 }),210 }),
...@@ -226,7 +218,6 @@ const readDocument = (...@@ -226,7 +218,6 @@ const readDocument = (
226 Effect.mapError(218 Effect.mapError(
227 (cause) =>219 (cause) =>
228 new DesktopConnectionCatalogStoreDocumentDecodeError({220 new DesktopConnectionCatalogStoreDocumentDecodeError({
229 operation: "decode-catalog-document",
230 catalogPath,221 catalogPath,
231 cause,222 cause,
232 }),223 }),
apps/desktop/src/app/DesktopLifecycle.ts+3-3
...@@ -8,7 +8,7 @@ import * as Scope from "effect/Scope";...@@ -8,7 +8,7 @@ import * as Scope from "effect/Scope";
8import type * as Electron from "electron";8import type * as Electron from "electron";
99
10import * as DesktopEnvironment from "./DesktopEnvironment.ts";10import * as DesktopEnvironment from "./DesktopEnvironment.ts";
11import * as DesktopObservability from "./DesktopObservability.ts";11import { makeComponentLogger } from "./DesktopObservability.ts";
12import * as DesktopShutdown from "./DesktopShutdown.ts";12import * as DesktopShutdown from "./DesktopShutdown.ts";
13import * as ElectronApp from "../electron/ElectronApp.ts";13import * as ElectronApp from "../electron/ElectronApp.ts";
14import * as ElectronTheme from "../electron/ElectronTheme.ts";14import * as ElectronTheme from "../electron/ElectronTheme.ts";
...@@ -37,7 +37,7 @@ export class DesktopLifecycle extends Context.Service<...@@ -37,7 +37,7 @@ export class DesktopLifecycle extends Context.Service<
37>()("@t3tools/desktop/app/DesktopLifecycle") {}37>()("@t3tools/desktop/app/DesktopLifecycle") {}
3838
39const { logInfo: logLifecycleInfo, logError: logLifecycleError } =39const { logInfo: logLifecycleInfo, logError: logLifecycleError } =
40 DesktopObservability.makeComponentLogger("desktop-lifecycle");40 makeComponentLogger("desktop-lifecycle");
4141
42function addScopedListener<Args extends ReadonlyArray<unknown>>(42function addScopedListener<Args extends ReadonlyArray<unknown>>(
43 target: unknown,43 target: unknown,
...@@ -122,7 +122,7 @@ function quitFromSignal(...@@ -122,7 +122,7 @@ function quitFromSignal(
122 );122 );
123}123}
124124
125const make = DesktopLifecycle.of({125export const make = DesktopLifecycle.of({
126 relaunch: Effect.fn("desktop.lifecycle.relaunch")(function* (reason) {126 relaunch: Effect.fn("desktop.lifecycle.relaunch")(function* (reason) {
127 const electronApp = yield* ElectronApp.ElectronApp;127 const electronApp = yield* ElectronApp.ElectronApp;
128 const environment = yield* DesktopEnvironment.DesktopEnvironment;128 const environment = yield* DesktopEnvironment.DesktopEnvironment;
apps/desktop/src/electron/ElectronApp.ts+35-36
...@@ -13,41 +13,40 @@ export interface ElectronAppMetadata {...@@ -13,41 +13,40 @@ export interface ElectronAppMetadata {
13 readonly runningUnderArm64Translation: boolean;13 readonly runningUnderArm64Translation: boolean;
14}14}
1515
16export interface ElectronAppShape {16export class ElectronApp extends Context.Service<
17 readonly metadata: Effect.Effect<ElectronAppMetadata>;17 ElectronApp,
18 readonly name: Effect.Effect<string>;18 {
19 readonly whenReady: Effect.Effect<void>;19 readonly metadata: Effect.Effect<ElectronAppMetadata>;
20 readonly quit: Effect.Effect<void>;20 readonly name: Effect.Effect<string>;
21 readonly exit: (code: number) => Effect.Effect<void>;21 readonly whenReady: Effect.Effect<void>;
22 readonly relaunch: (options: Electron.RelaunchOptions) => Effect.Effect<void>;22 readonly quit: Effect.Effect<void>;
23 readonly setPath: (23 readonly exit: (code: number) => Effect.Effect<void>;
24 name: Parameters<Electron.App["setPath"]>[0],24 readonly relaunch: (options: Electron.RelaunchOptions) => Effect.Effect<void>;
25 path: string,25 readonly setPath: (
26 ) => Effect.Effect<void>;26 name: Parameters<Electron.App["setPath"]>[0],
27 readonly setName: (name: string) => Effect.Effect<void>;27 path: string,
28 readonly setAboutPanelOptions: (28 ) => Effect.Effect<void>;
29 options: Electron.AboutPanelOptionsOptions,29 readonly setName: (name: string) => Effect.Effect<void>;
30 ) => Effect.Effect<void>;30 readonly setAboutPanelOptions: (
31 readonly setAppUserModelId: (id: string) => Effect.Effect<void>;31 options: Electron.AboutPanelOptionsOptions,
32 readonly requestSingleInstanceLock: Effect.Effect<boolean>;32 ) => Effect.Effect<void>;
33 readonly isDefaultProtocolClient: (protocol: string) => Effect.Effect<boolean>;33 readonly setAppUserModelId: (id: string) => Effect.Effect<void>;
34 readonly setAsDefaultProtocolClient: (34 readonly requestSingleInstanceLock: Effect.Effect<boolean>;
35 protocol: string,35 readonly isDefaultProtocolClient: (protocol: string) => Effect.Effect<boolean>;
36 path?: string,36 readonly setAsDefaultProtocolClient: (
37 args?: readonly string[],37 protocol: string,
38 ) => Effect.Effect<boolean>;38 path?: string,
39 readonly setDesktopName: (desktopName: string) => Effect.Effect<void>;39 args?: readonly string[],
40 readonly setDockIcon: (iconPath: string) => Effect.Effect<void>;40 ) => Effect.Effect<boolean>;
41 readonly appendCommandLineSwitch: (switchName: string, value?: string) => Effect.Effect<void>;41 readonly setDesktopName: (desktopName: string) => Effect.Effect<void>;
42 readonly on: <Args extends ReadonlyArray<unknown>>(42 readonly setDockIcon: (iconPath: string) => Effect.Effect<void>;
43 eventName: string,43 readonly appendCommandLineSwitch: (switchName: string, value?: string) => Effect.Effect<void>;
44 listener: (...args: Args) => void,44 readonly on: <Args extends ReadonlyArray<unknown>>(
45 ) => Effect.Effect<void, never, Scope.Scope>;45 eventName: string,
46}46 listener: (...args: Args) => void,
4747 ) => Effect.Effect<void, never, Scope.Scope>;
48export class ElectronApp extends Context.Service<ElectronApp, ElectronAppShape>()(48 }
49 "@t3tools/desktop/electron/ElectronApp",49>()("@t3tools/desktop/electron/ElectronApp") {}
50) {}
5150
52const addScopedAppListener = <Args extends ReadonlyArray<unknown>>(51const addScopedAppListener = <Args extends ReadonlyArray<unknown>>(
53 eventName: string,52 eventName: string,
...@@ -63,7 +62,7 @@ const addScopedAppListener = <Args extends ReadonlyArray<unknown>>(...@@ -63,7 +62,7 @@ const addScopedAppListener = <Args extends ReadonlyArray<unknown>>(
63 }),62 }),
64 ).pipe(Effect.asVoid);63 ).pipe(Effect.asVoid);
6564
66const make = ElectronApp.of({65export const make = ElectronApp.of({
67 metadata: Effect.sync(() => ({66 metadata: Effect.sync(() => ({
68 appVersion: Electron.app.getVersion(),67 appVersion: Electron.app.getVersion(),
69 appPath: Electron.app.getAppPath(),68 appPath: Electron.app.getAppPath(),
apps/desktop/src/electron/ElectronDialog.ts+14-15
...@@ -17,22 +17,21 @@ export interface ElectronDialogConfirmInput {...@@ -17,22 +17,21 @@ export interface ElectronDialogConfirmInput {
17 readonly message: string;17 readonly message: string;
18}18}
1919
20export interface ElectronDialogShape {20export class ElectronDialog extends Context.Service<
21 readonly pickFolder: (21 ElectronDialog,
22 input: ElectronDialogPickFolderInput,22 {
23 ) => Effect.Effect<Option.Option<string>>;23 readonly pickFolder: (
24 readonly confirm: (input: ElectronDialogConfirmInput) => Effect.Effect<boolean>;24 input: ElectronDialogPickFolderInput,
25 readonly showMessageBox: (25 ) => Effect.Effect<Option.Option<string>>;
26 options: Electron.MessageBoxOptions,26 readonly confirm: (input: ElectronDialogConfirmInput) => Effect.Effect<boolean>;
27 ) => Effect.Effect<Electron.MessageBoxReturnValue>;27 readonly showMessageBox: (
28 readonly showErrorBox: (title: string, content: string) => Effect.Effect<void>;28 options: Electron.MessageBoxOptions,
29}29 ) => Effect.Effect<Electron.MessageBoxReturnValue>;
3030 readonly showErrorBox: (title: string, content: string) => Effect.Effect<void>;
31export class ElectronDialog extends Context.Service<ElectronDialog, ElectronDialogShape>()(31 }
32 "@t3tools/desktop/electron/ElectronDialog",32>()("@t3tools/desktop/electron/ElectronDialog") {}
33) {}
3433
35const make = ElectronDialog.of({34export const make = ElectronDialog.of({
36 pickFolder: Effect.fn("desktop.electron.dialog.pickFolder")(function* (input) {35 pickFolder: Effect.fn("desktop.electron.dialog.pickFolder")(function* (input) {
37 const openDialogOptions: Electron.OpenDialogOptions = Option.match(input.defaultPath, {36 const openDialogOptions: Electron.OpenDialogOptions = Option.match(input.defaultPath, {
38 onNone: () => ({37 onNone: () => ({
apps/desktop/src/electron/ElectronMenu.ts+111-113
...@@ -1,11 +1,11 @@...@@ -1,11 +1,11 @@
1import type { ContextMenuItem } from "@t3tools/contracts";1import type { ContextMenuItem } from "@t3tools/contracts";
2import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
2import * as Context from "effect/Context";3import * as Context from "effect/Context";
3import * as Effect from "effect/Effect";4import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";5import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";6import * as Option from "effect/Option";
67
7import * as Electron from "electron";8import * as Electron from "electron";
8import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
99
10export interface ElectronMenuPosition {10export interface ElectronMenuPosition {
11 readonly x: number;11 readonly x: number;
...@@ -23,19 +23,18 @@ export interface ElectronMenuTemplateInput {...@@ -23,19 +23,18 @@ export interface ElectronMenuTemplateInput {
23 readonly template: readonly Electron.MenuItemConstructorOptions[];23 readonly template: readonly Electron.MenuItemConstructorOptions[];
24}24}
2525
26export interface ElectronMenuShape {26export class ElectronMenu extends Context.Service<
27 readonly setApplicationMenu: (27 ElectronMenu,
28 template: readonly Electron.MenuItemConstructorOptions[],28 {
29 ) => Effect.Effect<void>;29 readonly setApplicationMenu: (
30 readonly showContextMenu: (30 template: readonly Electron.MenuItemConstructorOptions[],
31 input: ElectronMenuContextInput,31 ) => Effect.Effect<void>;
32 ) => Effect.Effect<Option.Option<string>>;32 readonly showContextMenu: (
33 readonly popupTemplate: (input: ElectronMenuTemplateInput) => Effect.Effect<void>;33 input: ElectronMenuContextInput,
34}34 ) => Effect.Effect<Option.Option<string>>;
3535 readonly popupTemplate: (input: ElectronMenuTemplateInput) => Effect.Effect<void>;
36export class ElectronMenu extends Context.Service<ElectronMenu, ElectronMenuShape>()(36 }
37 "@t3tools/desktop/electron/ElectronMenu",37>()("@t3tools/desktop/electron/ElectronMenu") {}
38) {}
3938
40function normalizeContextMenuItems(source: readonly ContextMenuItem[]): ContextMenuItem[] {39function normalizeContextMenuItems(source: readonly ContextMenuItem[]): ContextMenuItem[] {
41 const normalizedItems: ContextMenuItem[] = [];40 const normalizedItems: ContextMenuItem[] = [];
...@@ -80,114 +79,113 @@ const normalizePosition = (...@@ -80,114 +79,113 @@ const normalizePosition = (
80 ({ x, y }) => Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0,79 ({ x, y }) => Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0,
81 ).pipe(Option.map(({ x, y }) => ({ x: Math.floor(x), y: Math.floor(y) })));80 ).pipe(Option.map(({ x, y }) => ({ x: Math.floor(x), y: Math.floor(y) })));
8281
83export const layer = Layer.effect(82export const make = Effect.gen(function* () {
84 ElectronMenu,83 const platform = yield* HostProcessPlatform;
85 Effect.gen(function* () {84 let destructiveMenuIconCache: Option.Option<Electron.NativeImage> | undefined;
86 const platform = yield* HostProcessPlatform;
87 let destructiveMenuIconCache: Option.Option<Electron.NativeImage> | undefined;
8885
89 const getDestructiveMenuIcon = (): Option.Option<Electron.NativeImage> => {86 const getDestructiveMenuIcon = (): Option.Option<Electron.NativeImage> => {
90 if (platform !== "darwin") {87 if (platform !== "darwin") {
91 return Option.none();88 return Option.none();
92 }89 }
93 if (destructiveMenuIconCache !== undefined) {90 if (destructiveMenuIconCache !== undefined) {
94 return destructiveMenuIconCache;91 return destructiveMenuIconCache;
95 }92 }
9693
97 try {94 try {
98 const icon = Electron.nativeImage.createFromNamedImage("trash").resize({95 const icon = Electron.nativeImage.createFromNamedImage("trash").resize({
99 width: 12,96 width: 12,
100 height: 12,97 height: 12,
101 });98 });
102 icon.setTemplateImage(true);99 icon.setTemplateImage(true);
103 destructiveMenuIconCache = icon.isEmpty() ? Option.none() : Option.some(icon);100 destructiveMenuIconCache = icon.isEmpty() ? Option.none() : Option.some(icon);
104 } catch {101 } catch {
105 destructiveMenuIconCache = Option.none();102 destructiveMenuIconCache = Option.none();
106 }103 }
107104
108 return destructiveMenuIconCache;105 return destructiveMenuIconCache;
109 };106 };
110107
111 const buildTemplate = (108 const buildTemplate = (
112 entries: readonly ContextMenuItem[],109 entries: readonly ContextMenuItem[],
113 complete: (selectedItemId: Option.Option<string>) => void,110 complete: (selectedItemId: Option.Option<string>) => void,
114 ): Electron.MenuItemConstructorOptions[] => {111 ): Electron.MenuItemConstructorOptions[] => {
115 const template: Electron.MenuItemConstructorOptions[] = [];112 const template: Electron.MenuItemConstructorOptions[] = [];
116 let hasInsertedDestructiveSeparator = false;113 let hasInsertedDestructiveSeparator = false;
117
118 for (const item of entries) {
119 if (item.destructive && !hasInsertedDestructiveSeparator && template.length > 0) {
120 template.push({ type: "separator" });
121 hasInsertedDestructiveSeparator = true;
122 }
123114
124 const itemOption: Electron.MenuItemConstructorOptions = {115 for (const item of entries) {
125 label: item.label,116 if (item.destructive && !hasInsertedDestructiveSeparator && template.length > 0) {
126 enabled: !item.disabled,117 template.push({ type: "separator" });
127 };118 hasInsertedDestructiveSeparator = true;
128 if (item.children && item.children.length > 0) {119 }
129 itemOption.submenu = buildTemplate(item.children, complete);
130 } else {
131 itemOption.click = () => complete(Option.some(item.id));
132 }
133 if (item.destructive && (!item.children || item.children.length === 0)) {
134 const destructiveIcon = getDestructiveMenuIcon();
135 if (Option.isSome(destructiveIcon)) {
136 itemOption.icon = destructiveIcon.value;
137 }
138 }
139120
140 template.push(itemOption);121 const itemOption: Electron.MenuItemConstructorOptions = {
122 label: item.label,
123 enabled: !item.disabled,
124 };
125 if (item.children && item.children.length > 0) {
126 itemOption.submenu = buildTemplate(item.children, complete);
127 } else {
128 itemOption.click = () => complete(Option.some(item.id));
129 }
130 if (item.destructive && (!item.children || item.children.length === 0)) {
131 const destructiveIcon = getDestructiveMenuIcon();
132 if (Option.isSome(destructiveIcon)) {
133 itemOption.icon = destructiveIcon.value;
134 }
141 }135 }
142136
143 return template;137 template.push(itemOption);
144 };138 }
145139
146 return ElectronMenu.of({140 return template;
147 setApplicationMenu: (template) =>141 };
148 Effect.sync(() => {142
149 Electron.Menu.setApplicationMenu(Electron.Menu.buildFromTemplate([...template]));143 return ElectronMenu.of({
150 }),144 setApplicationMenu: (template) =>
151 popupTemplate: (input) =>145 Effect.sync(() => {
152 Effect.sync(() => {146 Electron.Menu.setApplicationMenu(Electron.Menu.buildFromTemplate([...template]));
153 if (input.template.length === 0) {147 }),
154 return;148 popupTemplate: (input) =>
155 }149 Effect.sync(() => {
156 Electron.Menu.buildFromTemplate([...input.template]).popup({ window: input.window });150 if (input.template.length === 0) {
157 }),151 return;
158 showContextMenu: (input) =>152 }
159 Effect.callback<Option.Option<string>>((resume) => {153 Electron.Menu.buildFromTemplate([...input.template]).popup({ window: input.window });
160 const normalizedItems = normalizeContextMenuItems(input.items);154 }),
161 if (normalizedItems.length === 0) {155 showContextMenu: (input) =>
162 resume(Effect.succeed(Option.none()));156 Effect.callback<Option.Option<string>>((resume) => {
157 const normalizedItems = normalizeContextMenuItems(input.items);
158 if (normalizedItems.length === 0) {
159 resume(Effect.succeed(Option.none()));
160 return;
161 }
162
163 let completed = false;
164 const complete = (selectedItemId: Option.Option<string>) => {
165 if (completed) {
163 return;166 return;
164 }167 }
168 completed = true;
169 resume(Effect.succeed(selectedItemId));
170 };
171
172 const menu = Electron.Menu.buildFromTemplate(buildTemplate(normalizedItems, complete));
173 const popupPosition = normalizePosition(input.position);
174 const popupOptions = Option.match(popupPosition, {
175 onNone: (): Electron.PopupOptions => ({
176 window: input.window,
177 callback: () => complete(Option.none()),
178 }),
179 onSome: (position): Electron.PopupOptions => ({
180 window: input.window,
181 x: position.x,
182 y: position.y,
183 callback: () => complete(Option.none()),
184 }),
185 });
186 menu.popup(popupOptions);
187 }),
188 });
189});
165190
166 let completed = false;191export const layer = Layer.effect(ElectronMenu, make);
167 const complete = (selectedItemId: Option.Option<string>) => {
168 if (completed) {
169 return;
170 }
171 completed = true;
172 resume(Effect.succeed(selectedItemId));
173 };
174
175 const menu = Electron.Menu.buildFromTemplate(buildTemplate(normalizedItems, complete));
176 const popupPosition = normalizePosition(input.position);
177 const popupOptions = Option.match(popupPosition, {
178 onNone: (): Electron.PopupOptions => ({
179 window: input.window,
180 callback: () => complete(Option.none()),
181 }),
182 onSome: (position): Electron.PopupOptions => ({
183 window: input.window,
184 x: position.x,
185 y: position.y,
186 callback: () => complete(Option.none()),
187 }),
188 });
189 menu.popup(popupOptions);
190 }),
191 });
192 }),
193);
apps/desktop/src/electron/ElectronProtocol.ts+17-17
...@@ -1,8 +1,8 @@...@@ -1,8 +1,8 @@
1import * as Context from "effect/Context";1import * as Context from "effect/Context";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
5import * as Ref from "effect/Ref";4import * as Ref from "effect/Ref";
5import * as Schema from "effect/Schema";
6import * as Scope from "effect/Scope";6import * as Scope from "effect/Scope";
77
8import * as Electron from "electron";8import * as Electron from "electron";
...@@ -23,13 +23,14 @@ export function getDesktopUrl(isDevelopment: boolean): string {...@@ -23,13 +23,14 @@ export function getDesktopUrl(isDevelopment: boolean): string {
23 return `${getDesktopOrigin(isDevelopment)}/`;23 return `${getDesktopOrigin(isDevelopment)}/`;
24}24}
2525
26export class ElectronProtocolRegistrationError extends Data.TaggedError(26export class ElectronProtocolRegistrationError extends Schema.TaggedErrorClass<ElectronProtocolRegistrationError>()(
27 "ElectronProtocolRegistrationError",27 "ElectronProtocolRegistrationError",
28)<{28 {
29 readonly scheme: string;29 scheme: Schema.String,
30 readonly cause: unknown;30 cause: Schema.Defect(),
31}> {31 },
32 override get message() {32) {
33 override get message(): string {
33 return `Failed to register ${this.scheme}: protocol.`;34 return `Failed to register ${this.scheme}: protocol.`;
34 }35 }
35}36}
...@@ -41,15 +42,14 @@ export interface DesktopProtocolRegistrationInput {...@@ -41,15 +42,14 @@ export interface DesktopProtocolRegistrationInput {
41 readonly clerkFrontendApiHostname: string | undefined;42 readonly clerkFrontendApiHostname: string | undefined;
42}43}
4344
44export interface ElectronProtocolShape {45export class ElectronProtocol extends Context.Service<
45 readonly registerDesktopProtocol: (46 ElectronProtocol,
46 input: DesktopProtocolRegistrationInput,47 {
47 ) => Effect.Effect<void, ElectronProtocolRegistrationError, Scope.Scope>;48 readonly registerDesktopProtocol: (
48}49 input: DesktopProtocolRegistrationInput,
4950 ) => Effect.Effect<void, ElectronProtocolRegistrationError, Scope.Scope>;
50export class ElectronProtocol extends Context.Service<ElectronProtocol, ElectronProtocolShape>()(51 }
51 "@t3tools/desktop/electron/ElectronProtocol",52>()("@t3tools/desktop/electron/ElectronProtocol") {}
52) {}
5353
54export function makeDesktopContentSecurityPolicy(input: DesktopProtocolRegistrationInput): string {54export function makeDesktopContentSecurityPolicy(input: DesktopProtocolRegistrationInput): string {
55 const clerkOrigin = input.clerkFrontendApiHostname55 const clerkOrigin = input.clerkFrontendApiHostname
...@@ -114,7 +114,7 @@ async function proxyRequest(...@@ -114,7 +114,7 @@ async function proxyRequest(
114 return withContentSecurityPolicy(response, contentSecurityPolicy);114 return withContentSecurityPolicy(response, contentSecurityPolicy);
115}115}
116116
117const make = Effect.gen(function* () {117export const make = Effect.gen(function* () {
118 const registered = yield* Ref.make(false);118 const registered = yield* Ref.make(false);
119119
120 const registerDesktopProtocol = Effect.fn("desktop.electron.protocol.registerDesktopProtocol")(120 const registerDesktopProtocol = Effect.fn("desktop.electron.protocol.registerDesktopProtocol")(
apps/desktop/src/electron/ElectronSafeStorage.ts+43-30
...@@ -1,56 +1,69 @@...@@ -1,56 +1,69 @@
1import * as Electron from "electron";1import * as Context from "effect/Context";
2
3import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
5import * as Context from "effect/Context";4import * as Schema from "effect/Schema";
6import * as Data from "effect/Data";
75
8export class ElectronSafeStorageAvailabilityError extends Data.TaggedError(6import * as Electron from "electron";
7
8const electronSafeStorageErrorFields = {
9 cause: Schema.Defect(),
10};
11
12export class ElectronSafeStorageAvailabilityError extends Schema.TaggedErrorClass<ElectronSafeStorageAvailabilityError>()(
9 "ElectronSafeStorageAvailabilityError",13 "ElectronSafeStorageAvailabilityError",
10)<{14 {
11 readonly cause: unknown;15 ...electronSafeStorageErrorFields,
12}> {16 },
13 override get message() {17) {
18 override get message(): string {
14 return "Electron safe storage failed to check encryption availability.";19 return "Electron safe storage failed to check encryption availability.";
15 }20 }
16}21}
1722
18export class ElectronSafeStorageEncryptError extends Data.TaggedError(23export class ElectronSafeStorageEncryptError extends Schema.TaggedErrorClass<ElectronSafeStorageEncryptError>()(
19 "ElectronSafeStorageEncryptError",24 "ElectronSafeStorageEncryptError",
20)<{25 {
21 readonly cause: unknown;26 ...electronSafeStorageErrorFields,
22}> {27 },
23 override get message() {28) {
29 override get message(): string {
24 return "Electron safe storage failed to encrypt a string.";30 return "Electron safe storage failed to encrypt a string.";
25 }31 }
26}32}
2733
28export class ElectronSafeStorageDecryptError extends Data.TaggedError(34export class ElectronSafeStorageDecryptError extends Schema.TaggedErrorClass<ElectronSafeStorageDecryptError>()(
29 "ElectronSafeStorageDecryptError",35 "ElectronSafeStorageDecryptError",
30)<{36 {
31 readonly cause: unknown;37 ...electronSafeStorageErrorFields,
32}> {38 },
33 override get message() {39) {
40 override get message(): string {
34 return "Electron safe storage failed to decrypt a string.";41 return "Electron safe storage failed to decrypt a string.";
35 }42 }
36}43}
3744
38export interface ElectronSafeStorageShape {45export const ElectronSafeStorageError = Schema.Union([
39 readonly isEncryptionAvailable: Effect.Effect<boolean, ElectronSafeStorageAvailabilityError>;46 ElectronSafeStorageAvailabilityError,
40 readonly encryptString: (47 ElectronSafeStorageEncryptError,
41 value: string,48 ElectronSafeStorageDecryptError,
42 ) => Effect.Effect<Uint8Array, ElectronSafeStorageEncryptError>;49]);
43 readonly decryptString: (50export type ElectronSafeStorageError = typeof ElectronSafeStorageError.Type;
44 value: Uint8Array,51export const isElectronSafeStorageError = Schema.is(ElectronSafeStorageError);
45 ) => Effect.Effect<string, ElectronSafeStorageDecryptError>;
46}
4752
48export class ElectronSafeStorage extends Context.Service<53export class ElectronSafeStorage extends Context.Service<
49 ElectronSafeStorage,54 ElectronSafeStorage,
50 ElectronSafeStorageShape55 {
56 readonly isEncryptionAvailable: Effect.Effect<boolean, ElectronSafeStorageAvailabilityError>;
57 readonly encryptString: (
58 value: string,
59 ) => Effect.Effect<Uint8Array, ElectronSafeStorageEncryptError>;
60 readonly decryptString: (
61 value: Uint8Array,
62 ) => Effect.Effect<string, ElectronSafeStorageDecryptError>;
63 }
51>()("@t3tools/desktop/electron/ElectronSafeStorage") {}64>()("@t3tools/desktop/electron/ElectronSafeStorage") {}
5265
53const make = ElectronSafeStorage.of({66export const make = ElectronSafeStorage.of({
54 isEncryptionAvailable: Effect.try({67 isEncryptionAvailable: Effect.try({
55 try: () => Electron.safeStorage.isEncryptionAvailable(),68 try: () => Electron.safeStorage.isEncryptionAvailable(),
56 catch: (cause) => new ElectronSafeStorageAvailabilityError({ cause }),69 catch: (cause) => new ElectronSafeStorageAvailabilityError({ cause }),
apps/desktop/src/electron/ElectronShell.ts+8-9
...@@ -20,16 +20,15 @@ export function parseSafeExternalUrl(rawUrl: unknown): Option.Option<string> {...@@ -20,16 +20,15 @@ export function parseSafeExternalUrl(rawUrl: unknown): Option.Option<string> {
20 }20 }
21}21}
2222
23export interface ElectronShellShape {23export class ElectronShell extends Context.Service<
24 readonly openExternal: (rawUrl: unknown) => Effect.Effect<boolean>;24 ElectronShell,
25 readonly copyText: (text: string) => Effect.Effect<void>;25 {
26}26 readonly openExternal: (rawUrl: unknown) => Effect.Effect<boolean>;
2727 readonly copyText: (text: string) => Effect.Effect<void>;
28export class ElectronShell extends Context.Service<ElectronShell, ElectronShellShape>()(28 }
29 "@t3tools/desktop/electron/ElectronShell",29>()("@t3tools/desktop/electron/ElectronShell") {}
30) {}
3130
32const make = ElectronShell.of({31export const make = ElectronShell.of({
33 openExternal: (rawUrl) =>32 openExternal: (rawUrl) =>
34 Option.match(parseSafeExternalUrl(rawUrl), {33 Option.match(parseSafeExternalUrl(rawUrl), {
35 onNone: () => Effect.succeed(false),34 onNone: () => Effect.succeed(false),
apps/desktop/src/electron/ElectronTheme.ts+9-10
...@@ -6,17 +6,16 @@ import * as Scope from "effect/Scope";...@@ -6,17 +6,16 @@ import * as Scope from "effect/Scope";
66
7import * as Electron from "electron";7import * as Electron from "electron";
88
9export interface ElectronThemeShape {9export class ElectronTheme extends Context.Service<
10 readonly shouldUseDarkColors: Effect.Effect<boolean>;10 ElectronTheme,
11 readonly setSource: (theme: DesktopTheme) => Effect.Effect<void>;11 {
12 readonly onUpdated: (listener: () => void) => Effect.Effect<void, never, Scope.Scope>;12 readonly shouldUseDarkColors: Effect.Effect<boolean>;
13}13 readonly setSource: (theme: DesktopTheme) => Effect.Effect<void>;
14 readonly onUpdated: (listener: () => void) => Effect.Effect<void, never, Scope.Scope>;
15 }
16>()("@t3tools/desktop/electron/ElectronTheme") {}
1417
15export class ElectronTheme extends Context.Service<ElectronTheme, ElectronThemeShape>()(18export const make = ElectronTheme.of({
16 "@t3tools/desktop/electron/ElectronTheme",
17) {}
18
19const make = ElectronTheme.of({
20 shouldUseDarkColors: Effect.sync(() => Electron.nativeTheme.shouldUseDarkColors),19 shouldUseDarkColors: Effect.sync(() => Electron.nativeTheme.shouldUseDarkColors),
21 setSource: (theme) =>20 setSource: (theme) =>
22 Effect.suspend(() => {21 Effect.suspend(() => {
apps/desktop/src/electron/ElectronUpdater.test.ts+1
...@@ -73,6 +73,7 @@ describe("ElectronUpdater", () => {...@@ -73,6 +73,7 @@ describe("ElectronUpdater", () => {
73 const error = Cause.squash(exit.cause);73 const error = Cause.squash(exit.cause);
74 assert.instanceOf(error, ElectronUpdater.ElectronUpdaterCheckForUpdatesError);74 assert.instanceOf(error, ElectronUpdater.ElectronUpdaterCheckForUpdatesError);
75 assert.equal(error.cause, cause);75 assert.equal(error.cause, cause);
76 assert.equal(error.message, "Electron updater failed to check for updates.");
76 }77 }
77 }).pipe(Effect.provide(ElectronUpdater.layer)),78 }).pipe(Effect.provide(ElectronUpdater.layer)),
78 );79 );
apps/desktop/src/electron/ElectronUpdater.ts+57-46
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import * as Context from "effect/Context";1import * as Context from "effect/Context";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";2import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";3import * as Layer from "effect/Layer";
4import * as Schema from "effect/Schema";
5import * as Scope from "effect/Scope";5import * as Scope from "effect/Scope";
66
7import { autoUpdater } from "electron-updater";7import { autoUpdater } from "electron-updater";
...@@ -10,67 +10,76 @@ type AutoUpdater = typeof autoUpdater;...@@ -10,67 +10,76 @@ type AutoUpdater = typeof autoUpdater;
1010
11export type ElectronUpdaterFeedUrl = Parameters<AutoUpdater["setFeedURL"]>[0];11export type ElectronUpdaterFeedUrl = Parameters<AutoUpdater["setFeedURL"]>[0];
1212
13export class ElectronUpdaterCheckForUpdatesError extends Data.TaggedError(13const electronUpdaterErrorFields = {
14 cause: Schema.Defect(),
15};
16
17export class ElectronUpdaterCheckForUpdatesError extends Schema.TaggedErrorClass<ElectronUpdaterCheckForUpdatesError>()(
14 "ElectronUpdaterCheckForUpdatesError",18 "ElectronUpdaterCheckForUpdatesError",
15)<{19 {
16 readonly cause: unknown;20 ...electronUpdaterErrorFields,
17}> {21 },
18 override get message() {22) {
23 override get message(): string {
19 return "Electron updater failed to check for updates.";24 return "Electron updater failed to check for updates.";
20 }25 }
21}26}
2227
23export class ElectronUpdaterDownloadUpdateError extends Data.TaggedError(28export class ElectronUpdaterDownloadUpdateError extends Schema.TaggedErrorClass<ElectronUpdaterDownloadUpdateError>()(
24 "ElectronUpdaterDownloadUpdateError",29 "ElectronUpdaterDownloadUpdateError",
25)<{30 {
26 readonly cause: unknown;31 ...electronUpdaterErrorFields,
27}> {32 },
28 override get message() {33) {
34 override get message(): string {
29 return "Electron updater failed to download the update.";35 return "Electron updater failed to download the update.";
30 }36 }
31}37}
3238
33export class ElectronUpdaterQuitAndInstallError extends Data.TaggedError(39export class ElectronUpdaterQuitAndInstallError extends Schema.TaggedErrorClass<ElectronUpdaterQuitAndInstallError>()(
34 "ElectronUpdaterQuitAndInstallError",40 "ElectronUpdaterQuitAndInstallError",
35)<{41 {
36 readonly cause: unknown;42 ...electronUpdaterErrorFields,
37}> {43 },
38 override get message() {44) {
45 override get message(): string {
39 return "Electron updater failed to quit and install the update.";46 return "Electron updater failed to quit and install the update.";
40 }47 }
41}48}
4249
43export type ElectronUpdaterError =50export const ElectronUpdaterError = Schema.Union([
44 | ElectronUpdaterCheckForUpdatesError51 ElectronUpdaterCheckForUpdatesError,
45 | ElectronUpdaterDownloadUpdateError52 ElectronUpdaterDownloadUpdateError,
46 | ElectronUpdaterQuitAndInstallError;53 ElectronUpdaterQuitAndInstallError,
54]);
55export type ElectronUpdaterError = typeof ElectronUpdaterError.Type;
56export const isElectronUpdaterError = Schema.is(ElectronUpdaterError);
4757
48export interface ElectronUpdaterShape {58export class ElectronUpdater extends Context.Service<
49 readonly setFeedURL: (options: ElectronUpdaterFeedUrl) => Effect.Effect<void>;59 ElectronUpdater,
50 readonly setAutoDownload: (value: boolean) => Effect.Effect<void>;60 {
51 readonly setAutoInstallOnAppQuit: (value: boolean) => Effect.Effect<void>;61 readonly setFeedURL: (options: ElectronUpdaterFeedUrl) => Effect.Effect<void>;
52 readonly setChannel: (channel: string) => Effect.Effect<void>;62 readonly setAutoDownload: (value: boolean) => Effect.Effect<void>;
53 readonly setAllowPrerelease: (value: boolean) => Effect.Effect<void>;63 readonly setAutoInstallOnAppQuit: (value: boolean) => Effect.Effect<void>;
54 readonly allowDowngrade: Effect.Effect<boolean>;64 readonly setChannel: (channel: string) => Effect.Effect<void>;
55 readonly setAllowDowngrade: (value: boolean) => Effect.Effect<void>;65 readonly setAllowPrerelease: (value: boolean) => Effect.Effect<void>;
56 readonly setDisableDifferentialDownload: (value: boolean) => Effect.Effect<void>;66 readonly allowDowngrade: Effect.Effect<boolean>;
57 readonly checkForUpdates: Effect.Effect<void, ElectronUpdaterCheckForUpdatesError>;67 readonly setAllowDowngrade: (value: boolean) => Effect.Effect<void>;
58 readonly downloadUpdate: Effect.Effect<void, ElectronUpdaterDownloadUpdateError>;68 readonly setDisableDifferentialDownload: (value: boolean) => Effect.Effect<void>;
59 readonly quitAndInstall: (options: {69 readonly checkForUpdates: Effect.Effect<void, ElectronUpdaterCheckForUpdatesError>;
60 readonly isSilent: boolean;70 readonly downloadUpdate: Effect.Effect<void, ElectronUpdaterDownloadUpdateError>;
61 readonly isForceRunAfter: boolean;71 readonly quitAndInstall: (options: {
62 }) => Effect.Effect<void, ElectronUpdaterQuitAndInstallError>;72 readonly isSilent: boolean;
63 readonly on: <Args extends ReadonlyArray<unknown>>(73 readonly isForceRunAfter: boolean;
64 eventName: string,74 }) => Effect.Effect<void, ElectronUpdaterQuitAndInstallError>;
65 listener: (...args: Args) => void,75 readonly on: <Args extends ReadonlyArray<unknown>>(
66 ) => Effect.Effect<void, never, Scope.Scope>;76 eventName: string,
67}77 listener: (...args: Args) => void,
6878 ) => Effect.Effect<void, never, Scope.Scope>;
69export class ElectronUpdater extends Context.Service<ElectronUpdater, ElectronUpdaterShape>()(79 }
70 "@t3tools/desktop/electron/ElectronUpdater",80>()("@t3tools/desktop/electron/ElectronUpdater") {}
71) {}
7281
73export const layer = Layer.succeed(ElectronUpdater, {82export const make = ElectronUpdater.of({
74 setFeedURL: (options) =>83 setFeedURL: (options) =>
75 Effect.suspend(() => {84 Effect.suspend(() => {
76 autoUpdater.setFeedURL(options);85 autoUpdater.setFeedURL(options);
...@@ -136,4 +145,6 @@ export const layer = Layer.succeed(ElectronUpdater, {...@@ -136,4 +145,6 @@ export const layer = Layer.succeed(ElectronUpdater, {
136 }),145 }),
137 ).pipe(Effect.asVoid);146 ).pipe(Effect.asVoid);
138 },147 },
139} satisfies ElectronUpdaterShape);148});
149
150export const layer = Layer.succeed(ElectronUpdater, make);
apps/desktop/src/electron/ElectronWindow.ts+29-27
...@@ -1,43 +1,45 @@...@@ -1,43 +1,45 @@
1import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
1import * as Context from "effect/Context";2import * as Context from "effect/Context";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";5import * as Option from "effect/Option";
6import * as Ref from "effect/Ref";6import * as Ref from "effect/Ref";
7import * as Schema from "effect/Schema";
78
8import * as Electron from "electron";9import * as Electron from "electron";
9import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
1010
11export class ElectronWindowCreateError extends Data.TaggedError("ElectronWindowCreateError")<{11export class ElectronWindowCreateError extends Schema.TaggedErrorClass<ElectronWindowCreateError>()(
12 readonly cause: unknown;12 "ElectronWindowCreateError",
13}> {13 {
14 override get message() {14 cause: Schema.Defect(),
15 },
16) {
17 override get message(): string {
15 return "Failed to create Electron BrowserWindow.";18 return "Failed to create Electron BrowserWindow.";
16 }19 }
17}20}
1821
19export interface ElectronWindowShape {22export class ElectronWindow extends Context.Service<
20 readonly create: (23 ElectronWindow,
21 options: Electron.BrowserWindowConstructorOptions,24 {
22 ) => Effect.Effect<Electron.BrowserWindow, ElectronWindowCreateError>;25 readonly create: (
23 readonly main: Effect.Effect<Option.Option<Electron.BrowserWindow>>;26 options: Electron.BrowserWindowConstructorOptions,
24 readonly currentMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>;27 ) => Effect.Effect<Electron.BrowserWindow, ElectronWindowCreateError>;
25 readonly focusedMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>;28 readonly main: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
26 readonly setMain: (window: Electron.BrowserWindow) => Effect.Effect<void>;29 readonly currentMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
27 readonly clearMain: (window: Option.Option<Electron.BrowserWindow>) => Effect.Effect<void>;30 readonly focusedMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
28 readonly reveal: (window: Electron.BrowserWindow) => Effect.Effect<void>;31 readonly setMain: (window: Electron.BrowserWindow) => Effect.Effect<void>;
29 readonly sendAll: (channel: string, ...args: readonly unknown[]) => Effect.Effect<void>;32 readonly clearMain: (window: Option.Option<Electron.BrowserWindow>) => Effect.Effect<void>;
30 readonly destroyAll: Effect.Effect<void>;33 readonly reveal: (window: Electron.BrowserWindow) => Effect.Effect<void>;
31 readonly syncAllAppearance: <E, R>(34 readonly sendAll: (channel: string, ...args: readonly unknown[]) => Effect.Effect<void>;
32 sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>,35 readonly destroyAll: Effect.Effect<void>;
33 ) => Effect.Effect<void, E, R>;36 readonly syncAllAppearance: <E, R>(
34}37 sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>,
3538 ) => Effect.Effect<void, E, R>;
36export class ElectronWindow extends Context.Service<ElectronWindow, ElectronWindowShape>()(39 }
37 "@t3tools/desktop/electron/ElectronWindow",40>()("@t3tools/desktop/electron/ElectronWindow") {}
38) {}
3941
40const make = Effect.gen(function* () {42export const make = Effect.gen(function* () {
41 const platform = yield* HostProcessPlatform;43 const platform = yield* HostProcessPlatform;
42 const mainWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());44 const mainWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
4345
apps/desktop/src/main.ts+4-4
...@@ -1,7 +1,7 @@...@@ -1,7 +1,7 @@
1import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";1import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
2import * as NodeRuntime from "@effect/platform-node/NodeRuntime";2import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
3import * as NodeServices from "@effect/platform-node/NodeServices";3import * as NodeServices from "@effect/platform-node/NodeServices";
4import * as NodeOS from "node:os";4import { homedir } from "node:os";
5import * as Effect from "effect/Effect";5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";6import * as Layer from "effect/Layer";
7import * as Option from "effect/Option";7import * as Option from "effect/Option";
...@@ -19,7 +19,7 @@ import * as ElectronApp from "./electron/ElectronApp.ts";...@@ -19,7 +19,7 @@ import * as ElectronApp from "./electron/ElectronApp.ts";
19import * as ElectronDialog from "./electron/ElectronDialog.ts";19import * as ElectronDialog from "./electron/ElectronDialog.ts";
20import * as ElectronMenu from "./electron/ElectronMenu.ts";20import * as ElectronMenu from "./electron/ElectronMenu.ts";
21import * as ElectronProtocol from "./electron/ElectronProtocol.ts";21import * as ElectronProtocol from "./electron/ElectronProtocol.ts";
22import * as DesktopSecretStorage from "./electron/ElectronSafeStorage.ts";22import * as ElectronSafeStorage from "./electron/ElectronSafeStorage.ts";
23import * as ElectronShell from "./electron/ElectronShell.ts";23import * as ElectronShell from "./electron/ElectronShell.ts";
24import * as ElectronTheme from "./electron/ElectronTheme.ts";24import * as ElectronTheme from "./electron/ElectronTheme.ts";
25import * as ElectronUpdater from "./electron/ElectronUpdater.ts";25import * as ElectronUpdater from "./electron/ElectronUpdater.ts";
...@@ -60,7 +60,7 @@ const desktopEnvironmentLayer = Layer.unwrap(...@@ -60,7 +60,7 @@ const desktopEnvironmentLayer = Layer.unwrap(
60 const processArch = yield* HostProcessArchitecture;60 const processArch = yield* HostProcessArchitecture;
61 return DesktopEnvironment.layer({61 return DesktopEnvironment.layer({
62 dirname: __dirname,62 dirname: __dirname,
63 homeDirectory: NodeOS.homedir(),63 homeDirectory: homedir(),
64 platform,64 platform,
65 processArch,65 processArch,
66 ...metadata,66 ...metadata,
...@@ -106,7 +106,7 @@ const electronLayer = Layer.mergeAll(...@@ -106,7 +106,7 @@ const electronLayer = Layer.mergeAll(
106 ElectronDialog.layer,106 ElectronDialog.layer,
107 ElectronMenu.layer,107 ElectronMenu.layer,
108 ElectronProtocol.layer,108 ElectronProtocol.layer,
109 DesktopSecretStorage.layer,109 ElectronSafeStorage.layer,
110 ElectronShell.layer,110 ElectronShell.layer,
111 ElectronTheme.layer,111 ElectronTheme.layer,
112 ElectronUpdater.layer,112 ElectronUpdater.layer,
apps/desktop/src/settings/DesktopSavedEnvironments.test.ts-3
...@@ -241,7 +241,6 @@ describe("DesktopSavedEnvironments", () => {...@@ -241,7 +241,6 @@ describe("DesktopSavedEnvironments", () => {
241 .getSecret(savedRegistryRecord.environmentId)241 .getSecret(savedRegistryRecord.environmentId)
242 .pipe(Effect.flip);242 .pipe(Effect.flip);
243 assert.instanceOf(error, DesktopSavedEnvironments.DesktopSavedEnvironmentSecretDecodeError);243 assert.instanceOf(error, DesktopSavedEnvironments.DesktopSavedEnvironmentSecretDecodeError);
244 assert.equal(error.operation, "decode-secret");
245 assert.equal(error.environmentId, savedRegistryRecord.environmentId);244 assert.equal(error.environmentId, savedRegistryRecord.environmentId);
246 assert.equal(error.registryPath, environment.savedEnvironmentRegistryPath);245 assert.equal(error.registryPath, environment.savedEnvironmentRegistryPath);
247 assert.equal(error.field, "encryptedBearerToken");246 assert.equal(error.field, "encryptedBearerToken");
...@@ -363,7 +362,6 @@ describe("DesktopSavedEnvironments", () => {...@@ -363,7 +362,6 @@ describe("DesktopSavedEnvironments", () => {
363 registryError,362 registryError,
364 DesktopSavedEnvironments.DesktopSavedEnvironmentsDocumentDecodeError,363 DesktopSavedEnvironments.DesktopSavedEnvironmentsDocumentDecodeError,
365 );364 );
366 assert.equal(registryError.operation, "decode-registry");
367 assert.equal(registryError.registryPath, environment.savedEnvironmentRegistryPath);365 assert.equal(registryError.registryPath, environment.savedEnvironmentRegistryPath);
368 assert.exists(registryError.cause);366 assert.exists(registryError.cause);
369 const secretError = yield* savedEnvironments367 const secretError = yield* savedEnvironments
...@@ -409,7 +407,6 @@ describe("DesktopSavedEnvironments", () => {...@@ -409,7 +407,6 @@ describe("DesktopSavedEnvironments", () => {
409407
410 const error = yield* savedEnvironments.getRegistry.pipe(Effect.flip);408 const error = yield* savedEnvironments.getRegistry.pipe(Effect.flip);
411 assert.instanceOf(error, DesktopSavedEnvironments.DesktopSavedEnvironmentsReadError);409 assert.instanceOf(error, DesktopSavedEnvironments.DesktopSavedEnvironmentsReadError);
412 assert.equal(error.operation, "read-registry");
413 assert.equal(error.registryPath, registryPath);410 assert.equal(error.registryPath, registryPath);
414 assert.strictEqual(error.cause, permissionError);411 assert.strictEqual(error.cause, permissionError);
415 assert.equal(error.message, `Failed to read desktop saved environments at ${registryPath}.`);412 assert.equal(error.message, `Failed to read desktop saved environments at ${registryPath}.`);
apps/desktop/src/settings/DesktopSavedEnvironments.ts+2-10
...@@ -106,7 +106,6 @@ const writeError = (...@@ -106,7 +106,6 @@ const writeError = (
106export class DesktopSavedEnvironmentsReadError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentsReadError>()(106export class DesktopSavedEnvironmentsReadError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentsReadError>()(
107 "DesktopSavedEnvironmentsReadError",107 "DesktopSavedEnvironmentsReadError",
108 {108 {
109 operation: Schema.Literal("read-registry"),
110 registryPath: Schema.String,109 registryPath: Schema.String,
111 cause: Schema.Defect(),110 cause: Schema.Defect(),
112 },111 },
...@@ -119,7 +118,6 @@ export class DesktopSavedEnvironmentsReadError extends Schema.TaggedErrorClass<D...@@ -119,7 +118,6 @@ export class DesktopSavedEnvironmentsReadError extends Schema.TaggedErrorClass<D
119export class DesktopSavedEnvironmentsDocumentDecodeError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentsDocumentDecodeError>()(118export class DesktopSavedEnvironmentsDocumentDecodeError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentsDocumentDecodeError>()(
120 "DesktopSavedEnvironmentsDocumentDecodeError",119 "DesktopSavedEnvironmentsDocumentDecodeError",
121 {120 {
122 operation: Schema.Literal("decode-registry"),
123 registryPath: Schema.String,121 registryPath: Schema.String,
124 cause: Schema.Defect(),122 cause: Schema.Defect(),
125 },123 },
...@@ -132,7 +130,6 @@ export class DesktopSavedEnvironmentsDocumentDecodeError extends Schema.TaggedEr...@@ -132,7 +130,6 @@ export class DesktopSavedEnvironmentsDocumentDecodeError extends Schema.TaggedEr
132export class DesktopSavedEnvironmentSecretDecodeError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentSecretDecodeError>()(130export class DesktopSavedEnvironmentSecretDecodeError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentSecretDecodeError>()(
133 "DesktopSavedEnvironmentSecretDecodeError",131 "DesktopSavedEnvironmentSecretDecodeError",
134 {132 {
135 operation: Schema.Literal("decode-secret"),
136 environmentId: Schema.String,133 environmentId: Schema.String,
137 registryPath: Schema.String,134 registryPath: Schema.String,
138 field: Schema.Literal("encryptedBearerToken"),135 field: Schema.Literal("encryptedBearerToken"),
...@@ -155,13 +152,11 @@ export type DesktopSavedEnvironmentsMutationError =...@@ -155,13 +152,11 @@ export type DesktopSavedEnvironmentsMutationError =
155export type DesktopSavedEnvironmentsGetSecretError =152export type DesktopSavedEnvironmentsGetSecretError =
156 | DesktopSavedEnvironmentsReadRegistryError153 | DesktopSavedEnvironmentsReadRegistryError
157 | DesktopSavedEnvironmentSecretDecodeError154 | DesktopSavedEnvironmentSecretDecodeError
158 | ElectronSafeStorage.ElectronSafeStorageAvailabilityError155 | ElectronSafeStorage.ElectronSafeStorageError;
159 | ElectronSafeStorage.ElectronSafeStorageDecryptError;
160156
161export type DesktopSavedEnvironmentsSetSecretError =157export type DesktopSavedEnvironmentsSetSecretError =
162 | DesktopSavedEnvironmentsMutationError158 | DesktopSavedEnvironmentsMutationError
163 | ElectronSafeStorage.ElectronSafeStorageAvailabilityError159 | ElectronSafeStorage.ElectronSafeStorageError;
164 | ElectronSafeStorage.ElectronSafeStorageEncryptError;
165160
166export class DesktopSavedEnvironments extends Context.Service<161export class DesktopSavedEnvironments extends Context.Service<
167 DesktopSavedEnvironments,162 DesktopSavedEnvironments,
...@@ -248,7 +243,6 @@ function readRegistryDocument(...@@ -248,7 +243,6 @@ function readRegistryDocument(
248 ? Effect.succeed<string | null>(null)243 ? Effect.succeed<string | null>(null)
249 : Effect.fail(244 : Effect.fail(
250 new DesktopSavedEnvironmentsReadError({245 new DesktopSavedEnvironmentsReadError({
251 operation: "read-registry",
252 registryPath,246 registryPath,
253 cause: error,247 cause: error,
254 }),248 }),
...@@ -262,7 +256,6 @@ function readRegistryDocument(...@@ -262,7 +256,6 @@ function readRegistryDocument(
262 Effect.mapError(256 Effect.mapError(
263 (cause) =>257 (cause) =>
264 new DesktopSavedEnvironmentsDocumentDecodeError({258 new DesktopSavedEnvironmentsDocumentDecodeError({
265 operation: "decode-registry",
266 registryPath,259 registryPath,
267 cause,260 cause,
268 }),261 }),
...@@ -329,7 +322,6 @@ function decodeSecretBytes(...@@ -329,7 +322,6 @@ function decodeSecretBytes(
329 Effect.mapError(322 Effect.mapError(
330 (cause) =>323 (cause) =>
331 new DesktopSavedEnvironmentSecretDecodeError({324 new DesktopSavedEnvironmentSecretDecodeError({
332 operation: "decode-secret",
333 environmentId,325 environmentId,
334 registryPath,326 registryPath,
335 field: "encryptedBearerToken",327 field: "encryptedBearerToken",
apps/desktop/src/ssh/DesktopSshPasswordPrompts.test.ts+2-2
...@@ -9,7 +9,7 @@ import * as TestClock from "effect/testing/TestClock";...@@ -9,7 +9,7 @@ import * as TestClock from "effect/testing/TestClock";
9import type * as Electron from "electron";9import type * as Electron from "electron";
1010
11import * as ElectronWindow from "../electron/ElectronWindow.ts";11import * as ElectronWindow from "../electron/ElectronWindow.ts";
12import * as IpcChannels from "../ipc/channels.ts";12import { SSH_PASSWORD_PROMPT_CHANNEL } from "../ipc/channels.ts";
13import * as DesktopSshPasswordPrompts from "./DesktopSshPasswordPrompts.ts";13import * as DesktopSshPasswordPrompts from "./DesktopSshPasswordPrompts.ts";
1414
15interface SentMessage {15interface SentMessage {
...@@ -111,7 +111,7 @@ describe("DesktopSshPasswordPrompts", () => {...@@ -111,7 +111,7 @@ describe("DesktopSshPasswordPrompts", () => {
111 assert.equal(testWindow.sentMessages.length, 1);111 assert.equal(testWindow.sentMessages.length, 1);
112 const sent = testWindow.sentMessages[0];112 const sent = testWindow.sentMessages[0];
113 assert.ok(sent);113 assert.ok(sent);
114 assert.equal(sent.channel, IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL);114 assert.equal(sent.channel, SSH_PASSWORD_PROMPT_CHANNEL);
115 const request = sent.args[0] as { readonly requestId: string; readonly destination: string };115 const request = sent.args[0] as { readonly requestId: string; readonly destination: string };
116 assert.equal(request.destination, "devbox");116 assert.equal(request.destination, "devbox");
117 assert.equal(testWindow.isRestored(), true);117 assert.equal(testWindow.isRestored(), true);
apps/desktop/src/updates/DesktopUpdates.test.ts+2-2
...@@ -83,7 +83,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {...@@ -83,7 +83,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {
83 removeListener(eventName, listener as unknown as (...args: readonly unknown[]) => void);83 removeListener(eventName, listener as unknown as (...args: readonly unknown[]) => void);
84 }),84 }),
85 ).pipe(Effect.asVoid),85 ).pipe(Effect.asVoid),
86 } satisfies ElectronUpdater.ElectronUpdaterShape);86 } satisfies ElectronUpdater.ElectronUpdater["Service"]);
8787
88 const windowLayer = Layer.succeed(ElectronWindow.ElectronWindow, {88 const windowLayer = Layer.succeed(ElectronWindow.ElectronWindow, {
89 create: () => Effect.die("unexpected BrowserWindow creation"),89 create: () => Effect.die("unexpected BrowserWindow creation"),
...@@ -99,7 +99,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {...@@ -99,7 +99,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {
99 }),99 }),
100 destroyAll: Effect.void,100 destroyAll: Effect.void,
101 syncAllAppearance: () => Effect.void,101 syncAllAppearance: () => Effect.void,
102 } satisfies ElectronWindow.ElectronWindowShape);102 } satisfies ElectronWindow.ElectronWindow["Service"]);
103103
104 const backendLayer = Layer.succeed(DesktopBackendManager.DesktopBackendManager, {104 const backendLayer = Layer.succeed(DesktopBackendManager.DesktopBackendManager, {
105 start: Effect.void,105 start: Effect.void,
apps/desktop/src/window/DesktopApplicationMenu.test.ts+3-3
...@@ -46,14 +46,14 @@ const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, {...@@ -46,14 +46,14 @@ const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, {
46 setDockIcon: () => Effect.void,46 setDockIcon: () => Effect.void,
47 appendCommandLineSwitch: () => Effect.void,47 appendCommandLineSwitch: () => Effect.void,
48 on: () => Effect.void,48 on: () => Effect.void,
49} satisfies ElectronApp.ElectronAppShape);49} satisfies ElectronApp.ElectronApp["Service"]);
5050
51const electronDialogLayer = Layer.succeed(ElectronDialog.ElectronDialog, {51const electronDialogLayer = Layer.succeed(ElectronDialog.ElectronDialog, {
52 pickFolder: () => Effect.succeed(Option.none()),52 pickFolder: () => Effect.succeed(Option.none()),
53 confirm: () => Effect.succeed(false),53 confirm: () => Effect.succeed(false),
54 showMessageBox: () => Effect.succeed({ response: 0, checkboxChecked: false }),54 showMessageBox: () => Effect.succeed({ response: 0, checkboxChecked: false }),
55 showErrorBox: () => Effect.void,55 showErrorBox: () => Effect.void,
56} satisfies ElectronDialog.ElectronDialogShape);56} satisfies ElectronDialog.ElectronDialog["Service"]);
5757
58const desktopUpdatesLayer = Layer.succeed(DesktopUpdates.DesktopUpdates, {58const desktopUpdatesLayer = Layer.succeed(DesktopUpdates.DesktopUpdates, {
59 getState: Effect.die("unexpected getState"),59 getState: Effect.die("unexpected getState"),
...@@ -86,7 +86,7 @@ const makeElectronMenuLayer = (...@@ -86,7 +86,7 @@ const makeElectronMenuLayer = (
86 Deferred.succeed(applicationMenuTemplate, template).pipe(Effect.asVoid),86 Deferred.succeed(applicationMenuTemplate, template).pipe(Effect.asVoid),
87 popupTemplate: () => Effect.void,87 popupTemplate: () => Effect.void,
88 showContextMenu: () => Effect.succeed(Option.none()),88 showContextMenu: () => Effect.succeed(Option.none()),
89 } satisfies ElectronMenu.ElectronMenuShape);89 } satisfies ElectronMenu.ElectronMenu["Service"]);
9090
91describe("DesktopApplicationMenu", () => {91describe("DesktopApplicationMenu", () => {
92 it.effect("installs the native menu and routes Settings through DesktopWindow", () =>92 it.effect("installs the native menu and routes Settings through DesktopWindow", () =>
apps/desktop/src/window/DesktopApplicationMenu.ts+3-3
...@@ -6,7 +6,7 @@ import * as Option from "effect/Option";...@@ -6,7 +6,7 @@ import * as Option from "effect/Option";
66
7import type * as Electron from "electron";7import type * as Electron from "electron";
88
9import * as DesktopObservability from "../app/DesktopObservability.ts";9import { makeComponentLogger } from "../app/DesktopObservability.ts";
10import * as ElectronApp from "../electron/ElectronApp.ts";10import * as ElectronApp from "../electron/ElectronApp.ts";
11import * as ElectronDialog from "../electron/ElectronDialog.ts";11import * as ElectronDialog from "../electron/ElectronDialog.ts";
12import * as ElectronMenu from "../electron/ElectronMenu.ts";12import * as ElectronMenu from "../electron/ElectronMenu.ts";
...@@ -26,9 +26,9 @@ type DesktopApplicationMenuRuntimeServices =...@@ -26,9 +26,9 @@ type DesktopApplicationMenuRuntimeServices =
26 | DesktopWindow.DesktopWindow26 | DesktopWindow.DesktopWindow
27 | ElectronDialog.ElectronDialog;27 | ElectronDialog.ElectronDialog;
2828
29const { logInfo: logUpdaterInfo } = DesktopObservability.makeComponentLogger("desktop-updater");29const { logInfo: logUpdaterInfo } = makeComponentLogger("desktop-updater");
3030
31const { logError: logMenuError } = DesktopObservability.makeComponentLogger("desktop-menu");31const { logError: logMenuError } = makeComponentLogger("desktop-menu");
3232
33const dispatchMenuAction = Effect.fn("desktop.menu.dispatchMenuAction")(function* (33const dispatchMenuAction = Effect.fn("desktop.menu.dispatchMenuAction")(function* (
34 action: string,34 action: string,
apps/desktop/src/window/DesktopWindow.ts+6-6
...@@ -8,15 +8,15 @@ import type * as Electron from "electron";...@@ -8,15 +8,15 @@ import type * as Electron from "electron";
88
9import * as DesktopAssets from "../app/DesktopAssets.ts";9import * as DesktopAssets from "../app/DesktopAssets.ts";
10import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";10import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
11import * as DesktopObservability from "../app/DesktopObservability.ts";11import { makeComponentLogger } from "../app/DesktopObservability.ts";
12import * as DesktopState from "../app/DesktopState.ts";12import * as DesktopState from "../app/DesktopState.ts";
13import * as PreviewManager from "../preview/Manager.ts";
14import * as ElectronMenu from "../electron/ElectronMenu.ts";13import * as ElectronMenu from "../electron/ElectronMenu.ts";
14import { getDesktopUrl } from "../electron/ElectronProtocol.ts";
15import * as ElectronShell from "../electron/ElectronShell.ts";15import * as ElectronShell from "../electron/ElectronShell.ts";
16import * as ElectronTheme from "../electron/ElectronTheme.ts";16import * as ElectronTheme from "../electron/ElectronTheme.ts";
17import * as ElectronWindow from "../electron/ElectronWindow.ts";17import * as ElectronWindow from "../electron/ElectronWindow.ts";
18import { getDesktopUrl } from "../electron/ElectronProtocol.ts";18import { MENU_ACTION_CHANNEL } from "../ipc/channels.ts";
19import * as IpcChannels from "../ipc/channels.ts";19import * as PreviewManager from "../preview/Manager.ts";
2020
21const TITLEBAR_HEIGHT = 40;21const TITLEBAR_HEIGHT = 40;
22const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux22const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux
...@@ -57,7 +57,7 @@ export class DesktopWindow extends Context.Service<...@@ -57,7 +57,7 @@ export class DesktopWindow extends Context.Service<
57>()("@t3tools/desktop/window/DesktopWindow") {}57>()("@t3tools/desktop/window/DesktopWindow") {}
5858
59const { logInfo: logWindowInfo, logWarning: logWindowWarning } =59const { logInfo: logWindowInfo, logWarning: logWindowWarning } =
60 DesktopObservability.makeComponentLogger("desktop-window");60 makeComponentLogger("desktop-window");
6161
62function getIconOption(62function getIconOption(
63 iconPaths: DesktopAssets.DesktopIconPaths,63 iconPaths: DesktopAssets.DesktopIconPaths,
...@@ -380,7 +380,7 @@ export const make = Effect.gen(function* () {...@@ -380,7 +380,7 @@ export const make = Effect.gen(function* () {
380380
381 const send = () => {381 const send = () => {
382 if (targetWindow.isDestroyed()) return;382 if (targetWindow.isDestroyed()) return;
383 targetWindow.webContents.send(IpcChannels.MENU_ACTION_CHANNEL, action);383 targetWindow.webContents.send(MENU_ACTION_CHANNEL, action);
384 void runPromise(electronWindow.reveal(targetWindow));384 void runPromise(electronWindow.reveal(targetWindow));
385 };385 };
386386