mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-03-25 09:25:50 +00:00
156 lines
5.3 KiB
Swift
156 lines
5.3 KiB
Swift
import AppKit
|
|
import ApplicationServices
|
|
import Foundation
|
|
import OSLog
|
|
|
|
/// Utilities for managing macOS accessibility permissions.
|
|
/// Provides convenient methods for checking and requesting accessibility permissions.
|
|
public enum AXPermissions {
|
|
private static let logger = Logger(
|
|
subsystem: BundleIdentifiers.loggerSubsystem,
|
|
category: "AXPermissions"
|
|
)
|
|
|
|
/// Checks if the app currently has accessibility permissions without prompting.
|
|
public static var hasPermissions: Bool {
|
|
AXIsProcessTrusted()
|
|
}
|
|
|
|
/// Requests accessibility permissions, showing the system prompt if needed.
|
|
/// - Returns: `true` if permissions are granted, `false` otherwise
|
|
@MainActor
|
|
public static func requestPermissions() -> Bool {
|
|
// Skip permission dialog in test environment
|
|
if isTestEnvironment {
|
|
logger.debug("Skipping permission request in test environment")
|
|
return false
|
|
}
|
|
|
|
// Use direct API without options to avoid concurrency issues
|
|
let trusted = AXIsProcessTrusted()
|
|
if !trusted {
|
|
// Open accessibility preferences to prompt user
|
|
openAccessibilityPreferences()
|
|
}
|
|
|
|
logger.info("Accessibility permissions checked, trusted: \(trusted)")
|
|
return trusted
|
|
}
|
|
|
|
/// Determines if the app is running in a test environment
|
|
private static var isTestEnvironment: Bool {
|
|
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil ||
|
|
ProcessInfo.processInfo.arguments.contains("--test-mode") ||
|
|
NSClassFromString("XCTest") != nil
|
|
}
|
|
|
|
/// Determines if the app is running in a sandboxed environment
|
|
public static var isSandboxed: Bool {
|
|
ProcessInfo.processInfo.environment["APP_SANDBOX_CONTAINER_ID"] != nil
|
|
}
|
|
|
|
/// Opens System Preferences to the Security & Privacy > Accessibility pane
|
|
@MainActor
|
|
public static func openAccessibilityPreferences() {
|
|
logger.info("Opening Accessibility preferences")
|
|
|
|
if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") {
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
}
|
|
|
|
/// Monitors accessibility permission changes asynchronously
|
|
/// - Parameter interval: The polling interval in seconds (default: 1.0)
|
|
/// - Returns: An AsyncStream that emits permission status changes
|
|
public static func permissionChanges(interval: TimeInterval = 1.0) -> AsyncStream<Bool> {
|
|
AsyncStream { continuation in
|
|
let initialState = hasPermissions
|
|
continuation.yield(initialState)
|
|
|
|
// Timer holder to avoid capture issues
|
|
final class TimerHolder: @unchecked Sendable {
|
|
var timer: Timer?
|
|
var lastState: Bool
|
|
|
|
init(initialState: Bool) {
|
|
self.lastState = initialState
|
|
}
|
|
|
|
deinit {
|
|
timer?.invalidate()
|
|
}
|
|
}
|
|
|
|
let holder = TimerHolder(initialState: initialState)
|
|
|
|
holder.timer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
|
|
let currentState = hasPermissions
|
|
if currentState != holder.lastState {
|
|
holder.lastState = currentState
|
|
continuation.yield(currentState)
|
|
logger.info("Accessibility permission changed to: \(currentState)")
|
|
}
|
|
}
|
|
|
|
continuation.onTermination = { @Sendable _ in
|
|
DispatchQueue.main.async {
|
|
holder.timer?.invalidate()
|
|
holder.timer = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Requests permissions asynchronously
|
|
/// - Returns: `true` if permissions are granted, `false` otherwise
|
|
@MainActor
|
|
public static func requestPermissionsAsync() async -> Bool {
|
|
requestPermissions()
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience Extensions
|
|
|
|
extension AXPermissions {
|
|
/// Ensures accessibility permissions are granted, prompting if necessary
|
|
/// - Parameter onDenied: Closure to execute if permissions are denied
|
|
/// - Returns: `true` if permissions are granted, `false` otherwise
|
|
@MainActor
|
|
public static func ensurePermissions(onDenied: (() -> Void)? = nil) -> Bool {
|
|
if hasPermissions {
|
|
return true
|
|
}
|
|
|
|
let granted = requestPermissions()
|
|
if !granted {
|
|
logger.warning("Accessibility permissions denied")
|
|
onDenied?()
|
|
}
|
|
|
|
return granted
|
|
}
|
|
|
|
/// Checks permissions and shows an alert if not granted
|
|
@MainActor
|
|
public static func checkPermissionsWithAlert() -> Bool {
|
|
if hasPermissions {
|
|
return true
|
|
}
|
|
|
|
let alert = NSAlert()
|
|
alert.messageText = "Accessibility Permission Required"
|
|
alert
|
|
.informativeText =
|
|
"VibeTunnel needs accessibility permissions to interact with terminal windows. Please grant access in System Preferences."
|
|
alert.alertStyle = .warning
|
|
alert.addButton(withTitle: "Open System Preferences")
|
|
alert.addButton(withTitle: "Cancel")
|
|
|
|
let response = alert.runModal()
|
|
if response == .alertFirstButtonReturn {
|
|
openAccessibilityPreferences()
|
|
}
|
|
|
|
return false
|
|
}
|
|
}
|