b2ae3a62b9
Native SwiftUI menu-bar app (LSUIElement, macOS 13+), generated from project.yml via XcodeGen. Includes: - PermissionsManager (Microphone / Screen Recording / Accessibility) + UI - SparkControlHealth: GET /api/status over self-signed TLS (InsecureTrustDelegate) - AppSettings persistence (host, TLS-skip, output folder, adapter toggles) - Menu-bar panel + Settings, app sandbox & hardened runtime off (LAN tool)
90 lines
2.9 KiB
Swift
90 lines
2.9 KiB
Swift
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"
|
|
}
|
|
}
|
|
}
|
|
}
|