Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/desktop/src/app/DesktopAppIdentity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";

import type * as Electron from "electron";
import type { AboutPanelOptionsOptions } from "electron";

import * as ElectronApp from "../electron/ElectronApp.ts";
import * as DesktopAppIdentity from "./DesktopAppIdentity.ts";
Expand All @@ -30,7 +30,7 @@ type TestEnvironmentInput = Partial<DesktopEnvironment.MakeDesktopEnvironmentInp
};

interface ElectronAppCalls {
readonly setAboutPanelOptions: Array<Electron.AboutPanelOptionsOptions>;
readonly setAboutPanelOptions: Array<AboutPanelOptionsOptions>;
readonly setDockIcon: string[];
readonly setName: string[];
}
Expand Down
9 changes: 4 additions & 5 deletions apps/desktop/src/app/DesktopConnectionCatalogStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ function makeSafeStorageLayer(available: boolean, failDecrypt: Ref.Ref<boolean>
!decoded.startsWith("encrypted:") ||
(failDecrypt !== null && (yield* Ref.get(failDecrypt)))
) {
return yield* new ElectronSafeStorage.ElectronSafeStorageDecryptError({
return yield* new ElectronSafeStorage.ElectronSafeStorageError({
operation: "decrypt a string",
cause: new Error("invalid encrypted catalog"),
});
}
Expand Down Expand Up @@ -237,7 +238,6 @@ describe("DesktopConnectionCatalogStore", () => {
error,
DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreDocumentDecodeError,
);
assert.equal(error.operation, "decode-catalog-document");
assert.equal(error.catalogPath, catalogPath);
assert.exists(error.cause);
assert.equal(yield* fileSystem.readFileString(catalogPath), "{not-json");
Expand Down Expand Up @@ -272,7 +272,6 @@ describe("DesktopConnectionCatalogStore", () => {
error,
DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreReadError,
);
assert.equal(error.operation, "read-catalog");
assert.equal(error.catalogPath, `${baseDir}/userdata/connection-catalog.json`);
assert.strictEqual(error.cause, permissionError);
assert.equal(
Expand Down Expand Up @@ -368,7 +367,6 @@ describe("DesktopConnectionCatalogStore", () => {
error,
DesktopConnectionCatalogStore.DesktopConnectionCatalogStoreDecodeError,
);
assert.equal(error.operation, "decode-encrypted-catalog");
assert.equal(error.resource, "encryptedCatalog");
assert.equal(error.catalogPath, catalogPath);
assert.exists(error.cause);
Expand Down Expand Up @@ -396,7 +394,8 @@ describe("DesktopConnectionCatalogStore", () => {
assert.isTrue(yield* store.set('{"schemaVersion":1,"targets":[]}'));
yield* Ref.set(failDecrypt, true);
const error = yield* store.get.pipe(Effect.flip);
assert.instanceOf(error, ElectronSafeStorage.ElectronSafeStorageDecryptError);
assert.instanceOf(error, ElectronSafeStorage.ElectronSafeStorageError);
assert.equal(error.operation, "decrypt a string");
yield* Ref.set(failDecrypt, false);
assert.deepStrictEqual(yield* store.get, Option.some('{"schemaVersion":1,"targets":[]}'));
}).pipe(Effect.provide(NodeServices.layer), Effect.scoped),
Expand Down
13 changes: 2 additions & 11 deletions apps/desktop/src/app/DesktopConnectionCatalogStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ const writeError = (
export class DesktopConnectionCatalogStoreDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDecodeError>()(
"DesktopConnectionCatalogStoreDecodeError",
{
operation: Schema.Literal("decode-encrypted-catalog"),
resource: Schema.Literal("encryptedCatalog"),
catalogPath: Schema.String,
cause: Schema.Defect(),
Expand All @@ -106,7 +105,6 @@ export class DesktopConnectionCatalogStoreDecodeError extends Schema.TaggedError
export class DesktopConnectionCatalogStoreReadError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreReadError>()(
"DesktopConnectionCatalogStoreReadError",
{
operation: Schema.Literal("read-catalog"),
catalogPath: Schema.String,
cause: Schema.Defect(),
},
Expand All @@ -119,7 +117,6 @@ export class DesktopConnectionCatalogStoreReadError extends Schema.TaggedErrorCl
export class DesktopConnectionCatalogStoreDocumentDecodeError extends Schema.TaggedErrorClass<DesktopConnectionCatalogStoreDocumentDecodeError>()(
"DesktopConnectionCatalogStoreDocumentDecodeError",
{
operation: Schema.Literal("decode-catalog-document"),
catalogPath: Schema.String,
cause: Schema.Defect(),
},
Expand Down Expand Up @@ -167,16 +164,13 @@ export class DesktopConnectionCatalogStore extends Context.Service<
| DesktopConnectionCatalogStoreDocumentDecodeError
| DesktopConnectionCatalogStoreDecodeError
| DesktopConnectionCatalogStoreMigrationError
| ElectronSafeStorage.ElectronSafeStorageAvailabilityError
| ElectronSafeStorage.ElectronSafeStorageDecryptError
| ElectronSafeStorage.ElectronSafeStorageError
>;
readonly set: (
catalog: string,
) => Effect.Effect<
boolean,
| DesktopConnectionCatalogStoreWriteError
| ElectronSafeStorage.ElectronSafeStorageAvailabilityError
| ElectronSafeStorage.ElectronSafeStorageEncryptError
DesktopConnectionCatalogStoreWriteError | ElectronSafeStorage.ElectronSafeStorageError
>;
readonly clear: Effect.Effect<void>;
}
Expand All @@ -190,7 +184,6 @@ function decodeSecretBytes(
Effect.mapError(
(cause) =>
new DesktopConnectionCatalogStoreDecodeError({
operation: "decode-encrypted-catalog",
resource: "encryptedCatalog",
catalogPath,
cause,
Expand All @@ -212,7 +205,6 @@ const readDocument = (
? Effect.succeed<string | null>(null)
: Effect.fail(
new DesktopConnectionCatalogStoreReadError({
operation: "read-catalog",
catalogPath,
cause: error,
}),
Expand All @@ -226,7 +218,6 @@ const readDocument = (
Effect.mapError(
(cause) =>
new DesktopConnectionCatalogStoreDocumentDecodeError({
operation: "decode-catalog-document",
catalogPath,
cause,
}),
Expand Down
12 changes: 6 additions & 6 deletions apps/desktop/src/app/DesktopLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import * as Layer from "effect/Layer";
import * as Ref from "effect/Ref";
import * as Scope from "effect/Scope";

import type * as Electron from "electron";
import type { Event } from "electron";

import * as DesktopEnvironment from "./DesktopEnvironment.ts";
import * as DesktopObservability from "./DesktopObservability.ts";
import { makeComponentLogger } from "./DesktopObservability.ts";
import * as DesktopShutdown from "./DesktopShutdown.ts";
import * as ElectronApp from "../electron/ElectronApp.ts";
import * as ElectronTheme from "../electron/ElectronTheme.ts";
Expand Down Expand Up @@ -37,7 +37,7 @@ export class DesktopLifecycle extends Context.Service<
>()("@t3tools/desktop/app/DesktopLifecycle") {}

const { logInfo: logLifecycleInfo, logError: logLifecycleError } =
DesktopObservability.makeComponentLogger("desktop-lifecycle");
makeComponentLogger("desktop-lifecycle");

function addScopedListener<Args extends ReadonlyArray<unknown>>(
target: unknown,
Expand Down Expand Up @@ -69,7 +69,7 @@ const requestDesktopShutdownAndWait = Effect.fn("desktop.lifecycle.requestShutdo
);

function handleBeforeQuit(
event: Electron.Event,
event: Event,
runEffect: <A, E>(effect: Effect.Effect<A, E, DesktopLifecycleRuntimeServices>) => Promise<A>,
allowQuit: () => boolean,
markQuitAllowed: () => void,
Expand Down Expand Up @@ -122,7 +122,7 @@ function quitFromSignal(
);
}

const make = DesktopLifecycle.of({
export const make = DesktopLifecycle.of({
relaunch: Effect.fn("desktop.lifecycle.relaunch")(function* (reason) {
const electronApp = yield* ElectronApp.ElectronApp;
const environment = yield* DesktopEnvironment.DesktopEnvironment;
Expand Down Expand Up @@ -164,7 +164,7 @@ const make = DesktopLifecycle.of({
desktopWindow.syncAppearance.pipe(Effect.withSpan("desktop.lifecycle.themeUpdated")),
);
});
yield* electronApp.on("before-quit", (event: Electron.Event) => {
yield* electronApp.on("before-quit", (event: Event) => {
handleBeforeQuit(
event,
runEffect,
Expand Down
115 changes: 54 additions & 61 deletions apps/desktop/src/electron/ElectronApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Scope from "effect/Scope";

import * as Electron from "electron";
import { app, type AboutPanelOptionsOptions, type App, type RelaunchOptions } from "electron";

export interface ElectronAppMetadata {
readonly appVersion: string;
Expand All @@ -13,121 +13,114 @@ export interface ElectronAppMetadata {
readonly runningUnderArm64Translation: boolean;
}

export interface ElectronAppShape {
readonly metadata: Effect.Effect<ElectronAppMetadata>;
readonly name: Effect.Effect<string>;
readonly whenReady: Effect.Effect<void>;
readonly quit: Effect.Effect<void>;
readonly exit: (code: number) => Effect.Effect<void>;
readonly relaunch: (options: Electron.RelaunchOptions) => Effect.Effect<void>;
readonly setPath: (
name: Parameters<Electron.App["setPath"]>[0],
path: string,
) => Effect.Effect<void>;
readonly setName: (name: string) => Effect.Effect<void>;
readonly setAboutPanelOptions: (
options: Electron.AboutPanelOptionsOptions,
) => Effect.Effect<void>;
readonly setAppUserModelId: (id: string) => Effect.Effect<void>;
readonly requestSingleInstanceLock: Effect.Effect<boolean>;
readonly isDefaultProtocolClient: (protocol: string) => Effect.Effect<boolean>;
readonly setAsDefaultProtocolClient: (
protocol: string,
path?: string,
args?: readonly string[],
) => Effect.Effect<boolean>;
readonly setDesktopName: (desktopName: string) => Effect.Effect<void>;
readonly setDockIcon: (iconPath: string) => Effect.Effect<void>;
readonly appendCommandLineSwitch: (switchName: string, value?: string) => Effect.Effect<void>;
readonly on: <Args extends ReadonlyArray<unknown>>(
eventName: string,
listener: (...args: Args) => void,
) => Effect.Effect<void, never, Scope.Scope>;
}

export class ElectronApp extends Context.Service<ElectronApp, ElectronAppShape>()(
"@t3tools/desktop/electron/ElectronApp",
) {}
export class ElectronApp extends Context.Service<
ElectronApp,
{
readonly metadata: Effect.Effect<ElectronAppMetadata>;
readonly name: Effect.Effect<string>;
readonly whenReady: Effect.Effect<void>;
readonly quit: Effect.Effect<void>;
readonly exit: (code: number) => Effect.Effect<void>;
readonly relaunch: (options: RelaunchOptions) => Effect.Effect<void>;
readonly setPath: (name: Parameters<App["setPath"]>[0], path: string) => Effect.Effect<void>;
readonly setName: (name: string) => Effect.Effect<void>;
readonly setAboutPanelOptions: (options: AboutPanelOptionsOptions) => Effect.Effect<void>;
readonly setAppUserModelId: (id: string) => Effect.Effect<void>;
readonly requestSingleInstanceLock: Effect.Effect<boolean>;
readonly isDefaultProtocolClient: (protocol: string) => Effect.Effect<boolean>;
readonly setAsDefaultProtocolClient: (
protocol: string,
path?: string,
args?: readonly string[],
) => Effect.Effect<boolean>;
readonly setDesktopName: (desktopName: string) => Effect.Effect<void>;
readonly setDockIcon: (iconPath: string) => Effect.Effect<void>;
readonly appendCommandLineSwitch: (switchName: string, value?: string) => Effect.Effect<void>;
readonly on: <Args extends ReadonlyArray<unknown>>(
eventName: string,
listener: (...args: Args) => void,
) => Effect.Effect<void, never, Scope.Scope>;
}
>()("@t3tools/desktop/electron/ElectronApp") {}

const addScopedAppListener = <Args extends ReadonlyArray<unknown>>(
eventName: string,
listener: (...args: Args) => void,
): Effect.Effect<void, never, Scope.Scope> =>
Effect.acquireRelease(
Effect.sync(() => {
Electron.app.on(eventName as any, listener as any);
app.on(eventName as any, listener as any);
}),
() =>
Effect.sync(() => {
Electron.app.removeListener(eventName as any, listener as any);
app.removeListener(eventName as any, listener as any);
}),
).pipe(Effect.asVoid);

const make = ElectronApp.of({
export const make = ElectronApp.of({
metadata: Effect.sync(() => ({
appVersion: Electron.app.getVersion(),
appPath: Electron.app.getAppPath(),
isPackaged: Electron.app.isPackaged,
appVersion: app.getVersion(),
appPath: app.getAppPath(),
isPackaged: app.isPackaged,
resourcesPath: process.resourcesPath,
runningUnderArm64Translation: Electron.app.runningUnderARM64Translation === true,
runningUnderArm64Translation: app.runningUnderARM64Translation === true,
})),
name: Effect.sync(() => Electron.app.name),
whenReady: Effect.promise(() => Electron.app.whenReady()).pipe(Effect.asVoid),
name: Effect.sync(() => app.name),
whenReady: Effect.promise(() => app.whenReady()).pipe(Effect.asVoid),
quit: Effect.sync(() => {
Electron.app.quit();
app.quit();
}),
exit: (code) =>
Effect.sync(() => {
Electron.app.exit(code);
app.exit(code);
}),
relaunch: (options) =>
Effect.sync(() => {
Electron.app.relaunch(options);
app.relaunch(options);
}),
setPath: (name, path) =>
Effect.sync(() => {
Electron.app.setPath(name, path);
app.setPath(name, path);
}),
setName: (name) =>
Effect.sync(() => {
Electron.app.setName(name);
app.setName(name);
}),
setAboutPanelOptions: (options) =>
Effect.sync(() => {
Electron.app.setAboutPanelOptions(options);
app.setAboutPanelOptions(options);
}),
setAppUserModelId: (id) =>
Effect.sync(() => {
Electron.app.setAppUserModelId(id);
app.setAppUserModelId(id);
}),
requestSingleInstanceLock: Effect.sync(() => Electron.app.requestSingleInstanceLock()),
isDefaultProtocolClient: (protocol) =>
Effect.sync(() => Electron.app.isDefaultProtocolClient(protocol)),
requestSingleInstanceLock: Effect.sync(() => app.requestSingleInstanceLock()),
isDefaultProtocolClient: (protocol) => Effect.sync(() => app.isDefaultProtocolClient(protocol)),
setAsDefaultProtocolClient: (protocol, path, args) =>
Effect.sync(() => {
if (path === undefined) {
return Electron.app.setAsDefaultProtocolClient(protocol);
return app.setAsDefaultProtocolClient(protocol);
}
return Electron.app.setAsDefaultProtocolClient(protocol, path, [...(args ?? [])]);
return app.setAsDefaultProtocolClient(protocol, path, [...(args ?? [])]);
}),
setDesktopName: (desktopName) =>
Effect.sync(() => {
const linuxApp = Electron.app as Electron.App & {
const linuxApp = app as App & {
setDesktopName?: (desktopName: string) => void;
};
linuxApp.setDesktopName?.(desktopName);
}),
setDockIcon: (iconPath) =>
Effect.sync(() => {
Electron.app.dock?.setIcon(iconPath);
app.dock?.setIcon(iconPath);
}),
appendCommandLineSwitch: (switchName, value) =>
Effect.sync(() => {
if (value === undefined) {
Electron.app.commandLine.appendSwitch(switchName);
app.commandLine.appendSwitch(switchName);
return;
}
Electron.app.commandLine.appendSwitch(switchName, value);
app.commandLine.appendSwitch(switchName, value);
}),
on: addScopedAppListener,
});
Expand Down
Loading
Loading