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
17 changes: 2 additions & 15 deletions apps/desktop/src/electron/ElectronShell.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { MAC_PERMISSION_SETTINGS_URLS } from "../permissions/MacPermission.ts";
import {
REMOTE_CAPABLE_EDITOR_IDS,
remoteSchemeForEditor,
Expand All @@ -10,20 +11,6 @@ import * as Option from "effect/Option";

import * as Electron from "electron";

/**
* Deep links to individual System Settings panes. These are app-fixed, not
* renderer-supplied, so they skip `parseSafeExternalUrl` — which exists to keep
* arbitrary link schemes from reaching the OS handler — and open through their
* own path below. The pane rather than the URL crosses the IPC boundary, so a
* renderer can only ask for one of these known destinations.
*
* Full Disk Access uses the post-Ventura `PrivacySecurity.extension` anchor.
*/
const SYSTEM_SETTINGS_URLS: Record<SystemSettingsPane, string> = {
"full-disk-access":
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles",
};

// Remote open-in-editor deep links (`vscode://vscode-remote/ssh-remote+…`,
// `zed://ssh/<host>/<path>`) must reach the OS handler; every other non-web
// scheme stays blocked.
Expand Down Expand Up @@ -99,7 +86,7 @@ export const make = ElectronShell.of({
}),
openSystemSettings: (pane) =>
Effect.promise(() =>
Electron.shell.openExternal(SYSTEM_SETTINGS_URLS[pane]).then(
Electron.shell.openExternal(MAC_PERMISSION_SETTINGS_URLS[pane]).then(
() => true,
() => false,
),
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
getWindowFullscreenState,
openExternal,
openSystemSettings,
checkSystemPermission,
probeRemoteEditors,
pickFolder,
pickProjectFavicon,
Expand Down Expand Up @@ -121,6 +122,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(showContextMenu);
yield* ipc.handle(openExternal);
yield* ipc.handle(openSystemSettings);
yield* ipc.handle(checkSystemPermission);
yield* ipc.handle(probeRemoteEditors);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(downloadUpdate);
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,7 @@ export const PREVIEW_RECORDING_SAVE_CHANNEL = "desktop:preview-recording-save";
export const PREVIEW_RECORDING_FRAME_CHANNEL = "desktop:preview-recording-frame";
export const PREVIEW_STATE_CHANGE_CHANNEL = "desktop:preview-state-change";
export const PREVIEW_POINTER_EVENT_CHANNEL = "desktop:preview-pointer-event";

export const MAC_PERMISSION_HELPER_CHANNEL = "desktop:mac-permission-helper";

export const CHECK_SYSTEM_PERMISSION_CHANNEL = "desktop:check-system-permission";
26 changes: 25 additions & 1 deletion apps/desktop/src/ipc/methods/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import * as ElectronMenu from "../../electron/ElectronMenu.ts";
import * as ElectronShell from "../../electron/ElectronShell.ts";
import * as ElectronTheme from "../../electron/ElectronTheme.ts";
import * as ElectronWindow from "../../electron/ElectronWindow.ts";
import * as Electron from "electron";
import * as MacPermissions from "../../permissions/MacPermissions.ts";
import { safariPermissionCheck } from "../../preview/BrowserImport/SafariPermission.ts";
import * as IpcChannels from "../channels.ts";
import * as DesktopIpc from "../DesktopIpc.ts";
import {
Expand Down Expand Up @@ -305,7 +308,16 @@ export const openSystemSettings = DesktopIpc.makeIpcMethod({
result: Schema.Boolean,
handler: Effect.fn("desktop.ipc.window.openSystemSettings")(function* (pane) {
const shell = yield* ElectronShell.ElectronShell;
return yield* shell.openSystemSettings(pane);
const environment = yield* DesktopEnvironment.DesktopEnvironment;
if (environment.platform !== "darwin") return false;
const owner = Electron.BrowserWindow.getFocusedWindow();
const opened = yield* shell.openSystemSettings(pane);
if (opened && environment.isPackaged) {
const permissions = yield* MacPermissions.MacPermissions;
const isGranted = yield* safariPermissionCheck;
yield* permissions.showHelper(pane, owner, isGranted);
}
return opened;
}),
});

Expand Down Expand Up @@ -379,3 +391,15 @@ export const pickThemeFiles = DesktopIpc.makeIpcMethod({
});
}),
});

export const checkSystemPermission = DesktopIpc.makeIpcMethod({
channel: IpcChannels.CHECK_SYSTEM_PERMISSION_CHANNEL,
payload: SystemSettingsPaneSchema,
result: Schema.Boolean,
handler: Effect.fn("desktop.ipc.window.checkSystemPermission")(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
if (environment.platform !== "darwin") return false;
const check = yield* safariPermissionCheck;
return yield* Effect.promise(check);
}),
});
17 changes: 17 additions & 0 deletions apps/desktop/src/mac-permission-preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ipcRenderer } from "electron";
import { MAC_PERMISSION_HELPER_CHANNEL } from "./ipc/channels.ts";

// This preload belongs only to the static permission panel. No general desktop bridge is exposed.
window.addEventListener("DOMContentLoaded", () => {
const send = (action: "drag" | "finder" | "close") =>
ipcRenderer.send(MAC_PERMISSION_HELPER_CHANNEL, action);
document.getElementById("app")?.addEventListener("dragstart", (event) => {
event.preventDefault();
send("drag");
});
document.getElementById("app")?.addEventListener("click", () => send("finder"));
document.getElementById("close")?.addEventListener("click", () => send("close"));
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") send("close");
});
});
2 changes: 2 additions & 0 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as MacPermissions from "./permissions/MacPermissions.ts";
for (const stream of [process.stdout, process.stderr]) {
stream.on("error", (err: NodeJS.ErrnoException) => {
if (err.code !== "EPIPE") throw err;
Expand Down Expand Up @@ -130,6 +131,7 @@ const electronLayer = Layer.mergeAll(
);

const desktopFoundationLayer = Layer.mergeAll(
MacPermissions.layer,
DesktopState.layer,
DesktopShutdown.layer,
DesktopAppSettings.layer,
Expand Down
15 changes: 15 additions & 0 deletions apps/desktop/src/permissions/MacPermission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const MAC_PERMISSION_SETTINGS_URLS = {
"screen-recording":
"x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture",
accessibility: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",
"full-disk-access":
"x-apple.systempreferences:com.apple.settings.PrivacySecurity.extension?Privacy_AllFiles",
};

export type MacPermission = keyof typeof MAC_PERMISSION_SETTINGS_URLS;

export const MAC_PERMISSION_TITLES: Record<MacPermission, string> = {
"screen-recording": "Screen Recording",
accessibility: "Accessibility",
"full-disk-access": "Full Disk Access",
};
Loading
Loading