import AVFoundation import CoreGraphics import ApplicationServices import AppKit import Combine enum PermissionState { case granted case denied case notDetermined } /// Tracks and requests the three TCC permissions the app needs. /// /// - Microphone: AVFoundation authorization (has a real "not determined" state). /// - Screen Recording: CoreGraphics preflight/request (binary granted/denied). /// - Accessibility: AXIsProcessTrusted (binary granted/denied). @MainActor final class PermissionsManager: ObservableObject { @Published private(set) var microphone: PermissionState = .notDetermined @Published private(set) var screenRecording: PermissionState = .notDetermined @Published private(set) var accessibility: PermissionState = .notDetermined init() { refresh() } func refresh() { microphone = Self.microphoneState() screenRecording = CGPreflightScreenCaptureAccess() ? .granted : .denied accessibility = AXIsProcessTrusted() ? .granted : .denied } // MARK: - Requests func requestMicrophone() { AVCaptureDevice.requestAccess(for: .audio) { _ in Task { @MainActor in self.refresh() } } } /// Triggers the system Screen Recording prompt on first call. The user must /// still toggle the app on in System Settings; `refresh()` reflects it after. func requestScreenRecording() { _ = CGRequestScreenCaptureAccess() refresh() } /// Shows the Accessibility trust prompt (deep-links to the right pane). func requestAccessibility() { // Literal is the value of `kAXTrustedCheckOptionPrompt`; used directly to // stay robust across SDK import shapes of that constant. let options = ["AXTrustedCheckOptionPrompt": true] as CFDictionary _ = AXIsProcessTrustedWithOptions(options) refresh() } func openSettings(_ pane: SettingsPane) { guard let url = URL(string: pane.urlString) else { return } NSWorkspace.shared.open(url) } // MARK: - Helpers private static func microphoneState() -> PermissionState { switch AVCaptureDevice.authorizationStatus(for: .audio) { case .authorized: return .granted case .denied, .restricted: return .denied case .notDetermined: return .notDetermined @unknown default: return .notDetermined } } enum SettingsPane { case microphone case screenRecording case accessibility var urlString: String { let root = "x-apple.systempreferences:com.apple.preference.security?" switch self { case .microphone: return root + "Privacy_Microphone" case .screenRecording: return root + "Privacy_ScreenCapture" case .accessibility: return root + "Privacy_Accessibility" } } } }