Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
22 changes: 22 additions & 0 deletions apps/desktop/src/electron/ElectronTheme.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const { onMock, removeListenerMock, themeState } = vi.hoisted(() => ({
themeState: {
shouldUseDarkColors: true,
themeSource: "system",
setSourceError: null as unknown,
},
}));

Expand All @@ -17,6 +18,9 @@ vi.mock("electron", () => ({
return themeState.shouldUseDarkColors;
},
set themeSource(value: string) {
if (themeState.setSourceError !== null) {
throw themeState.setSourceError;
}
themeState.themeSource = value;
},
on: onMock,
Expand All @@ -32,6 +36,7 @@ describe("ElectronTheme", () => {
removeListenerMock.mockClear();
themeState.shouldUseDarkColors = true;
themeState.themeSource = "system";
themeState.setSourceError = null;
});

it.effect("scopes native theme update listeners", () =>
Expand All @@ -49,4 +54,21 @@ describe("ElectronTheme", () => {
assert.deepEqual(removeListenerMock.mock.calls, [["updated", listener]]);
}).pipe(Effect.provide(ElectronTheme.layer)),
);

it.effect("preserves the requested source and cause when setting the theme fails", () =>
Effect.gen(function* () {
const cause = new Error("theme source failed");
themeState.setSourceError = cause;
const electronTheme = yield* ElectronTheme.ElectronTheme;

const error = yield* Effect.flip(electronTheme.setSource("dark"));

assert.instanceOf(error, ElectronTheme.ElectronThemeSetSourceError);
assert.isTrue(ElectronTheme.isElectronThemeSetSourceError(error));
assert.strictEqual(error.source, "dark");
assert.strictEqual(error.cause, cause);
assert.include(error.message, "dark");
assert.notInclude(error.message, cause.message);
}).pipe(Effect.provide(ElectronTheme.layer)),
);
});
27 changes: 22 additions & 5 deletions apps/desktop/src/electron/ElectronTheme.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,43 @@
import type { DesktopTheme } from "@t3tools/contracts";
import { DesktopThemeSchema, type DesktopTheme } from "@t3tools/contracts";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
import * as Scope from "effect/Scope";

import * as Electron from "electron";

export class ElectronThemeSetSourceError extends Schema.TaggedErrorClass<ElectronThemeSetSourceError>()(
"ElectronThemeSetSourceError",
{
source: DesktopThemeSchema,
cause: Schema.Defect(),
},
) {
override get message(): string {
return `Failed to set the Electron theme source to ${this.source}.`;
}
}

export const isElectronThemeSetSourceError = Schema.is(ElectronThemeSetSourceError);

export class ElectronTheme extends Context.Service<
ElectronTheme,
{
readonly shouldUseDarkColors: Effect.Effect<boolean>;
readonly setSource: (theme: DesktopTheme) => Effect.Effect<void>;
readonly setSource: (theme: DesktopTheme) => Effect.Effect<void, ElectronThemeSetSourceError>;
readonly onUpdated: (listener: () => void) => Effect.Effect<void, never, Scope.Scope>;
}
>()("@t3tools/desktop/electron/ElectronTheme") {}

export const make = ElectronTheme.of({
shouldUseDarkColors: Effect.sync(() => Electron.nativeTheme.shouldUseDarkColors),
setSource: (theme) =>
Effect.suspend(() => {
Electron.nativeTheme.themeSource = theme;
return Effect.void;
Effect.try({
try: () => {
Electron.nativeTheme.themeSource = theme;
},
catch: (cause) => new ElectronThemeSetSourceError({ source: theme, cause }),
}),
onUpdated: (listener) =>
Effect.acquireRelease(
Expand Down
Loading