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
20 changes: 8 additions & 12 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,6 @@ interface Window {
openSourceSelector: () => Promise<{
opened: boolean;
reason?: string;
access?: {
success: boolean;
granted: boolean;
status: string;
error?: string;
};
}>;
openNotes: () => Promise<{
opened: boolean;
Expand Down Expand Up @@ -77,12 +71,14 @@ interface Window {
status: string;
error?: string;
}>;
requestScreenAccess: () => Promise<{
success: boolean;
granted: boolean;
status: string;
error?: string;
}>;
/** macOS privacy permissions; see electron/permissions/macPermissions.ts. */
permissions: {
get: () => Promise<import("./permissions/macPermissions").PermissionsSnapshot>;
request: (kind: import("./permissions/macPermissions").PermissionKind) => Promise<void>;
openSettings: (kind: import("./permissions/macPermissions").PermissionKind) => Promise<void>;
relaunch: () => Promise<void>;
close: () => Promise<void>;
};
requestNativeMacCursorAccess: () => Promise<{
success: boolean;
granted: boolean;
Expand Down
104 changes: 16 additions & 88 deletions electron/ipc/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
import { findPipeWireCursorHelperPath } from "../native-bridge/cursor/recording/pipeWireCursorRecordingSession";
import type { CursorRecordingSession } from "../native-bridge/cursor/recording/session";
import { toHelperRect } from "../native-bridge/helperCoordinates";
import { getMacPermissions, showPermissionsWindow } from "../permissions";
import { scoreDeviceNameMatch } from "../recording/deviceNameMatching";
import {
describeSalvagedTake,
Expand Down Expand Up @@ -1864,43 +1865,6 @@ export function registerIpcHandlers(
const sameSelectedSource = (left: SelectedSource | null, right: SelectedSource | null) =>
left?.id === right?.id && left?.name === right?.name && left?.display_id === right?.display_id;

async function requestScreenAccess() {
if (process.platform !== "darwin") {
return { success: true, granted: true, status: "granted" };
}

try {
const status = systemPreferences.getMediaAccessStatus("screen");
if (status === "granted") {
return { success: true, granted: true, status };
}

// Screen recording has no askForMediaAccess equivalent, so trigger the
// TCC prompt without opening OpenScreen's source selector above it.
if (status === "not-determined") {
const mainWin = getMainWindow();
if (mainWin && !mainWin.isDestroyed()) {
if (!mainWin.isVisible()) {
mainWin.show();
}
mainWin.focus();
}
app.focus({ steal: true });
desktopCapturer
.getSources({ types: ["screen"], thumbnailSize: { width: 1, height: 1 } })
.catch(() => {
// Permission probing failure is reported by the explicit status check below.
});
return { success: true, granted: false, status: "not-determined" };
}

return { success: true, granted: false, status };
} catch (error) {
console.error("Failed to request screen access:", error);
return { success: false, granted: false, status: "unknown", error: String(error) };
}
}

ipcMain.handle("get-sources", async (_, opts) => {
// desktopCapturer.getSources can never settle where the GL stack cannot be
// reached -- a container, a CI runner, a host whose ANGLE fails to
Expand Down Expand Up @@ -2129,10 +2093,6 @@ export function registerIpcHandlers(
}
});

ipcMain.handle("request-screen-access", async () => {
return requestScreenAccess();
});

ipcMain.handle("request-native-mac-cursor-access", async () => {
const access = await requestMacCursorAccessibilityAccess();

Expand All @@ -2154,26 +2114,12 @@ export function registerIpcHandlers(
return access;
}

const mainWin = getMainWindow();
const detail =
"Allow OpenScreen under System Settings → Privacy & Security → Accessibility, then press record again to start the countdown.";
const messageOptions = {
type: "warning",
buttons: ["Open Accessibility Settings", "Cancel"],
defaultId: 0,
cancelId: 1,
message: "Accessibility access is required for the editable cursor",
detail,
} satisfies Electron.MessageBoxOptions;
const result =
mainWin && !mainWin.isDestroyed()
? await dialog.showMessageBox(mainWin, messageOptions)
: await dialog.showMessageBox(messageOptions);
if (result.response === 0) {
await shell.openExternal(
"x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility",
);
}
// The helper that answered has just raised macOS' own Accessibility prompt on
// its way up, so the window must offer System Settings, not a second prompt. It
// explains what the grant is for and tracks it live, where a message box could
// only say "go to System Settings" in English.
getMacPermissions().noteRequested("accessibility");
showPermissionsWindow();
}

return access;
Expand All @@ -2193,34 +2139,16 @@ export function registerIpcHandlers(
return { opened: false, reason: "portal-owns-selection" };
}

const access = await requestScreenAccess();
if (!access.granted) {
if (process.platform === "darwin" && access.status !== "not-determined") {
const mainWin = getMainWindow();
const messageOptions = {
type: "warning",
buttons: ["Open System Settings", "Cancel"],
defaultId: 0,
cancelId: 1,
message: "Screen Recording permission is required",
detail:
"Allow OpenScreen in macOS System Settings, then come back and choose a screen or window.",
} satisfies Electron.MessageBoxOptions;
const result =
mainWin && !mainWin.isDestroyed()
? await dialog.showMessageBox(mainWin, messageOptions)
: await dialog.showMessageBox(messageOptions);
if (result.response === 0) {
await shell.openExternal(
"x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture",
);
}
// Chromium's picker can only list sources once THIS process can capture, which on
// macOS means granted, and granted before launch: the app's own read is cached for
// the life of the process. Anything short of that belongs in the permissions
// window, which says what is missing and offers the relaunch when that is all.
if (process.platform === "darwin") {
const permissions = await getMacPermissions().read();
if (permissions.screen !== "granted" || permissions.screenRequiresRelaunch) {
showPermissionsWindow();
return { opened: false, reason: "screen-access-required" };
}
return {
opened: false,
reason: "screen-access-required",
access,
};
}

const sourceSelectorWin = getSourceSelectorWindow();
Expand Down
45 changes: 25 additions & 20 deletions electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
net,
session,
shell,
systemPreferences,
Tray,
} from "electron";
import { ShortcutBinding } from "../src/lib/shortcuts";
Expand Down Expand Up @@ -60,6 +59,11 @@ import {
registerIpcHandlers,
} from "./ipc/handlers";
import { installMainProcessErrorGuards } from "./main-process-errors";
import {
registerPermissionsIpc,
showPermissionsWindow,
showPermissionsWindowIfNeeded,
} from "./permissions";
import { registerSttIpc, shutdownStt } from "./stt";
import { checkLatestRelease } from "./update-checker";
import { loadUpdateMode, saveUpdateMode } from "./update-settings";
Expand Down Expand Up @@ -231,6 +235,10 @@ function setupApplicationMenu() {
role: "about",
label: mainT("common", "actions.about") || "About OpenScreen",
},
{
label: mainT("common", "actions.permissions") || "Permissions…",
click: showPermissionsWindow,
},
{ type: "separator" as const },
{
label: mainT("common", "actions.saveDiagnostics") || "Save Diagnostics",
Expand Down Expand Up @@ -923,6 +931,14 @@ function updateTrayMenu(recording: boolean = false) {
label: mainT("common", "actions.saveDiagnostics") || "Save Diagnostics",
click: runSaveDiagnostics,
},
...(isMac
? [
{
label: mainT("common", "actions.permissions") || "Permissions…",
click: showPermissionsWindow,
},
]
: []),
{ type: "separator" as const },
{
label: mainT("common", "actions.quit") || "Quit",
Expand Down Expand Up @@ -1182,25 +1198,11 @@ appReady?.then(async () => {
});
}

// Request mic permission now. Screen Recording is requested lazily from the
// source-picker action so its prompt isn't hidden behind the selector window.
//
// NOT awaited, on purpose. `askForMediaAccess` resolves only once the user
// answers the modal TCC prompt, and `createWindow()` is 70 lines below this in
// the same async block — so on a Mac where the microphone is still
// `not-determined` (every first run, and every fresh dev machine) the app
// showed a permission dialog with NO window behind it and created the HUD only
// after it was dismissed. Nothing between here and `createWindow()` needs the
// answer: the recorder re-checks the status when the user actually arms the mic.
if (process.platform === "darwin") {
const micStatus = systemPreferences.getMediaAccessStatus("microphone");
if (micStatus !== "granted") {
systemPreferences
.askForMediaAccess("microphone")
.then((granted) => console.info(`[permissions] microphone granted=${granted}`))
.catch((error) => console.warn("[permissions] microphone request failed:", error));
}
}
// No permission is requested at launch. Screen Recording, Accessibility, the microphone
// and the camera are all gathered in the permissions window (electron/permissions),
// opened below when recording cannot work yet; the microphone and camera are also
// requested at the moment a take first uses them.
registerPermissionsIpc();

ipcMain.on("hud-overlay-close", () => {
app.quit();
Expand Down Expand Up @@ -1332,4 +1334,7 @@ appReady?.then(async () => {
}

createWindow();
void showPermissionsWindowIfNeeded().catch((error) =>
console.warn("[permissions] could not read the permissions at launch:", error),
);
});
Loading
Loading