Phase 0: menu-bar scaffold, permissions, backend health check

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)
This commit is contained in:
Grant Gilliam
2026-06-05 19:33:53 -05:00
commit b2ae3a62b9
19 changed files with 1448 additions and 0 deletions
@@ -0,0 +1,89 @@
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"
}
}
}
}