1import {
2 DesktopPendingSnapShot,
3 DesktopSnapShot as DesktopSnapShotSchema,
4 DesktopSnapShotAnimationDestination,
5 DesktopSnapShotId,
6 DesktopSnapShotShortcutAvailability,
7 DesktopSnapShotState,
8 DesktopSnapShotSetupAction,
9 SnapShotShortcut,
10 DesktopCaptureConfigRequest,
11 DesktopCaptureConfigPreview,
12 DesktopCaptureConfigApplied,
13} from "@t3tools/contracts";
14import * as Effect from "effect/Effect";
15import * as Option from "effect/Option";
16import * as Schema from "effect/Schema";
17import type * as Electron from "electron";
18
19import * as ElectronWindow from "../../electron/ElectronWindow.ts";
20import * as ElectronDialog from "../../electron/ElectronDialog.ts";
21import * as DesktopSnapShot from "../../snapShot/DesktopSnapShot.ts";
22import * as IpcChannels from "../channels.ts";
23import * as DesktopIpc from "../DesktopIpc.ts";
24
25class SnapShotIpcUnauthorizedSenderError extends Schema.TaggedError<SnapShotIpcUnauthorizedSenderError>()(
26 "SnapShotIpcUnauthorizedSenderError",
27 {},
28) {
29 override get message(): string {
30 return "Snapshot request was rejected.";
31 }
32}
33
34const ensureTrustedSnapShotSender = Effect.fn("desktop.ipc.snapShot.ensureTrustedSender")(
35 function* (event: DesktopIpc.DesktopIpcInvokeEvent | undefined) {
36 const main = yield* (yield* ElectronWindow.ElectronWindow).main;
37 if (
38 event === undefined ||
39 Option.isNone(main) ||
40 main.value.webContents.id !== event.sender.id
41 ) {
42 return yield* new SnapShotIpcUnauthorizedSenderError();
43 }
44 return main.value;
45 },
46);
47
48export function snapShotScreenFrame(
49 viewportFrame: DesktopSnapShotAnimationDestination["viewportFrame"],
50 contentBounds: Electron.Rectangle,
51 zoomFactor: number,
52): Electron.Rectangle {
53 return {
54 x: contentBounds.x + viewportFrame.x * zoomFactor,
55 y: contentBounds.y + viewportFrame.y * zoomFactor,
56 width: viewportFrame.width * zoomFactor,
57 height: viewportFrame.height * zoomFactor,
58 };
59}
60
61export function snapShotRelativeFrame(
62 frame: DesktopSnapShotAnimationDestination["viewportFrame"],
63 bounds: Electron.Rectangle,
64 zoom: number,
65): Electron.Rectangle | undefined {
66 if (bounds.width <= 0 || bounds.height <= 0) return undefined;
67 return {
68 x: (frame.x * zoom) / bounds.width,
69 y: (frame.y * zoom) / bounds.height,
70 width: (frame.width * zoom) / bounds.width,
71 height: (frame.height * zoom) / bounds.height,
72 };
73}
74
75export const getSnapShotState = DesktopIpc.makeIpcMethod({
76 channel: IpcChannels.GET_SNAP_SHOT_STATE_CHANNEL,
77 payload: Schema.Void,
78 result: DesktopSnapShotState,
79 handler: Effect.fn("desktop.ipc.snapShot.getState")(function* () {
80 return yield* (yield* DesktopSnapShot.DesktopSnapShot).state;
81 }),
82});
83
84export const requestSnapShotPermissions = DesktopIpc.makeIpcMethod({
85 channel: IpcChannels.REQUEST_SNAP_SHOT_PERMISSIONS_CHANNEL,
86 payload: Schema.Boolean,
87 result: Schema.Void,
88 handler: Effect.fn("desktop.ipc.snapShot.requestPermissions")(
89 function* (includeAccessibility, event) {
90 yield* ensureTrustedSnapShotSender(event);
91 yield* (yield* DesktopSnapShot.DesktopSnapShot).requestPermissions(includeAccessibility);
92 },
93 ),
94});
95
96export const setupSnapShot = DesktopIpc.makeIpcMethod({
97 channel: IpcChannels.SETUP_SNAP_SHOT_CHANNEL,
98 payload: DesktopSnapShotSetupAction,
99 result: Schema.Void,
100 handler: Effect.fn("desktop.ipc.snapShot.setup")(function* (action, event) {
101 yield* ensureTrustedSnapShotSender(event);
102 yield* (yield* DesktopSnapShot.DesktopSnapShot).setup(action);
103 }),
104});
105
106export const previewSnapShotConfig = DesktopIpc.makeIpcMethod({
107 channel: IpcChannels.PREVIEW_SNAP_SHOT_CONFIG_CHANNEL,
108 payload: DesktopCaptureConfigRequest,
109 result: Schema.NullOr(DesktopCaptureConfigPreview),
110 handler: Effect.fn("desktop.ipc.snapShot.previewConfig")(function* (request, event) {
111 const window = yield* ensureTrustedSnapShotSender(event);
112 const capture = yield* DesktopSnapShot.DesktopSnapShot;
113 let selectedPath: string | undefined;
114 if (request.chooseFile) {
115 const state = yield* capture.state;
116 const paths = yield* (yield* ElectronDialog.ElectronDialog).pickFiles({
117 owner: Option.some(window),
118 defaultPath: Option.fromUndefinedOr(state.shortcutConfigPath),
119 filters: [
120 {
121 name: "Desktop config",
122 extensions: state.linuxBackend === "niri" ? ["kdl"] : ["conf", "lua"],
123 },
124 ],
125 multiple: false,
126 });
127 selectedPath = paths[0];
128 if (!selectedPath) return null;
129 }
130 return yield* capture.previewConfig(request, selectedPath);
131 }),
132});
133
134export const applySnapShotConfig = DesktopIpc.makeIpcMethod({
135 channel: IpcChannels.APPLY_SNAP_SHOT_CONFIG_CHANNEL,
136 payload: DesktopSnapShotId,
137 result: DesktopCaptureConfigApplied,
138 handler: Effect.fn("desktop.ipc.snapShot.applyConfig")(function* (id, event) {
139 yield* ensureTrustedSnapShotSender(event);
140 return yield* (yield* DesktopSnapShot.DesktopSnapShot).applyConfig(id);
141 }),
142});
143
144export const checkSnapShotShortcut = DesktopIpc.makeIpcMethod({
145 channel: IpcChannels.CHECK_SNAP_SHOT_SHORTCUT_CHANNEL,
146 payload: SnapShotShortcut,
147 result: DesktopSnapShotShortcutAvailability,
148 handler: Effect.fn("desktop.ipc.snapShot.checkShortcut")(function* (shortcut, event) {
149 yield* ensureTrustedSnapShotSender(event);
150 return yield* (yield* DesktopSnapShot.DesktopSnapShot).checkShortcut(shortcut);
151 }),
152});
153
154export const setSnapShotShortcutSuppressed = DesktopIpc.makeIpcMethod({
155 channel: IpcChannels.SET_SNAP_SHOT_SHORTCUT_SUPPRESSED_CHANNEL,
156 payload: Schema.Boolean,
157 result: Schema.Void,
158 handler: Effect.fn("desktop.ipc.snapShot.setShortcutSuppressed")(function* (suppressed, event) {
159 yield* ensureTrustedSnapShotSender(event);
160 yield* (yield* DesktopSnapShot.DesktopSnapShot).setShortcutSuppressed(suppressed);
161 }),
162});
163
164export const listPendingSnapShots = DesktopIpc.makeIpcMethod({
165 channel: IpcChannels.LIST_PENDING_SNAP_SHOTS_CHANNEL,
166 payload: Schema.Void,
167 result: Schema.Array(DesktopPendingSnapShot),
168 handler: Effect.fn("desktop.ipc.snapShot.listPending")(function* (_, event) {
169 yield* ensureTrustedSnapShotSender(event);
170 return yield* (yield* DesktopSnapShot.DesktopSnapShot).listPending;
171 }),
172});
173
174export const readSnapShot = DesktopIpc.makeIpcMethod({
175 channel: IpcChannels.READ_SNAP_SHOT_CHANNEL,
176 payload: DesktopSnapShotId,
177 result: DesktopSnapShotSchema,
178 handler: Effect.fn("desktop.ipc.snapShot.read")(function* (id, event) {
179 yield* ensureTrustedSnapShotSender(event);
180 return yield* (yield* DesktopSnapShot.DesktopSnapShot).read(id);
181 }),
182});
183
184export const setSnapShotAnimationDestination = DesktopIpc.makeIpcMethod({
185 channel: IpcChannels.SET_SNAP_SHOT_ANIMATION_DESTINATION_CHANNEL,
186 payload: DesktopSnapShotAnimationDestination,
187 result: Schema.Void,
188 handler: Effect.fn("desktop.ipc.snapShot.setAnimationDestination")(
189 function* (destination, event) {
190 const window = yield* ensureTrustedSnapShotSender(event);
191 if (
192 destination.viewportFrame.width <= 0 ||
193 destination.viewportFrame.height <= 0 ||
194 destination.borderWidth < 0 ||
195 destination.cornerRadius < 0
196 ) {
197 return;
198 }
199 yield* (yield* DesktopSnapShot.DesktopSnapShot).setAnimationDestination(destination.id, {
200 relativeFrame: snapShotRelativeFrame(
201 destination.viewportFrame,
202 window.getContentBounds(),
203 window.webContents.getZoomFactor(),
204 ),
205 frame: snapShotScreenFrame(
206 destination.viewportFrame,
207 window.getContentBounds(),
208 window.webContents.getZoomFactor(),
209 ),
210 backgroundColor: destination.backgroundColor,
211 borderColor: destination.borderColor,
212 borderWidth: destination.borderWidth * window.webContents.getZoomFactor(),
213 cornerRadius: destination.cornerRadius * window.webContents.getZoomFactor(),
214 scaleFactor: window.webContents.getZoomFactor(),
215 details: destination.details,
216 });
217 },
218 ),
219});
220
221export const dismissSnapShotAnimation = DesktopIpc.makeIpcMethod({
222 channel: IpcChannels.DISMISS_SNAP_SHOT_ANIMATION_CHANNEL,
223 payload: DesktopSnapShotId,
224 result: Schema.Void,
225 handler: Effect.fn("desktop.ipc.snapShot.dismissAnimation")(function* (id, event) {
226 yield* ensureTrustedSnapShotSender(event);
227 yield* (yield* DesktopSnapShot.DesktopSnapShot).dismissAnimation(id);
228 }),
229});
230
231export const acknowledgeSnapShot = DesktopIpc.makeIpcMethod({
232 channel: IpcChannels.ACKNOWLEDGE_SNAP_SHOT_CHANNEL,
233 payload: DesktopSnapShotId,
234 result: Schema.Void,
235 handler: Effect.fn("desktop.ipc.snapShot.acknowledge")(function* (id, event) {
236 yield* ensureTrustedSnapShotSender(event);
237 yield* (yield* DesktopSnapShot.DesktopSnapShot).acknowledge(id);
238 }),
239});