1import { MAC_PERMISSION_SETTINGS_URLS } from "../permissions/MacPermission.ts";
2import {
3 REMOTE_CAPABLE_EDITOR_IDS,
4 remoteSchemeForEditor,
5 type SystemSettingsPane,
6} from "@t3tools/contracts";
7import * as Context from "effect/Context";
8import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";
10import * as Option from "effect/Option";
11
12import * as Electron from "electron";
13
14// Remote open-in-editor deep links (`vscode://vscode-remote/ssh-remote+…`,
15// `zed://ssh/<host>/<path>`, `jetbrains://gateway/ssh/environment?h=…`) must
16// reach the OS handler; every other non-web scheme stays blocked.
17const SAFE_WEB_PROTOCOLS = new Set(["http:", "https:"]);
18const REMOTE_EDITOR_PROTOCOLS = new Set(
19 REMOTE_CAPABLE_EDITOR_IDS.flatMap((id) => {
20 const scheme = remoteSchemeForEditor(id);
21 return scheme === undefined ? [] : [`${scheme}:`];
22 }),
23);
24
25// Zed's host sits in the first path segment, so it needs its own userinfo ban.
26const ZED_SSH_PATHNAME = /^\/[^/@:]+\/.*$/;
27
28const isRemoteEditorUrl = (url: URL) =>
29 REMOTE_EDITOR_PROTOCOLS.has(url.protocol) &&
30 url.username.length === 0 &&
31 url.password.length === 0 &&
32 (url.protocol === "zed:"
33 ? url.host === "ssh" && ZED_SSH_PATHNAME.test(url.pathname)
34 : url.protocol === "jetbrains:"
35 ? url.host === "gateway" &&
36 url.pathname === "/ssh/environment" &&
37 (url.searchParams.get("h") ?? "").length > 0
38 : url.host === "vscode-remote" &&
39 url.pathname.startsWith("/ssh-remote+") &&
40 url.pathname.length > "/ssh-remote+".length);
41
42export function parseSafeExternalUrl(rawUrl: unknown): Option.Option<string> {
43 if (typeof rawUrl !== "string") {
44 return Option.none();
45 }
46
47 try {
48 const url = new URL(rawUrl);
49 return SAFE_WEB_PROTOCOLS.has(url.protocol) || isRemoteEditorUrl(url)
50 ? Option.some(url.href)
51 : Option.none();
52 } catch {
53 return Option.none();
54 }
55}
56
57export class ElectronShell extends Context.Service<
58 ElectronShell,
59 {
60 readonly openExternal: (rawUrl: unknown) => Effect.Effect<boolean>;
61 /** Opens a known System Settings pane by identifier, not by URL. */
62 readonly openSystemSettings: (pane: SystemSettingsPane) => Effect.Effect<boolean>;
63 readonly copyText: (text: string) => Effect.Effect<void>;
64 }
65>()("@t3tools/desktop/electron/ElectronShell") {}
66
67/** @public Service construction is part of the canonical Effect module API. */
68export const make = ElectronShell.of({
69 openExternal: (rawUrl) =>
70 Option.match(parseSafeExternalUrl(rawUrl), {
71 onNone: () => Effect.succeed(false),
72 onSome: (externalUrl) =>
73 Effect.promise(() =>
74 Electron.shell.openExternal(externalUrl).then(
75 () => true,
76 () => false,
77 ),
78 ),
79 }),
80 openSystemSettings: (pane) =>
81 Effect.promise(() =>
82 Electron.shell.openExternal(MAC_PERMISSION_SETTINGS_URLS[pane]).then(
83 () => true,
84 () => false,
85 ),
86 ),
87 copyText: (text) =>
88 Effect.promise(() => Electron.clipboard.writeText(text).catch(() => undefined)),
89});
90
91export const layer = Layer.succeed(ElectronShell, make);