diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index bf0bc97..b13dd75 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -718,6 +718,32 @@ export function registerIpcHandlers( } }); + ipcMain.handle("request-screen-access", async () => { + 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 — the TCC prompt + // is triggered by desktopCapturer.getSources(). Fire it and return so + // the renderer can re-check status after the user responds. + if (status === "not-determined") { + desktopCapturer.getSources({ types: ["screen"] }).catch(() => {}); + 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) }; + } + }); + // macOS Accessibility prompt for global click capture. First call shows the // system dialog; the user has to toggle the app in System Settings (no // programmatic grant exists for Accessibility). diff --git a/electron/main.ts b/electron/main.ts index cf20b5a..4e443a9 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url"; import { app, BrowserWindow, + desktopCapturer, ipcMain, Menu, nativeImage, @@ -476,12 +477,21 @@ app.whenReady().then(async () => { callback(allowed.includes(permission)); }); - // Request microphone permission from macOS + // Request microphone and screen recording permissions from macOS if (process.platform === "darwin") { const micStatus = systemPreferences.getMediaAccessStatus("microphone"); if (micStatus !== "granted") { await systemPreferences.askForMediaAccess("microphone"); } + + // Screen recording has no askForMediaAccess equivalent — the TCC prompt is + // triggered by the first desktopCapturer.getSources() call. Firing it here + // at startup settles the permission state early and prevents repeated prompts + // driven by later getSources() calls (fixes repeated permission dialog). + const screenStatus = systemPreferences.getMediaAccessStatus("screen"); + if (screenStatus === "not-determined") { + desktopCapturer.getSources({ types: ["screen"] }).catch(() => {}); + } } // Listen for HUD overlay quit event (macOS only)