authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:02:37-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:02:37-07:00
log8331511b972242301bdaec328148a88b9b6cfa6c
tree98b8ee7b5a36cbc545c4e51ffd338eae43383285
parent08650a7424f0f4d28c36d50938c0027e5b4cafff
signature Signed by PGP key B5690EEEBB952194

[codex] structure Electron theme source errors (#3294)

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

2 files changed, 44 insertions(+), 5 deletions(-)

apps/desktop/src/electron/ElectronTheme.test.ts+22
...@@ -8,6 +8,7 @@ const { onMock, removeListenerMock, themeState } = vi.hoisted(() => ({...@@ -8,6 +8,7 @@ const { onMock, removeListenerMock, themeState } = vi.hoisted(() => ({
8 themeState: {8 themeState: {
9 shouldUseDarkColors: true,9 shouldUseDarkColors: true,
10 themeSource: "system",10 themeSource: "system",
11 setSourceError: null as unknown,
11 },12 },
12}));13}));
1314
...@@ -17,6 +18,9 @@ vi.mock("electron", () => ({...@@ -17,6 +18,9 @@ vi.mock("electron", () => ({
17 return themeState.shouldUseDarkColors;18 return themeState.shouldUseDarkColors;
18 },19 },
19 set themeSource(value: string) {20 set themeSource(value: string) {
21 if (themeState.setSourceError !== null) {
22 throw themeState.setSourceError;
23 }
20 themeState.themeSource = value;24 themeState.themeSource = value;
21 },25 },
22 on: onMock,26 on: onMock,
...@@ -32,6 +36,7 @@ describe("ElectronTheme", () => {...@@ -32,6 +36,7 @@ describe("ElectronTheme", () => {
32 removeListenerMock.mockClear();36 removeListenerMock.mockClear();
33 themeState.shouldUseDarkColors = true;37 themeState.shouldUseDarkColors = true;
34 themeState.themeSource = "system";38 themeState.themeSource = "system";
39 themeState.setSourceError = null;
35 });40 });
3641
37 it.effect("scopes native theme update listeners", () =>42 it.effect("scopes native theme update listeners", () =>
...@@ -49,4 +54,21 @@ describe("ElectronTheme", () => {...@@ -49,4 +54,21 @@ describe("ElectronTheme", () => {
49 assert.deepEqual(removeListenerMock.mock.calls, [["updated", listener]]);54 assert.deepEqual(removeListenerMock.mock.calls, [["updated", listener]]);
50 }).pipe(Effect.provide(ElectronTheme.layer)),55 }).pipe(Effect.provide(ElectronTheme.layer)),
51 );56 );
57
58 it.effect("preserves the requested source and cause when setting the theme fails", () =>
59 Effect.gen(function* () {
60 const cause = new Error("theme source failed");
61 themeState.setSourceError = cause;
62 const electronTheme = yield* ElectronTheme.ElectronTheme;
63
64 const error = yield* Effect.flip(electronTheme.setSource("dark"));
65
66 assert.instanceOf(error, ElectronTheme.ElectronThemeSetSourceError);
67 assert.isTrue(ElectronTheme.isElectronThemeSetSourceError(error));
68 assert.strictEqual(error.source, "dark");
69 assert.strictEqual(error.cause, cause);
70 assert.include(error.message, "dark");
71 assert.notInclude(error.message, cause.message);
72 }).pipe(Effect.provide(ElectronTheme.layer)),
73 );
52});74});
apps/desktop/src/electron/ElectronTheme.ts+22-5
...@@ -1,16 +1,31 @@...@@ -1,16 +1,31 @@
1import type { DesktopTheme } from "@t3tools/contracts";1import { DesktopThemeSchema, type DesktopTheme } from "@t3tools/contracts";
2import * as Context from "effect/Context";2import * as Context from "effect/Context";
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 Schema from "effect/Schema";
5import * as Scope from "effect/Scope";6import * as Scope from "effect/Scope";
67
7import * as Electron from "electron";8import * as Electron from "electron";
89
10export class ElectronThemeSetSourceError extends Schema.TaggedErrorClass<ElectronThemeSetSourceError>()(
11 "ElectronThemeSetSourceError",
12 {
13 source: DesktopThemeSchema,
14 cause: Schema.Defect(),
15 },
16) {
17 override get message(): string {
18 return `Failed to set the Electron theme source to ${this.source}.`;
19 }
20}
21
22export const isElectronThemeSetSourceError = Schema.is(ElectronThemeSetSourceError);
23
9export class ElectronTheme extends Context.Service<24export class ElectronTheme extends Context.Service<
10 ElectronTheme,25 ElectronTheme,
11 {26 {
12 readonly shouldUseDarkColors: Effect.Effect<boolean>;27 readonly shouldUseDarkColors: Effect.Effect<boolean>;
13 readonly setSource: (theme: DesktopTheme) => Effect.Effect<void>;28 readonly setSource: (theme: DesktopTheme) => Effect.Effect<void, ElectronThemeSetSourceError>;
14 readonly onUpdated: (listener: () => void) => Effect.Effect<void, never, Scope.Scope>;29 readonly onUpdated: (listener: () => void) => Effect.Effect<void, never, Scope.Scope>;
15 }30 }
16>()("@t3tools/desktop/electron/ElectronTheme") {}31>()("@t3tools/desktop/electron/ElectronTheme") {}
...@@ -18,9 +33,11 @@ export class ElectronTheme extends Context.Service<...@@ -18,9 +33,11 @@ export class ElectronTheme extends Context.Service<
18export const make = ElectronTheme.of({33export const make = ElectronTheme.of({
19 shouldUseDarkColors: Effect.sync(() => Electron.nativeTheme.shouldUseDarkColors),34 shouldUseDarkColors: Effect.sync(() => Electron.nativeTheme.shouldUseDarkColors),
20 setSource: (theme) =>35 setSource: (theme) =>
21 Effect.suspend(() => {36 Effect.try({
22 Electron.nativeTheme.themeSource = theme;37 try: () => {
23 return Effect.void;38 Electron.nativeTheme.themeSource = theme;
39 },
40 catch: (cause) => new ElectronThemeSetSourceError({ source: theme, cause }),
24 }),41 }),
25 onUpdated: (listener) =>42 onUpdated: (listener) =>
26 Effect.acquireRelease(43 Effect.acquireRelease(