Skip to content

Commit bfb2e42

Browse files
refactor(desktop): send capture lifecycle events on one typed channel
Five prefixed strings rode the generic menu-action channel and were parsed by prefix in the renderer, next to a separate ready channel. One DesktopSnapShotEvent union now carries requested, started, ready, failed, and shortcut-changed, and only started reveals the window. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 5e0bb76 commit bfb2e42

16 files changed

Lines changed: 166 additions & 133 deletions

apps/desktop/src/app/DesktopLifecycle.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ function makeDesktopWindowLayer(
9595
flushMainWindowBounds: input.flushMainWindowBounds ?? Effect.void,
9696
prepareCaptureReveal: Effect.void,
9797
dispatchMenuAction: () => Effect.void,
98-
dispatchSnapShotReady: () => Effect.void,
98+
dispatchSnapShotEvent: () => Effect.void,
9999
zoomMain: () => Effect.void,
100100
syncAppearance: Effect.void,
101101
});

apps/desktop/src/backend/DesktopBackendPool.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ function makePoolLayer(
9898
flushMainWindowBounds: Effect.void,
9999
prepareCaptureReveal: Effect.void,
100100
dispatchMenuAction: () => Effect.die("unexpected menu action"),
101-
dispatchSnapShotReady: () => Effect.void,
101+
dispatchSnapShotEvent: () => Effect.void,
102102
zoomMain: () => Effect.die("unexpected zoom"),
103103
syncAppearance: Effect.void,
104104
} satisfies DesktopWindow.DesktopWindow["Service"]),

apps/desktop/src/ipc/channels.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
77
export const OPEN_SYSTEM_SETTINGS_CHANNEL = "desktop:open-system-settings";
88
export const PROBE_REMOTE_EDITORS_CHANNEL = "desktop:probe-remote-editors";
99
export const MENU_ACTION_CHANNEL = "desktop:menu-action";
10-
export const SNAP_SHOT_READY_CHANNEL = "desktop:snap-shot-ready";
10+
export const SNAP_SHOT_EVENT_CHANNEL = "desktop:snap-shot-event";
1111
export const QUIT_SHORTCUT_CHANNEL = "desktop:quit-shortcut";
1212
export const GET_WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:get-window-fullscreen-state";
1313
export const WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:window-fullscreen-state";

apps/desktop/src/preload.ts

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,30 @@ import type {
33
DesktopPreviewPointerEvent,
44
DesktopPreviewRecordingFrame,
55
DesktopPreviewTabState,
6+
DesktopSnapShotEvent,
67
} from "@t3tools/contracts";
78
import { exposeClerkBridge } from "@clerk/electron/preload";
89
import { contextBridge, ipcRenderer } from "electron";
910

1011
import * as IpcChannels from "./ipc/channels.ts";
1112

13+
const SNAP_SHOT_EVENT_TYPES = new Set([
14+
"requested",
15+
"started",
16+
"ready",
17+
"failed",
18+
"shortcut-changed",
19+
]);
20+
function isSnapShotEvent(value: unknown): value is DesktopSnapShotEvent {
21+
if (typeof value !== "object" || value === null) return false;
22+
const { type, id } = value as { type?: unknown; id?: unknown };
23+
return (
24+
typeof type === "string" &&
25+
SNAP_SHOT_EVENT_TYPES.has(type) &&
26+
(id === undefined || typeof id === "string")
27+
);
28+
}
29+
1230
exposeClerkBridge({ passkeys: true });
1331

1432
// oxlint-disable-next-line t3code/no-global-process-runtime -- Electron exposes the client platform in its sandboxed preload process.
@@ -148,15 +166,15 @@ contextBridge.exposeInMainWorld("desktopBridge", {
148166
ipcRenderer.removeListener(IpcChannels.MENU_ACTION_CHANNEL, wrappedListener);
149167
};
150168
},
151-
onSnapShotReady: (listener) => {
152-
const wrappedListener = (_event: Electron.IpcRendererEvent, id: unknown) => {
153-
if (typeof id !== "string") return;
154-
listener(id as Parameters<typeof listener>[0]);
169+
onSnapShotEvent: (listener) => {
170+
const wrappedListener = (_event: Electron.IpcRendererEvent, event: unknown) => {
171+
if (!isSnapShotEvent(event)) return;
172+
listener(event);
155173
};
156174

157-
ipcRenderer.on(IpcChannels.SNAP_SHOT_READY_CHANNEL, wrappedListener);
175+
ipcRenderer.on(IpcChannels.SNAP_SHOT_EVENT_CHANNEL, wrappedListener);
158176
return () => {
159-
ipcRenderer.removeListener(IpcChannels.SNAP_SHOT_READY_CHANNEL, wrappedListener);
177+
ipcRenderer.removeListener(IpcChannels.SNAP_SHOT_EVENT_CHANNEL, wrappedListener);
160178
};
161179
},
162180
onQuitShortcut: (listener) => {

apps/desktop/src/snapShot/DesktopSnapShot.test.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
DEFAULT_CLIENT_SETTINGS,
44
DesktopPendingSnapShot,
55
type ClientSettings,
6+
type DesktopSnapShotEvent,
67
} from "@t3tools/contracts";
78
import * as Crypto from "effect/Crypto";
89
import * as Effect from "effect/Effect";
@@ -498,7 +499,7 @@ const testLayer = (
498499
activate: Effect.void,
499500
prepareCaptureReveal: Effect.sync(prepareCaptureRevealMock),
500501
dispatchMenuAction: () => Effect.void,
501-
dispatchSnapShotReady: () => Effect.void,
502+
dispatchSnapShotEvent: () => Effect.void,
502503
} as unknown as DesktopWindow.DesktopWindow["Service"]),
503504
),
504505
FileSystem.layerNoop(fileSystemOverrides),
@@ -672,20 +673,23 @@ function concurrentCaptureFixture(platform: NodeJS.Platform, animations: boolean
672673
state.preparations++;
673674
state.preparedWithoutOverlay &&= flashWindows.every((window) => window.destroyed);
674675
}),
675-
dispatchMenuAction: (action: string, options?: { readonly reveal?: boolean }) => {
676-
if (action.startsWith("snap-shot-requested:")) {
677-
return Effect.sync(() => {
678-
assert.isFalse(options?.reveal);
679-
requestedIds.push(action.slice("snap-shot-requested:".length));
680-
});
676+
dispatchMenuAction: () => Effect.void,
677+
dispatchSnapShotEvent: (event: DesktopSnapShotEvent) => {
678+
switch (event.type) {
679+
case "requested":
680+
return Effect.sync(() => {
681+
requestedIds.push(event.id);
682+
});
683+
case "started":
684+
return handoff;
685+
case "ready":
686+
return Effect.sync(() => {
687+
readyIds.push(event.id);
688+
});
689+
default:
690+
return Effect.void;
681691
}
682-
if (!action.startsWith("snap-shot-started:")) return Effect.void;
683-
return handoff;
684692
},
685-
dispatchSnapShotReady: (id: string) =>
686-
Effect.sync(() => {
687-
readyIds.push(id);
688-
}),
689693
} as unknown as DesktopWindow.DesktopWindow["Service"]),
690694
),
691695
);

apps/desktop/src/snapShot/DesktopSnapShot.ts

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
type SnapShotModifier,
1818
type SnapShotModifierPairShortcut,
1919
type SnapShotShortcut,
20+
type DesktopSnapShotEvent,
21+
type DesktopSnapShotId,
2022
} from "@t3tools/contracts";
2123
import * as Clock from "effect/Clock";
2224
import * as Context from "effect/Context";
@@ -84,7 +86,6 @@ import {
8486
const MAX_CAPTURE_WIDTH = 2_560;
8587
const MAX_CAPTURE_HEIGHT = 1_600;
8688
const SHORTCUT_COOLDOWN_NS = 200_000_000n;
87-
const CAPTURE_FAILED_ACTION = "snap-shot-failed";
8889
const WAYLAND_MODIFIER_PAIR_UNAVAILABLE_MESSAGE =
8990
"Modifier-pair shortcuts aren't available in this Wayland session. Choose another shortcut or use Take snapshot from the command palette.";
9091
const FLASH_ANIMATION_DURATION_MS = 180;
@@ -815,17 +816,14 @@ export const make = Effect.gen(function* () {
815816
stopShiftShortcut = undefined;
816817
};
817818

818-
const notifyFailure = desktopWindow
819-
.dispatchMenuAction(CAPTURE_FAILED_ACTION)
820-
.pipe(Effect.catch(() => Effect.void));
819+
const emit = (event: DesktopSnapShotEvent) =>
820+
desktopWindow.dispatchSnapShotEvent(event).pipe(Effect.catchCause(() => Effect.void));
821821
const setFailure = (message: string, captureId?: string) =>
822822
Ref.update(stateRef, (state) => ({ ...state, message })).pipe(
823823
Effect.andThen(
824-
captureId
825-
? desktopWindow
826-
.dispatchMenuAction(`${CAPTURE_FAILED_ACTION}:${captureId}`)
827-
.pipe(Effect.catch(() => Effect.void))
828-
: notifyFailure,
824+
emit(
825+
captureId ? { type: "failed", id: captureId as DesktopSnapShotId } : { type: "failed" },
826+
),
829827
),
830828
);
831829
const setShortcutFailure = (shortcutMessage: string) =>
@@ -839,7 +837,7 @@ export const make = Effect.gen(function* () {
839837
shortcutMessage,
840838
})),
841839
),
842-
Effect.andThen(notifyFailure),
840+
Effect.andThen(emit({ type: "failed" })),
843841
);
844842

845843
const discardCapture = Effect.fn("desktop.snapShot.discardCapture")(function* (id: string) {
@@ -870,9 +868,7 @@ export const make = Effect.gen(function* () {
870868
flash.dispose();
871869
transition.dispose();
872870
yield* fileSystem.makeDirectory(captureDirectory, { recursive: true });
873-
yield* desktopWindow
874-
.dispatchMenuAction(`snap-shot-requested:${id}`, { reveal: false })
875-
.pipe(Effect.catch(() => Effect.void));
871+
yield* emit({ type: "requested", id: id as DesktopSnapShotId });
876872
const snapshot = yield* Effect.tryPromise({
877873
try: () =>
878874
captureSource({
@@ -902,9 +898,7 @@ export const make = Effect.gen(function* () {
902898
);
903899
}
904900
if (snapshot.animationStarted) {
905-
yield* desktopWindow
906-
.dispatchMenuAction(`snap-shot-started:${id}`)
907-
.pipe(Effect.catchCause(() => Effect.void));
901+
yield* emit({ type: "started", id: id as DesktopSnapShotId });
908902
} else {
909903
yield* desktopWindow.activate.pipe(Effect.catchCause(() => Effect.void));
910904
}
@@ -987,19 +981,13 @@ export const make = Effect.gen(function* () {
987981
yield* persistCapture(capture).pipe(
988982
Effect.tap(() =>
989983
Ref.update(stateRef, (state) => ({ ...state, message: null })).pipe(
990-
Effect.andThen(
991-
desktopWindow.dispatchSnapShotReady(capture.id).pipe(Effect.catch(() => Effect.void)),
992-
),
984+
Effect.andThen(emit({ type: "ready", id: capture.id as DesktopSnapShotId })),
993985
),
994986
),
995987
Effect.tapError((error) =>
996988
discardCapture(capture.id).pipe(
997989
Effect.andThen(Ref.update(stateRef, (state) => ({ ...state, message: error.message }))),
998-
Effect.andThen(
999-
desktopWindow
1000-
.dispatchMenuAction(`${CAPTURE_FAILED_ACTION}:${capture.id}`, { reveal: false })
1001-
.pipe(Effect.catch(() => Effect.void)),
1002-
),
990+
Effect.andThen(emit({ type: "failed", id: capture.id as DesktopSnapShotId })),
1003991
),
1004992
),
1005993
);
@@ -1234,9 +1222,7 @@ export const make = Effect.gen(function* () {
12341222
() => {
12351223
if (generation !== shortcutGeneration) return;
12361224
shortcutVerified = false;
1237-
void runPromise(desktopWindow.dispatchMenuAction("snap-shot-shortcut-changed")).catch(
1238-
() => undefined,
1239-
);
1225+
void runPromise(emit({ type: "shortcut-changed" })).catch(() => undefined);
12401226
},
12411227
undefined,
12421228
hyprland,

apps/desktop/src/window/DesktopApplicationMenu.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ const makeDesktopWindowLayer = (selectedAction: Deferred.Deferred<string>) =>
8686
flushMainWindowBounds: Effect.void,
8787
prepareCaptureReveal: Effect.void,
8888
dispatchMenuAction: (action) => Deferred.succeed(selectedAction, action).pipe(Effect.asVoid),
89-
dispatchSnapShotReady: () => Effect.void,
89+
dispatchSnapShotEvent: () => Effect.void,
9090
zoomMain: (direction) =>
9191
Deferred.succeed(selectedAction, `zoom-${direction}`).pipe(Effect.asVoid),
9292
syncAppearance: Effect.void,

apps/desktop/src/window/DesktopWindow.test.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as NodeServices from "@effect/platform-node/NodeServices";
22
import { assert, describe, it } from "@effect/vitest";
33
import * as Deferred from "effect/Deferred";
4+
import { DesktopSnapShotId } from "@t3tools/contracts";
45
import * as Effect from "effect/Effect";
56
import * as Fiber from "effect/Fiber";
67
import * as Layer from "effect/Layer";
@@ -45,7 +46,7 @@ import * as ElectronTheme from "../electron/ElectronTheme.ts";
4546
import * as ElectronWindow from "../electron/ElectronWindow.ts";
4647
import {
4748
MENU_ACTION_CHANNEL,
48-
SNAP_SHOT_READY_CHANNEL,
49+
SNAP_SHOT_EVENT_CHANNEL,
4950
WINDOW_FULLSCREEN_STATE_CHANNEL,
5051
} from "../ipc/channels.ts";
5152
import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";
@@ -407,6 +408,9 @@ const makeSplashScenario = (createOutcomes: readonly (Electron.BrowserWindow | n
407408
return { layer, createCalls, mainWindow, revealedWindows } as const;
408409
});
409410

411+
const captureOne = DesktopSnapShotId.make("11111111-1111-4111-8111-111111111111");
412+
const captureTwo = DesktopSnapShotId.make("22222222-2222-4222-8222-222222222222");
413+
410414
describe("DesktopWindow", () => {
411415
it("leaves fullscreen before concealing a pending quit", () => {
412416
const fakeWindow = makeFakeBrowserWindow();
@@ -1292,20 +1296,18 @@ describe("DesktopWindow", () => {
12921296
yield* Effect.gen(function* () {
12931297
const desktopWindow = yield* DesktopWindow.DesktopWindow;
12941298
yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1295-
yield* desktopWindow.dispatchMenuAction("snap-shot-started:capture-1");
1299+
yield* desktopWindow.dispatchSnapShotEvent({ type: "started", id: captureOne });
12961300
assert.equal(foreground, "T3 Code");
12971301
foreground = "Explorer";
1298-
yield* desktopWindow.dispatchSnapShotReady("capture-1");
1299-
yield* desktopWindow.dispatchMenuAction("snap-shot-failed:capture-2", {
1300-
reveal: false,
1301-
});
1302+
yield* desktopWindow.dispatchSnapShotEvent({ type: "ready", id: captureOne });
1303+
yield* desktopWindow.dispatchSnapShotEvent({ type: "failed", id: captureTwo });
13021304

13031305
assert.equal(foreground, "Explorer");
13041306
assert.deepEqual(operations, ["send", "reveal", "send", "send"]);
13051307
assert.deepEqual(fakeWindow.send.mock.calls, [
1306-
[MENU_ACTION_CHANNEL, "snap-shot-started:capture-1"],
1307-
[SNAP_SHOT_READY_CHANNEL, "capture-1"],
1308-
[MENU_ACTION_CHANNEL, "snap-shot-failed:capture-2"],
1308+
[SNAP_SHOT_EVENT_CHANNEL, { type: "started", id: captureOne }],
1309+
[SNAP_SHOT_EVENT_CHANNEL, { type: "ready", id: captureOne }],
1310+
[SNAP_SHOT_EVENT_CHANNEL, { type: "failed", id: captureTwo }],
13091311
]);
13101312
}).pipe(Effect.provide(layer));
13111313
}),
@@ -1328,10 +1330,12 @@ describe("DesktopWindow", () => {
13281330
yield* Effect.gen(function* () {
13291331
const desktopWindow = yield* DesktopWindow.DesktopWindow;
13301332
yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
1331-
yield* Effect.exit(desktopWindow.dispatchMenuAction("snap-shot-started:capture-1"));
1333+
yield* Effect.exit(
1334+
desktopWindow.dispatchSnapShotEvent({ type: "started", id: captureOne }),
1335+
);
13321336

13331337
assert.deepEqual(fakeWindow.send.mock.calls, [
1334-
[MENU_ACTION_CHANNEL, "snap-shot-started:capture-1"],
1338+
[SNAP_SHOT_EVENT_CHANNEL, { type: "started", id: captureOne }],
13351339
]);
13361340
}).pipe(Effect.provide(layer));
13371341
}),
@@ -1345,7 +1349,7 @@ describe("DesktopWindow", () => {
13451349
yield* Effect.gen(function* () {
13461350
const desktopWindow = yield* DesktopWindow.DesktopWindow;
13471351
yield* desktopWindow.showConnectingSplash;
1348-
yield* desktopWindow.dispatchSnapShotReady("capture-1");
1352+
yield* desktopWindow.dispatchSnapShotEvent({ type: "ready", id: captureOne });
13491353

13501354
assert.equal(yield* Ref.get(scenario.createCalls), 1);
13511355
assert.equal(splash.send.mock.calls.length, 0);
@@ -1367,7 +1371,7 @@ describe("DesktopWindow", () => {
13671371
yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
13681372
fakeWindow.isDestroyed.mockReturnValue(true);
13691373
yield* Ref.set(mainWindow, Option.none());
1370-
yield* desktopWindow.dispatchSnapShotReady("capture-1");
1374+
yield* desktopWindow.dispatchSnapShotEvent({ type: "ready", id: captureOne });
13711375

13721376
assert.equal(yield* Ref.get(createCount), 1);
13731377
assert.equal(fakeWindow.send.mock.calls.length, 0);
@@ -1389,15 +1393,17 @@ describe("DesktopWindow", () => {
13891393

13901394
yield* Effect.gen(function* () {
13911395
const desktopWindow = yield* DesktopWindow.DesktopWindow;
1392-
yield* desktopWindow.dispatchSnapShotReady("capture-1");
1396+
yield* desktopWindow.dispatchSnapShotEvent({ type: "ready", id: captureOne });
13931397
assert.equal(fakeWindow.send.mock.calls.length, 0);
13941398
const onLoad = fakeWindow.webContentsOnce.mock.calls.find(
13951399
([event]) => event === "did-finish-load",
13961400
)?.[1];
13971401
assert.isDefined(onLoad);
13981402
onLoad?.();
13991403

1400-
assert.deepEqual(fakeWindow.send.mock.calls, [[SNAP_SHOT_READY_CHANNEL, "capture-1"]]);
1404+
assert.deepEqual(fakeWindow.send.mock.calls, [
1405+
[SNAP_SHOT_EVENT_CHANNEL, { type: "ready", id: captureOne }],
1406+
]);
14011407
assert.equal(onReveal.mock.calls.length, 0);
14021408
assert.equal(yield* Ref.get(createCount), 0);
14031409
}).pipe(Effect.provide(layer));

0 commit comments

Comments
 (0)