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", () => {
237237 error,
238238 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreDocumentDecodeError,
239239 );
240 assert.equal(error.operation, "decode-catalog-document");
241240 assert.equal(error.catalogPath, catalogPath);
242241 assert.exists(error.cause);
243242 assert.equal(yield* fileSystem.readFileString(catalogPath), "{not-json");
......@@ -272,7 +271,6 @@ describe("DesktopConnectionCatalogStore", () => {
272271 error,
273272 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreReadError,
274273 );
275 assert.equal(error.operation, "read-catalog");
276274 assert.equal(error.catalogPath, `${baseDir}/userdata/connection-catalog.json`);
277275 assert.strictEqual(error.cause, permissionError);
278276 assert.equal(
......@@ -368,7 +366,6 @@ describe("DesktopConnectionCatalogStore", () => {
368366 error,
369367 DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreDecodeError,
370368 );
371 assert.equal(error.operation, "decode-encrypted-catalog");
372369 assert.equal(error.resource, "encryptedCatalog");
373370 assert.equal(error.catalogPath, catalogPath);
374371 assert.exists(error.cause);
apps/desktop/src/app/DesktopConnectionCatalogStore.ts+2-11
......@@ -92,7 +92,6 @@ const writeError = (
9292export class DesktopConnectionCatalogStoreDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDecodeError>()(
9393 "DesktopConnectionCatalogStoreDecodeError",
9494 {
95 operation: Schema.Literal("decode-encrypted-catalog"),
9695 resource: Schema.Literal("encryptedCatalog"),
9796 catalogPath: Schema.String,
9897 cause: Schema.Defect(),
......@@ -106,7 +105,6 @@ export class DesktopConnectionCatalogStoreDecodeError extends Schema.TaggedError
106105export class DesktopConnectionCatalogStoreReadError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreReadError>()(
107106 "DesktopConnectionCatalogStoreReadError",
108107 {
109 operation: Schema.Literal("read-catalog"),
110108 catalogPath: Schema.String,
111109 cause: Schema.Defect(),
112110 },
......@@ -119,7 +117,6 @@ export class DesktopConnectionCatalogStoreReadError extends Schema.TaggedErrorCl
119117export class DesktopConnectionCatalogStoreDocumentDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDocumentDecodeError>()(
120118 "DesktopConnectionCatalogStoreDocumentDecodeError",
121119 {
122 operation: Schema.Literal("decode-catalog-document"),
123120 catalogPath: Schema.String,
124121 cause: Schema.Defect(),
125122 },
......@@ -167,16 +164,13 @@ export class DesktopConnectionCatalogStore extends Context.Service<
167164 | DesktopConnectionCatalogStoreDocumentDecodeError
168165 | DesktopConnectionCatalogStoreDecodeError
169166 | DesktopConnectionCatalogStoreMigrationError
170 | ElectronSafeStorage.ElectronSafeStorageAvailabilityError
171 | ElectronSafeStorage.ElectronSafeStorageDecryptError
167 | ElectronSafeStorage.ElectronSafeStorageError
172168 >;
173169 readonly set: (
174170 catalog: string,
175171 ) => Effect.Effect<
176172 boolean,
177 | DesktopConnectionCatalogStoreWriteError
178 | ElectronSafeStorage.ElectronSafeStorageAvailabilityError
179 | ElectronSafeStorage.ElectronSafeStorageEncryptError
173 DesktopConnectionCatalogStoreWriteError | ElectronSafeStorage.ElectronSafeStorageError
180174 >;
181175 readonly clear: Effect.Effect<void>;
182176 }
......@@ -190,7 +184,6 @@ function decodeSecretBytes(
190184 Effect.mapError(
191185 (cause) =>
192186 new DesktopConnectionCatalogStoreDecodeError({
193 operation: "decode-encrypted-catalog",
194187 resource: "encryptedCatalog",
195188 catalogPath,
196189 cause,
......@@ -212,7 +205,6 @@ const readDocument = (
212205 ? Effect.succeed<string | null>(null)
213206 : Effect.fail(
214207 new DesktopConnectionCatalogStoreReadError({
215 operation: "read-catalog",
216208 catalogPath,
217209 cause: error,
218210 }),
......@@ -226,7 +218,6 @@ const readDocument = (
226218 Effect.mapError(
227219 (cause) =>
228220 new DesktopConnectionCatalogStoreDocumentDecodeError({
229 operation: "decode-catalog-document",
230221 catalogPath,
231222 cause,
232223 }),
apps/desktop/src/app/DesktopLifecycle.ts+3-3
......@@ -8,7 +8,7 @@ import * as Scope from "effect/Scope";
88import type * as Electron from "electron";
99
1010import * as DesktopEnvironment from "./DesktopEnvironment.ts";
11import * as DesktopObservability from "./DesktopObservability.ts";
11import { makeComponentLogger } from "./DesktopObservability.ts";
1212import * as DesktopShutdown from "./DesktopShutdown.ts";
1313import * as ElectronApp from "../electron/ElectronApp.ts";
1414import * as ElectronTheme from "../electron/ElectronTheme.ts";
......@@ -37,7 +37,7 @@ export class DesktopLifecycle extends Context.Service<
3737>()("@t3tools/desktop/app/DesktopLifecycle") {}
3838
3939const { logInfo: logLifecycleInfo, logError: logLifecycleError } =
40 DesktopObservability.makeComponentLogger("desktop-lifecycle");
40 makeComponentLogger("desktop-lifecycle");
4141
4242function addScopedListener<Args extends ReadonlyArray<unknown>>(
4343 target: unknown,
......@@ -122,7 +122,7 @@ function quitFromSignal(
122122 );
123123}
124124
125const make = DesktopLifecycle.of({
125export const make = DesktopLifecycle.of({
126126 relaunch: Effect.fn("desktop.lifecycle.relaunch")(function* (reason) {
127127 const electronApp = yield* ElectronApp.ElectronApp;
128128 const environment = yield* DesktopEnvironment.DesktopEnvironment;
apps/desktop/src/electron/ElectronApp.ts+35-36
......@@ -13,41 +13,40 @@ export interface ElectronAppMetadata {
1313 readonly runningUnderArm64Translation: boolean;
1414}
1515
16export interface ElectronAppShape {
17 readonly metadata: Effect.Effect<ElectronAppMetadata>;
18 readonly name: Effect.Effect<string>;
19 readonly whenReady: Effect.Effect<void>;
20 readonly quit: Effect.Effect<void>;
21 readonly exit: (code: number) => Effect.Effect<void>;
22 readonly relaunch: (options: Electron.RelaunchOptions) => Effect.Effect<void>;
23 readonly setPath: (
24 name: Parameters<Electron.App["setPath"]>[0],
25 path: string,
26 ) => Effect.Effect<void>;
27 readonly setName: (name: string) => Effect.Effect<void>;
28 readonly setAboutPanelOptions: (
29 options: Electron.AboutPanelOptionsOptions,
30 ) => Effect.Effect<void>;
31 readonly setAppUserModelId: (id: string) => Effect.Effect<void>;
32 readonly requestSingleInstanceLock: Effect.Effect<boolean>;
33 readonly isDefaultProtocolClient: (protocol: string) => Effect.Effect<boolean>;
34 readonly setAsDefaultProtocolClient: (
35 protocol: string,
36 path?: string,
37 args?: readonly string[],
38 ) => Effect.Effect<boolean>;
39 readonly setDesktopName: (desktopName: string) => Effect.Effect<void>;
40 readonly setDockIcon: (iconPath: string) => Effect.Effect<void>;
41 readonly appendCommandLineSwitch: (switchName: string, value?: string) => Effect.Effect<void>;
42 readonly on: <Args extends ReadonlyArray<unknown>>(
43 eventName: string,
44 listener: (...args: Args) => void,
45 ) => Effect.Effect<void, never, Scope.Scope>;
46}
47
48export class ElectronApp extends Context.Service<ElectronApp, ElectronAppShape>()(
49 "@t3tools/desktop/electron/ElectronApp",
50) {}
16export class ElectronApp extends Context.Service<
17 ElectronApp,
18 {
19 readonly metadata: Effect.Effect<ElectronAppMetadata>;
20 readonly name: Effect.Effect<string>;
21 readonly whenReady: Effect.Effect<void>;
22 readonly quit: Effect.Effect<void>;
23 readonly exit: (code: number) => Effect.Effect<void>;
24 readonly relaunch: (options: Electron.RelaunchOptions) => Effect.Effect<void>;
25 readonly setPath: (
26 name: Parameters<Electron.App["setPath"]>[0],
27 path: string,
28 ) => Effect.Effect<void>;
29 readonly setName: (name: string) => Effect.Effect<void>;
30 readonly setAboutPanelOptions: (
31 options: Electron.AboutPanelOptionsOptions,
32 ) => Effect.Effect<void>;
33 readonly setAppUserModelId: (id: string) => Effect.Effect<void>;
34 readonly requestSingleInstanceLock: Effect.Effect<boolean>;
35 readonly isDefaultProtocolClient: (protocol: string) => Effect.Effect<boolean>;
36 readonly setAsDefaultProtocolClient: (
37 protocol: string,
38 path?: string,
39 args?: readonly string[],
40 ) => Effect.Effect<boolean>;
41 readonly setDesktopName: (desktopName: string) => Effect.Effect<void>;
42 readonly setDockIcon: (iconPath: string) => Effect.Effect<void>;
43 readonly appendCommandLineSwitch: (switchName: string, value?: string) => Effect.Effect<void>;
44 readonly on: <Args extends ReadonlyArray<unknown>>(
45 eventName: string,
46 listener: (...args: Args) => void,
47 ) => Effect.Effect<void, never, Scope.Scope>;
48 }
49>()("@t3tools/desktop/electron/ElectronApp") {}
5150
5251const addScopedAppListener = <Args extends ReadonlyArray<unknown>>(
5352 eventName: string,
......@@ -63,7 +62,7 @@ const addScopedAppListener = <Args extends ReadonlyArray<unknown>>(
6362 }),
6463 ).pipe(Effect.asVoid);
6564
66const make = ElectronApp.of({
65export const make = ElectronApp.of({
6766 metadata: Effect.sync(() => ({
6867 appVersion: Electron.app.getVersion(),
6968 appPath: Electron.app.getAppPath(),
apps/desktop/src/electron/ElectronDialog.ts+14-15
......@@ -17,22 +17,21 @@ export interface ElectronDialogConfirmInput {
1717 readonly message: string;
1818}
1919
20export interface ElectronDialogShape {
21 readonly pickFolder: (
22 input: ElectronDialogPickFolderInput,
23 ) => Effect.Effect<Option.Option<string>>;
24 readonly confirm: (input: ElectronDialogConfirmInput) => Effect.Effect<boolean>;
25 readonly showMessageBox: (
26 options: Electron.MessageBoxOptions,
27 ) => Effect.Effect<Electron.MessageBoxReturnValue>;
28 readonly showErrorBox: (title: string, content: string) => Effect.Effect<void>;
29}
30
31export class ElectronDialog extends Context.Service<ElectronDialog, ElectronDialogShape>()(
32 "@t3tools/desktop/electron/ElectronDialog",
33) {}
20export class ElectronDialog extends Context.Service<
21 ElectronDialog,
22 {
23 readonly pickFolder: (
24 input: ElectronDialogPickFolderInput,
25 ) => Effect.Effect<Option.Option<string>>;
26 readonly confirm: (input: ElectronDialogConfirmInput) => Effect.Effect<boolean>;
27 readonly showMessageBox: (
28 options: Electron.MessageBoxOptions,
29 ) => Effect.Effect<Electron.MessageBoxReturnValue>;
30 readonly showErrorBox: (title: string, content: string) => Effect.Effect<void>;
31 }
32>()("@t3tools/desktop/electron/ElectronDialog") {}
3433
35const make = ElectronDialog.of({
34export const make = ElectronDialog.of({
3635 pickFolder: Effect.fn("desktop.electron.dialog.pickFolder")(function* (input) {
3736 const openDialogOptions: Electron.OpenDialogOptions = Option.match(input.defaultPath, {
3837 onNone: () => ({
apps/desktop/src/electron/ElectronMenu.ts+111-113
......@@ -1,11 +1,11 @@
11import type { ContextMenuItem } from "@t3tools/contracts";
2import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
23import * as Context from "effect/Context";
34import * as Effect from "effect/Effect";
45import * as Layer from "effect/Layer";
56import * as Option from "effect/Option";
67
78import * as Electron from "electron";
8import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
99
1010export interface ElectronMenuPosition {
1111 readonly x: number;
......@@ -23,19 +23,18 @@ export interface ElectronMenuTemplateInput {
2323 readonly template: readonly Electron.MenuItemConstructorOptions[];
2424}
2525
26export interface ElectronMenuShape {
27 readonly setApplicationMenu: (
28 template: readonly Electron.MenuItemConstructorOptions[],
29 ) => Effect.Effect<void>;
30 readonly showContextMenu: (
31 input: ElectronMenuContextInput,
32 ) => Effect.Effect<Option.Option<string>>;
33 readonly popupTemplate: (input: ElectronMenuTemplateInput) => Effect.Effect<void>;
34}
35
36export class ElectronMenu extends Context.Service<ElectronMenu, ElectronMenuShape>()(
37 "@t3tools/desktop/electron/ElectronMenu",
38) {}
26export class ElectronMenu extends Context.Service<
27 ElectronMenu,
28 {
29 readonly setApplicationMenu: (
30 template: readonly Electron.MenuItemConstructorOptions[],
31 ) => Effect.Effect<void>;
32 readonly showContextMenu: (
33 input: ElectronMenuContextInput,
34 ) => Effect.Effect<Option.Option<string>>;
35 readonly popupTemplate: (input: ElectronMenuTemplateInput) => Effect.Effect<void>;
36 }
37>()("@t3tools/desktop/electron/ElectronMenu") {}
3938
4039function normalizeContextMenuItems(source: readonly ContextMenuItem[]): ContextMenuItem[] {
4140 const normalizedItems: ContextMenuItem[] = [];
......@@ -80,114 +79,113 @@ const normalizePosition = (
8079 ({ x, y }) => Number.isFinite(x) && Number.isFinite(y) && x >= 0 && y >= 0,
8180 ).pipe(Option.map(({ x, y }) => ({ x: Math.floor(x), y: Math.floor(y) })));
8281
83export const layer = Layer.effect(
84 ElectronMenu,
85 Effect.gen(function* () {
86 const platform = yield* HostProcessPlatform;
87 let destructiveMenuIconCache: Option.Option<Electron.NativeImage> | undefined;
82export const make = Effect.gen(function* () {
83 const platform = yield* HostProcessPlatform;
84 let destructiveMenuIconCache: Option.Option<Electron.NativeImage> | undefined;
8885
89 const getDestructiveMenuIcon = (): Option.Option<Electron.NativeImage> => {
90 if (platform !== "darwin") {
91 return Option.none();
92 }
93 if (destructiveMenuIconCache !== undefined) {
94 return destructiveMenuIconCache;
95 }
86 const getDestructiveMenuIcon = (): Option.Option<Electron.NativeImage> => {
87 if (platform !== "darwin") {
88 return Option.none();
89 }
90 if (destructiveMenuIconCache !== undefined) {
91 return destructiveMenuIconCache;
92 }
9693
97 try {
98 const icon = Electron.nativeImage.createFromNamedImage("trash").resize({
99 width: 12,
100 height: 12,
101 });
102 icon.setTemplateImage(true);
103 destructiveMenuIconCache = icon.isEmpty() ? Option.none() : Option.some(icon);
104 } catch {
105 destructiveMenuIconCache = Option.none();
106 }
94 try {
95 const icon = Electron.nativeImage.createFromNamedImage("trash").resize({
96 width: 12,
97 height: 12,
98 });
99 icon.setTemplateImage(true);
100 destructiveMenuIconCache = icon.isEmpty() ? Option.none() : Option.some(icon);
101 } catch {
102 destructiveMenuIconCache = Option.none();
103 }
107104
108 return destructiveMenuIconCache;
109 };
105 return destructiveMenuIconCache;
106 };
110107
111 const buildTemplate = (
112 entries: readonly ContextMenuItem[],
113 complete: (selectedItemId: Option.Option<string>) => void,
114 ): Electron.MenuItemConstructorOptions[] => {
115 const template: Electron.MenuItemConstructorOptions[] = [];
116 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 }
108 const buildTemplate = (
109 entries: readonly ContextMenuItem[],
110 complete: (selectedItemId: Option.Option<string>) => void,
111 ): Electron.MenuItemConstructorOptions[] => {
112 const template: Electron.MenuItemConstructorOptions[] = [];
113 let hasInsertedDestructiveSeparator = false;
123114
124 const itemOption: Electron.MenuItemConstructorOptions = {
125 label: item.label,
126 enabled: !item.disabled,
127 };
128 if (item.children && item.children.length > 0) {
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 }
115 for (const item of entries) {
116 if (item.destructive && !hasInsertedDestructiveSeparator && template.length > 0) {
117 template.push({ type: "separator" });
118 hasInsertedDestructiveSeparator = true;
119 }
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 }
141135 }
142136
143 return template;
144 };
137 template.push(itemOption);
138 }
145139
146 return ElectronMenu.of({
147 setApplicationMenu: (template) =>
148 Effect.sync(() => {
149 Electron.Menu.setApplicationMenu(Electron.Menu.buildFromTemplate([...template]));
150 }),
151 popupTemplate: (input) =>
152 Effect.sync(() => {
153 if (input.template.length === 0) {
154 return;
155 }
156 Electron.Menu.buildFromTemplate([...input.template]).popup({ window: input.window });
157 }),
158 showContextMenu: (input) =>
159 Effect.callback<Option.Option<string>>((resume) => {
160 const normalizedItems = normalizeContextMenuItems(input.items);
161 if (normalizedItems.length === 0) {
162 resume(Effect.succeed(Option.none()));
140 return template;
141 };
142
143 return ElectronMenu.of({
144 setApplicationMenu: (template) =>
145 Effect.sync(() => {
146 Electron.Menu.setApplicationMenu(Electron.Menu.buildFromTemplate([...template]));
147 }),
148 popupTemplate: (input) =>
149 Effect.sync(() => {
150 if (input.template.length === 0) {
151 return;
152 }
153 Electron.Menu.buildFromTemplate([...input.template]).popup({ window: input.window });
154 }),
155 showContextMenu: (input) =>
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) {
163166 return;
164167 }
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;
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);
191export const layer = Layer.effect(ElectronMenu, make);
apps/desktop/src/electron/ElectronProtocol.ts+17-17
......@@ -1,8 +1,8 @@
11import * as Context from "effect/Context";
2import * as Data from "effect/Data";
32import * as Effect from "effect/Effect";
43import * as Layer from "effect/Layer";
54import * as Ref from "effect/Ref";
5import * as Schema from "effect/Schema";
66import * as Scope from "effect/Scope";
77
88import * as Electron from "electron";
......@@ -23,13 +23,14 @@ export function getDesktopUrl(isDevelopment: boolean): string {
2323 return `${getDesktopOrigin(isDevelopment)}/`;
2424}
2525
26export class ElectronProtocolRegistrationError extends Data.TaggedError(
26export class ElectronProtocolRegistrationError extends Schema.TaggedErrorClass<ElectronProtocolRegistrationError>()(
2727 "ElectronProtocolRegistrationError",
28)<{
29 readonly scheme: string;
30 readonly cause: unknown;
31}> {
32 override get message() {
28 {
29 scheme: Schema.String,
30 cause: Schema.Defect(),
31 },
32) {
33 override get message(): string {
3334 return `Failed to register ${this.scheme}: protocol.`;
3435 }
3536}
......@@ -41,15 +42,14 @@ export interface DesktopProtocolRegistrationInput {
4142 readonly clerkFrontendApiHostname: string | undefined;
4243}
4344
44export interface ElectronProtocolShape {
45 readonly registerDesktopProtocol: (
46 input: DesktopProtocolRegistrationInput,
47 ) => Effect.Effect<void, ElectronProtocolRegistrationError, Scope.Scope>;
48}
49
50export class ElectronProtocol extends Context.Service<ElectronProtocol, ElectronProtocolShape>()(
51 "@t3tools/desktop/electron/ElectronProtocol",
52) {}
45export class ElectronProtocol extends Context.Service<
46 ElectronProtocol,
47 {
48 readonly registerDesktopProtocol: (
49 input: DesktopProtocolRegistrationInput,
50 ) => Effect.Effect<void, ElectronProtocolRegistrationError, Scope.Scope>;
51 }
52>()("@t3tools/desktop/electron/ElectronProtocol") {}
5353
5454export function makeDesktopContentSecurityPolicy(input: DesktopProtocolRegistrationInput): string {
5555 const clerkOrigin = input.clerkFrontendApiHostname
......@@ -114,7 +114,7 @@ async function proxyRequest(
114114 return withContentSecurityPolicy(response, contentSecurityPolicy);
115115}
116116
117const make = Effect.gen(function* () {
117export const make = Effect.gen(function* () {
118118 const registered = yield* Ref.make(false);
119119
120120 const registerDesktopProtocol = Effect.fn("desktop.electron.protocol.registerDesktopProtocol")(
apps/desktop/src/electron/ElectronSafeStorage.ts+43-30
......@@ -1,56 +1,69 @@
1import * as Electron from "electron";
2
1import * as Context from "effect/Context";
32import * as Effect from "effect/Effect";
43import * as Layer from "effect/Layer";
5import * as Context from "effect/Context";
6import * as Data from "effect/Data";
4import * as Schema from "effect/Schema";
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>()(
913 "ElectronSafeStorageAvailabilityError",
10)<{
11 readonly cause: unknown;
12}> {
13 override get message() {
14 {
15 ...electronSafeStorageErrorFields,
16 },
17) {
18 override get message(): string {
1419 return "Electron safe storage failed to check encryption availability.";
1520 }
1621}
1722
18export class ElectronSafeStorageEncryptError extends Data.TaggedError(
23export class ElectronSafeStorageEncryptError extends Schema.TaggedErrorClass<ElectronSafeStorageEncryptError>()(
1924 "ElectronSafeStorageEncryptError",
20)<{
21 readonly cause: unknown;
22}> {
23 override get message() {
25 {
26 ...electronSafeStorageErrorFields,
27 },
28) {
29 override get message(): string {
2430 return "Electron safe storage failed to encrypt a string.";
2531 }
2632}
2733
28export class ElectronSafeStorageDecryptError extends Data.TaggedError(
34export class ElectronSafeStorageDecryptError extends Schema.TaggedErrorClass<ElectronSafeStorageDecryptError>()(
2935 "ElectronSafeStorageDecryptError",
30)<{
31 readonly cause: unknown;
32}> {
33 override get message() {
36 {
37 ...electronSafeStorageErrorFields,
38 },
39) {
40 override get message(): string {
3441 return "Electron safe storage failed to decrypt a string.";
3542 }
3643}
3744
38export interface ElectronSafeStorageShape {
39 readonly isEncryptionAvailable: Effect.Effect<boolean, ElectronSafeStorageAvailabilityError>;
40 readonly encryptString: (
41 value: string,
42 ) => Effect.Effect<Uint8Array, ElectronSafeStorageEncryptError>;
43 readonly decryptString: (
44 value: Uint8Array,
45 ) => Effect.Effect<string, ElectronSafeStorageDecryptError>;
46}
45export const ElectronSafeStorageError = Schema.Union([
46 ElectronSafeStorageAvailabilityError,
47 ElectronSafeStorageEncryptError,
48 ElectronSafeStorageDecryptError,
49]);
50export type ElectronSafeStorageError = typeof ElectronSafeStorageError.Type;
51export const isElectronSafeStorageError = Schema.is(ElectronSafeStorageError);
4752
4853export class ElectronSafeStorage extends Context.Service<
4954 ElectronSafeStorage,
50 ElectronSafeStorageShape
55 {
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 }
5164>()("@t3tools/desktop/electron/ElectronSafeStorage") {}
5265
53const make = ElectronSafeStorage.of({
66export const make = ElectronSafeStorage.of({
5467 isEncryptionAvailable: Effect.try({
5568 try: () => Electron.safeStorage.isEncryptionAvailable(),
5669 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> {
2020 }
2121}
2222
23export interface ElectronShellShape {
24 readonly openExternal: (rawUrl: unknown) => Effect.Effect<boolean>;
25 readonly copyText: (text: string) => Effect.Effect<void>;
26}
27
28export class ElectronShell extends Context.Service<ElectronShell, ElectronShellShape>()(
29 "@t3tools/desktop/electron/ElectronShell",
30) {}
23export class ElectronShell extends Context.Service<
24 ElectronShell,
25 {
26 readonly openExternal: (rawUrl: unknown) => Effect.Effect<boolean>;
27 readonly copyText: (text: string) => Effect.Effect<void>;
28 }
29>()("@t3tools/desktop/electron/ElectronShell") {}
3130
32const make = ElectronShell.of({
31export const make = ElectronShell.of({
3332 openExternal: (rawUrl) =>
3433 Option.match(parseSafeExternalUrl(rawUrl), {
3534 onNone: () => Effect.succeed(false),
apps/desktop/src/electron/ElectronTheme.ts+9-10
......@@ -6,17 +6,16 @@ import * as Scope from "effect/Scope";
66
77import * as Electron from "electron";
88
9export interface ElectronThemeShape {
10 readonly shouldUseDarkColors: Effect.Effect<boolean>;
11 readonly setSource: (theme: DesktopTheme) => Effect.Effect<void>;
12 readonly onUpdated: (listener: () => void) => Effect.Effect<void, never, Scope.Scope>;
13}
9export class ElectronTheme extends Context.Service<
10 ElectronTheme,
11 {
12 readonly shouldUseDarkColors: Effect.Effect<boolean>;
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>()(
16 "@t3tools/desktop/electron/ElectronTheme",
17) {}
18
19const make = ElectronTheme.of({
18export const make = ElectronTheme.of({
2019 shouldUseDarkColors: Effect.sync(() => Electron.nativeTheme.shouldUseDarkColors),
2120 setSource: (theme) =>
2221 Effect.suspend(() => {
apps/desktop/src/electron/ElectronUpdater.test.ts+1
......@@ -73,6 +73,7 @@ describe("ElectronUpdater", () => {
7373 const error = Cause.squash(exit.cause);
7474 assert.instanceOf(error, ElectronUpdater.ElectronUpdaterCheckForUpdatesError);
7575 assert.equal(error.cause, cause);
76 assert.equal(error.message, "Electron updater failed to check for updates.");
7677 }
7778 }).pipe(Effect.provide(ElectronUpdater.layer)),
7879 );
apps/desktop/src/electron/ElectronUpdater.ts+57-46
......@@ -1,7 +1,7 @@
11import * as Context from "effect/Context";
2import * as Data from "effect/Data";
32import * as Effect from "effect/Effect";
43import * as Layer from "effect/Layer";
4import * as Schema from "effect/Schema";
55import * as Scope from "effect/Scope";
66
77import { autoUpdater } from "electron-updater";
......@@ -10,67 +10,76 @@ type AutoUpdater = typeof autoUpdater;
1010
1111export 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>()(
1418 "ElectronUpdaterCheckForUpdatesError",
15)<{
16 readonly cause: unknown;
17}> {
18 override get message() {
19 {
20 ...electronUpdaterErrorFields,
21 },
22) {
23 override get message(): string {
1924 return "Electron updater failed to check for updates.";
2025 }
2126}
2227
23export class ElectronUpdaterDownloadUpdateError extends Data.TaggedError(
28export class ElectronUpdaterDownloadUpdateError extends Schema.TaggedErrorClass<ElectronUpdaterDownloadUpdateError>()(
2429 "ElectronUpdaterDownloadUpdateError",
25)<{
26 readonly cause: unknown;
27}> {
28 override get message() {
30 {
31 ...electronUpdaterErrorFields,
32 },
33) {
34 override get message(): string {
2935 return "Electron updater failed to download the update.";
3036 }
3137}
3238
33export class ElectronUpdaterQuitAndInstallError extends Data.TaggedError(
39export class ElectronUpdaterQuitAndInstallError extends Schema.TaggedErrorClass<ElectronUpdaterQuitAndInstallError>()(
3440 "ElectronUpdaterQuitAndInstallError",
35)<{
36 readonly cause: unknown;
37}> {
38 override get message() {
41 {
42 ...electronUpdaterErrorFields,
43 },
44) {
45 override get message(): string {
3946 return "Electron updater failed to quit and install the update.";
4047 }
4148}
4249
43export type ElectronUpdaterError =
44 | ElectronUpdaterCheckForUpdatesError
45 | ElectronUpdaterDownloadUpdateError
46 | ElectronUpdaterQuitAndInstallError;
50export const ElectronUpdaterError = Schema.Union([
51 ElectronUpdaterCheckForUpdatesError,
52 ElectronUpdaterDownloadUpdateError,
53 ElectronUpdaterQuitAndInstallError,
54]);
55export type ElectronUpdaterError = typeof ElectronUpdaterError.Type;
56export const isElectronUpdaterError = Schema.is(ElectronUpdaterError);
4757
48export interface ElectronUpdaterShape {
49 readonly setFeedURL: (options: ElectronUpdaterFeedUrl) => Effect.Effect<void>;
50 readonly setAutoDownload: (value: boolean) => Effect.Effect<void>;
51 readonly setAutoInstallOnAppQuit: (value: boolean) => Effect.Effect<void>;
52 readonly setChannel: (channel: string) => Effect.Effect<void>;
53 readonly setAllowPrerelease: (value: boolean) => Effect.Effect<void>;
54 readonly allowDowngrade: Effect.Effect<boolean>;
55 readonly setAllowDowngrade: (value: boolean) => Effect.Effect<void>;
56 readonly setDisableDifferentialDownload: (value: boolean) => Effect.Effect<void>;
57 readonly checkForUpdates: Effect.Effect<void, ElectronUpdaterCheckForUpdatesError>;
58 readonly downloadUpdate: Effect.Effect<void, ElectronUpdaterDownloadUpdateError>;
59 readonly quitAndInstall: (options: {
60 readonly isSilent: boolean;
61 readonly isForceRunAfter: boolean;
62 }) => Effect.Effect<void, ElectronUpdaterQuitAndInstallError>;
63 readonly on: <Args extends ReadonlyArray<unknown>>(
64 eventName: string,
65 listener: (...args: Args) => void,
66 ) => Effect.Effect<void, never, Scope.Scope>;
67}
68
69export class ElectronUpdater extends Context.Service<ElectronUpdater, ElectronUpdaterShape>()(
70 "@t3tools/desktop/electron/ElectronUpdater",
71) {}
58export class ElectronUpdater extends Context.Service<
59 ElectronUpdater,
60 {
61 readonly setFeedURL: (options: ElectronUpdaterFeedUrl) => Effect.Effect<void>;
62 readonly setAutoDownload: (value: boolean) => Effect.Effect<void>;
63 readonly setAutoInstallOnAppQuit: (value: boolean) => Effect.Effect<void>;
64 readonly setChannel: (channel: string) => Effect.Effect<void>;
65 readonly setAllowPrerelease: (value: boolean) => Effect.Effect<void>;
66 readonly allowDowngrade: Effect.Effect<boolean>;
67 readonly setAllowDowngrade: (value: boolean) => Effect.Effect<void>;
68 readonly setDisableDifferentialDownload: (value: boolean) => Effect.Effect<void>;
69 readonly checkForUpdates: Effect.Effect<void, ElectronUpdaterCheckForUpdatesError>;
70 readonly downloadUpdate: Effect.Effect<void, ElectronUpdaterDownloadUpdateError>;
71 readonly quitAndInstall: (options: {
72 readonly isSilent: boolean;
73 readonly isForceRunAfter: boolean;
74 }) => Effect.Effect<void, ElectronUpdaterQuitAndInstallError>;
75 readonly on: <Args extends ReadonlyArray<unknown>>(
76 eventName: string,
77 listener: (...args: Args) => void,
78 ) => Effect.Effect<void, never, Scope.Scope>;
79 }
80>()("@t3tools/desktop/electron/ElectronUpdater") {}
7281
73export const layer = Layer.succeed(ElectronUpdater, {
82export const make = ElectronUpdater.of({
7483 setFeedURL: (options) =>
7584 Effect.suspend(() => {
7685 autoUpdater.setFeedURL(options);
......@@ -136,4 +145,6 @@ export const layer = Layer.succeed(ElectronUpdater, {
136145 }),
137146 ).pipe(Effect.asVoid);
138147 },
139} satisfies ElectronUpdaterShape);
148});
149
150export const layer = Layer.succeed(ElectronUpdater, make);
apps/desktop/src/electron/ElectronWindow.ts+29-27
......@@ -1,43 +1,45 @@
1import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
12import * as Context from "effect/Context";
2import * as Data from "effect/Data";
33import * as Effect from "effect/Effect";
44import * as Layer from "effect/Layer";
55import * as Option from "effect/Option";
66import * as Ref from "effect/Ref";
7import * as Schema from "effect/Schema";
78
89import * as Electron from "electron";
9import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
1010
11export class ElectronWindowCreateError extends Data.TaggedError("ElectronWindowCreateError")<{
12 readonly cause: unknown;
13}> {
14 override get message() {
11export class ElectronWindowCreateError extends Schema.TaggedErrorClass<ElectronWindowCreateError>()(
12 "ElectronWindowCreateError",
13 {
14 cause: Schema.Defect(),
15 },
16) {
17 override get message(): string {
1518 return "Failed to create Electron BrowserWindow.";
1619 }
1720}
1821
19export interface ElectronWindowShape {
20 readonly create: (
21 options: Electron.BrowserWindowConstructorOptions,
22 ) => Effect.Effect<Electron.BrowserWindow, ElectronWindowCreateError>;
23 readonly main: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
24 readonly currentMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
25 readonly focusedMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
26 readonly setMain: (window: Electron.BrowserWindow) => Effect.Effect<void>;
27 readonly clearMain: (window: Option.Option<Electron.BrowserWindow>) => Effect.Effect<void>;
28 readonly reveal: (window: Electron.BrowserWindow) => Effect.Effect<void>;
29 readonly sendAll: (channel: string, ...args: readonly unknown[]) => Effect.Effect<void>;
30 readonly destroyAll: Effect.Effect<void>;
31 readonly syncAllAppearance: <E, R>(
32 sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>,
33 ) => Effect.Effect<void, E, R>;
34}
35
36export class ElectronWindow extends Context.Service<ElectronWindow, ElectronWindowShape>()(
37 "@t3tools/desktop/electron/ElectronWindow",
38) {}
22export class ElectronWindow extends Context.Service<
23 ElectronWindow,
24 {
25 readonly create: (
26 options: Electron.BrowserWindowConstructorOptions,
27 ) => Effect.Effect<Electron.BrowserWindow, ElectronWindowCreateError>;
28 readonly main: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
29 readonly currentMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
30 readonly focusedMainOrFirst: Effect.Effect<Option.Option<Electron.BrowserWindow>>;
31 readonly setMain: (window: Electron.BrowserWindow) => Effect.Effect<void>;
32 readonly clearMain: (window: Option.Option<Electron.BrowserWindow>) => Effect.Effect<void>;
33 readonly reveal: (window: Electron.BrowserWindow) => Effect.Effect<void>;
34 readonly sendAll: (channel: string, ...args: readonly unknown[]) => Effect.Effect<void>;
35 readonly destroyAll: Effect.Effect<void>;
36 readonly syncAllAppearance: <E, R>(
37 sync: (window: Electron.BrowserWindow) => Effect.Effect<void, E, R>,
38 ) => Effect.Effect<void, E, R>;
39 }
40>()("@t3tools/desktop/electron/ElectronWindow") {}
3941
40const make = Effect.gen(function* () {
42export const make = Effect.gen(function* () {
4143 const platform = yield* HostProcessPlatform;
4244 const mainWindowRef = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
4345
apps/desktop/src/main.ts+4-4
......@@ -1,7 +1,7 @@
11import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
22import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
33import * as NodeServices from "@effect/platform-node/NodeServices";
4import * as NodeOS from "node:os";
4import { homedir } from "node:os";
55import * as Effect from "effect/Effect";
66import * as Layer from "effect/Layer";
77import * as Option from "effect/Option";
......@@ -19,7 +19,7 @@ import * as ElectronApp from "./electron/ElectronApp.ts";
1919import * as ElectronDialog from "./electron/ElectronDialog.ts";
2020import * as ElectronMenu from "./electron/ElectronMenu.ts";
2121import * as ElectronProtocol from "./electron/ElectronProtocol.ts";
22import * as DesktopSecretStorage from "./electron/ElectronSafeStorage.ts";
22import * as ElectronSafeStorage from "./electron/ElectronSafeStorage.ts";
2323import * as ElectronShell from "./electron/ElectronShell.ts";
2424import * as ElectronTheme from "./electron/ElectronTheme.ts";
2525import * as ElectronUpdater from "./electron/ElectronUpdater.ts";
......@@ -60,7 +60,7 @@ const desktopEnvironmentLayer = Layer.unwrap(
6060 const processArch = yield* HostProcessArchitecture;
6161 return DesktopEnvironment.layer({
6262 dirname: __dirname,
63 homeDirectory: NodeOS.homedir(),
63 homeDirectory: homedir(),
6464 platform,
6565 processArch,
6666 ...metadata,
......@@ -106,7 +106,7 @@ const electronLayer = Layer.mergeAll(
106106 ElectronDialog.layer,
107107 ElectronMenu.layer,
108108 ElectronProtocol.layer,
109 DesktopSecretStorage.layer,
109 ElectronSafeStorage.layer,
110110 ElectronShell.layer,
111111 ElectronTheme.layer,
112112 ElectronUpdater.layer,
apps/desktop/src/settings/DesktopSavedEnvironments.test.ts-3
......@@ -241,7 +241,6 @@ describe("DesktopSavedEnvironments", () => {
241241 .getSecret(savedRegistryRecord.environmentId)
242242 .pipe(Effect.flip);
243243 assert.instanceOf(error, DesktopSavedEnvironments.DesktopSavedEnvironmentSecretDecodeError);
244 assert.equal(error.operation, "decode-secret");
245244 assert.equal(error.environmentId, savedRegistryRecord.environmentId);
246245 assert.equal(error.registryPath, environment.savedEnvironmentRegistryPath);
247246 assert.equal(error.field, "encryptedBearerToken");
......@@ -363,7 +362,6 @@ describe("DesktopSavedEnvironments", () => {
363362 registryError,
364363 DesktopSavedEnvironments.DesktopSavedEnvironmentsDocumentDecodeError,
365364 );
366 assert.equal(registryError.operation, "decode-registry");
367365 assert.equal(registryError.registryPath, environment.savedEnvironmentRegistryPath);
368366 assert.exists(registryError.cause);
369367 const secretError = yield* savedEnvironments
......@@ -409,7 +407,6 @@ describe("DesktopSavedEnvironments", () => {
409407
410408 const error = yield* savedEnvironments.getRegistry.pipe(Effect.flip);
411409 assert.instanceOf(error, DesktopSavedEnvironments.DesktopSavedEnvironmentsReadError);
412 assert.equal(error.operation, "read-registry");
413410 assert.equal(error.registryPath, registryPath);
414411 assert.strictEqual(error.cause, permissionError);
415412 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 = (
106106export class DesktopSavedEnvironmentsReadError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentsReadError>()(
107107 "DesktopSavedEnvironmentsReadError",
108108 {
109 operation: Schema.Literal("read-registry"),
110109 registryPath: Schema.String,
111110 cause: Schema.Defect(),
112111 },
......@@ -119,7 +118,6 @@ export class DesktopSavedEnvironmentsReadError extends Schema.TaggedErrorClass<D
119118export class DesktopSavedEnvironmentsDocumentDecodeError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentsDocumentDecodeError>()(
120119 "DesktopSavedEnvironmentsDocumentDecodeError",
121120 {
122 operation: Schema.Literal("decode-registry"),
123121 registryPath: Schema.String,
124122 cause: Schema.Defect(),
125123 },
......@@ -132,7 +130,6 @@ export class DesktopSavedEnvironmentsDocumentDecodeError extends Schema.TaggedEr
132130export class DesktopSavedEnvironmentSecretDecodeError extends Schema.TaggedErrorClass<DesktopSavedEnvironmentSecretDecodeError>()(
133131 "DesktopSavedEnvironmentSecretDecodeError",
134132 {
135 operation: Schema.Literal("decode-secret"),
136133 environmentId: Schema.String,
137134 registryPath: Schema.String,
138135 field: Schema.Literal("encryptedBearerToken"),
......@@ -155,13 +152,11 @@ export type DesktopSavedEnvironmentsMutationError =
155152export type DesktopSavedEnvironmentsGetSecretError =
156153 | DesktopSavedEnvironmentsReadRegistryError
157154 | DesktopSavedEnvironmentSecretDecodeError
158 | ElectronSafeStorage.ElectronSafeStorageAvailabilityError
159 | ElectronSafeStorage.ElectronSafeStorageDecryptError;
155 | ElectronSafeStorage.ElectronSafeStorageError;
160156
161157export type DesktopSavedEnvironmentsSetSecretError =
162158 | DesktopSavedEnvironmentsMutationError
163 | ElectronSafeStorage.ElectronSafeStorageAvailabilityError
164 | ElectronSafeStorage.ElectronSafeStorageEncryptError;
159 | ElectronSafeStorage.ElectronSafeStorageError;
165160
166161export class DesktopSavedEnvironments extends Context.Service<
167162 DesktopSavedEnvironments,
......@@ -248,7 +243,6 @@ function readRegistryDocument(
248243 ? Effect.succeed<string | null>(null)
249244 : Effect.fail(
250245 new DesktopSavedEnvironmentsReadError({
251 operation: "read-registry",
252246 registryPath,
253247 cause: error,
254248 }),
......@@ -262,7 +256,6 @@ function readRegistryDocument(
262256 Effect.mapError(
263257 (cause) =>
264258 new DesktopSavedEnvironmentsDocumentDecodeError({
265 operation: "decode-registry",
266259 registryPath,
267260 cause,
268261 }),
......@@ -329,7 +322,6 @@ function decodeSecretBytes(
329322 Effect.mapError(
330323 (cause) =>
331324 new DesktopSavedEnvironmentSecretDecodeError({
332 operation: "decode-secret",
333325 environmentId,
334326 registryPath,
335327 field: "encryptedBearerToken",
apps/desktop/src/ssh/DesktopSshPasswordPrompts.test.ts+2-2
......@@ -9,7 +9,7 @@ import * as TestClock from "effect/testing/TestClock";
99import type * as Electron from "electron";
1010
1111import * as ElectronWindow from "../electron/ElectronWindow.ts";
12import * as IpcChannels from "../ipc/channels.ts";
12import { SSH_PASSWORD_PROMPT_CHANNEL } from "../ipc/channels.ts";
1313import * as DesktopSshPasswordPrompts from "./DesktopSshPasswordPrompts.ts";
1414
1515interface SentMessage {
......@@ -111,7 +111,7 @@ describe("DesktopSshPasswordPrompts", () => {
111111 assert.equal(testWindow.sentMessages.length, 1);
112112 const sent = testWindow.sentMessages[0];
113113 assert.ok(sent);
114 assert.equal(sent.channel, IpcChannels.SSH_PASSWORD_PROMPT_CHANNEL);
114 assert.equal(sent.channel, SSH_PASSWORD_PROMPT_CHANNEL);
115115 const request = sent.args[0] as { readonly requestId: string; readonly destination: string };
116116 assert.equal(request.destination, "devbox");
117117 assert.equal(testWindow.isRestored(), true);
apps/desktop/src/updates/DesktopUpdates.test.ts+2-2
......@@ -83,7 +83,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {
8383 removeListener(eventName, listener as unknown as (...args: readonly unknown[]) => void);
8484 }),
8585 ).pipe(Effect.asVoid),
86 } satisfies ElectronUpdater.ElectronUpdaterShape);
86 } satisfies ElectronUpdater.ElectronUpdater["Service"]);
8787
8888 const windowLayer = Layer.succeed(ElectronWindow.ElectronWindow, {
8989 create: () => Effect.die("unexpected BrowserWindow creation"),
......@@ -99,7 +99,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {
9999 }),
100100 destroyAll: Effect.void,
101101 syncAllAppearance: () => Effect.void,
102 } satisfies ElectronWindow.ElectronWindowShape);
102 } satisfies ElectronWindow.ElectronWindow["Service"]);
103103
104104 const backendLayer = Layer.succeed(DesktopBackendManager.DesktopBackendManager, {
105105 start: Effect.void,
apps/desktop/src/window/DesktopApplicationMenu.test.ts+3-3
......@@ -46,14 +46,14 @@ const electronAppLayer = Layer.succeed(ElectronApp.ElectronApp, {
4646 setDockIcon: () => Effect.void,
4747 appendCommandLineSwitch: () => Effect.void,
4848 on: () => Effect.void,
49} satisfies ElectronApp.ElectronAppShape);
49} satisfies ElectronApp.ElectronApp["Service"]);
5050
5151const electronDialogLayer = Layer.succeed(ElectronDialog.ElectronDialog, {
5252 pickFolder: () => Effect.succeed(Option.none()),
5353 confirm: () => Effect.succeed(false),
5454 showMessageBox: () => Effect.succeed({ response: 0, checkboxChecked: false }),
5555 showErrorBox: () => Effect.void,
56} satisfies ElectronDialog.ElectronDialogShape);
56} satisfies ElectronDialog.ElectronDialog["Service"]);
5757
5858const desktopUpdatesLayer = Layer.succeed(DesktopUpdates.DesktopUpdates, {
5959 getState: Effect.die("unexpected getState"),
......@@ -86,7 +86,7 @@ const makeElectronMenuLayer = (
8686 Deferred.succeed(applicationMenuTemplate, template).pipe(Effect.asVoid),
8787 popupTemplate: () => Effect.void,
8888 showContextMenu: () => Effect.succeed(Option.none()),
89 } satisfies ElectronMenu.ElectronMenuShape);
89 } satisfies ElectronMenu.ElectronMenu["Service"]);
9090
9191describe("DesktopApplicationMenu", () => {
9292 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";
66
77import type * as Electron from "electron";
88
9import * as DesktopObservability from "../app/DesktopObservability.ts";
9import { makeComponentLogger } from "../app/DesktopObservability.ts";
1010import * as ElectronApp from "../electron/ElectronApp.ts";
1111import * as ElectronDialog from "../electron/ElectronDialog.ts";
1212import * as ElectronMenu from "../electron/ElectronMenu.ts";
......@@ -26,9 +26,9 @@ type DesktopApplicationMenuRuntimeServices =
2626 | DesktopWindow.DesktopWindow
2727 | 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
3333const dispatchMenuAction = Effect.fn("desktop.menu.dispatchMenuAction")(function* (
3434 action: string,
apps/desktop/src/window/DesktopWindow.ts+6-6
......@@ -8,15 +8,15 @@ import type * as Electron from "electron";
88
99import * as DesktopAssets from "../app/DesktopAssets.ts";
1010import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
11import * as DesktopObservability from "../app/DesktopObservability.ts";
11import { makeComponentLogger } from "../app/DesktopObservability.ts";
1212import * as DesktopState from "../app/DesktopState.ts";
13import * as PreviewManager from "../preview/Manager.ts";
1413import * as ElectronMenu from "../electron/ElectronMenu.ts";
14import { getDesktopUrl } from "../electron/ElectronProtocol.ts";
1515import * as ElectronShell from "../electron/ElectronShell.ts";
1616import * as ElectronTheme from "../electron/ElectronTheme.ts";
1717import * as ElectronWindow from "../electron/ElectronWindow.ts";
18import { getDesktopUrl } from "../electron/ElectronProtocol.ts";
19import * as IpcChannels from "../ipc/channels.ts";
18import { MENU_ACTION_CHANNEL } from "../ipc/channels.ts";
19import * as PreviewManager from "../preview/Manager.ts";
2020
2121const TITLEBAR_HEIGHT = 40;
2222const TITLEBAR_COLOR = "#01000000"; // #00000000 does not work correctly on Linux
......@@ -57,7 +57,7 @@ export class DesktopWindow extends Context.Service<
5757>()("@t3tools/desktop/window/DesktopWindow") {}
5858
5959const { logInfo: logWindowInfo, logWarning: logWindowWarning } =
60 DesktopObservability.makeComponentLogger("desktop-window");
60 makeComponentLogger("desktop-window");
6161
6262function getIconOption(
6363 iconPaths: DesktopAssets.DesktopIconPaths,
......@@ -380,7 +380,7 @@ export const make = Effect.gen(function* () {
380380
381381 const send = () => {
382382 if (targetWindow.isDestroyed()) return;
383 targetWindow.webContents.send(IpcChannels.MENU_ACTION_CHANNEL, action);
383 targetWindow.webContents.send(MENU_ACTION_CHANNEL, action);
384384 void runPromise(electronWindow.reveal(targetWindow));
385385 };
386386