diff --git a/mac/VibeTunnel/Core/Services/SystemPermissionManager.swift b/mac/VibeTunnel/Core/Services/SystemPermissionManager.swift index 398edf71..4ed75ada 100644 --- a/mac/VibeTunnel/Core/Services/SystemPermissionManager.swift +++ b/mac/VibeTunnel/Core/Services/SystemPermissionManager.swift @@ -5,6 +5,10 @@ import Foundation import Observation import OSLog +extension Notification.Name { + static let permissionsUpdated = Notification.Name("sh.vibetunnel.permissionsUpdated") +} + /// Types of system permissions that VibeTunnel requires enum SystemPermission { case appleScript @@ -63,8 +67,14 @@ final class SystemPermissionManager { category: "SystemPermissions" ) + /// Timer for monitoring permission changes + private var monitorTimer: Timer? + + /// Count of views that have registered for monitoring + private var monitorRegistrationCount = 0 + private init() { - // No automatic monitoring - UI components will check when visible + // No automatic monitoring - UI components will register when visible } // MARK: - Public API @@ -127,13 +137,67 @@ final class SystemPermissionManager { } } + // MARK: - Permission Monitoring + + /// Register for permission monitoring (call when a view appears) + func registerForMonitoring() { + monitorRegistrationCount += 1 + logger.debug("Registered for monitoring, count: \(self.monitorRegistrationCount)") + + if monitorRegistrationCount == 1 { + // First registration, start monitoring + startMonitoring() + } + } + + /// Unregister from permission monitoring (call when a view disappears) + func unregisterFromMonitoring() { + monitorRegistrationCount = max(0, monitorRegistrationCount - 1) + logger.debug("Unregistered from monitoring, count: \(self.monitorRegistrationCount)") + + if monitorRegistrationCount == 0 { + // No more registrations, stop monitoring + stopMonitoring() + } + } + + private func startMonitoring() { + logger.info("Starting permission monitoring") + + // Initial check + Task { + await checkAllPermissions() + } + + // Start timer for periodic checks + monitorTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + guard let self else { return } + Task { @MainActor in + await self.checkAllPermissions() + } + } + } + + private func stopMonitoring() { + logger.info("Stopping permission monitoring") + monitorTimer?.invalidate() + monitorTimer = nil + } + // MARK: - Permission Checking func checkAllPermissions() async { + let oldPermissions = permissions + // Check each permission type permissions[.appleScript] = await checkAppleScriptPermission() permissions[.screenRecording] = checkScreenRecordingPermission() permissions[.accessibility] = checkAccessibilityPermission() + + // Post notification if any permissions changed + if oldPermissions != permissions { + NotificationCenter.default.post(name: .permissionsUpdated, object: nil) + } } // MARK: - AppleScript Permission diff --git a/mac/VibeTunnel/Presentation/Views/Settings/GeneralSettingsView.swift b/mac/VibeTunnel/Presentation/Views/Settings/GeneralSettingsView.swift index 96414d95..1bbb821a 100644 --- a/mac/VibeTunnel/Presentation/Views/Settings/GeneralSettingsView.swift +++ b/mac/VibeTunnel/Presentation/Views/Settings/GeneralSettingsView.swift @@ -130,14 +130,20 @@ private struct PermissionsSection: View { @State private var permissionManager = SystemPermissionManager.shared @State private var permissionUpdateTrigger = 0 + // IMPORTANT: These computed properties ensure the UI always shows current permission state. + // The permissionUpdateTrigger dependency forces SwiftUI to re-evaluate these properties + // when permissions change. Without this, the UI would not update when permissions are + // granted in System Settings while this view is visible. + // + // We use computed properties instead of @State to avoid UI flashing - the initial + // permission check in .task happens before the first render, ensuring correct state + // from the start. private var hasAppleScriptPermission: Bool { - // This will cause a re-read whenever permissionUpdateTrigger changes _ = permissionUpdateTrigger return permissionManager.hasPermission(.appleScript) } private var hasAccessibilityPermission: Bool { - // This will cause a re-read whenever permissionUpdateTrigger changes _ = permissionUpdateTrigger return permissionManager.hasPermission(.accessibility) } @@ -232,13 +238,19 @@ private struct PermissionsSection: View { .multilineTextAlignment(.center) } } - .onReceive(Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()) { _ in - // Force a re-render to check permissions - permissionUpdateTrigger += 1 - } .task { - // Check all permissions + // Check permissions before first render to avoid UI flashing await permissionManager.checkAllPermissions() + + // Register for continuous monitoring + permissionManager.registerForMonitoring() + } + .onDisappear { + permissionManager.unregisterFromMonitoring() + } + .onReceive(NotificationCenter.default.publisher(for: .permissionsUpdated)) { _ in + // Increment trigger to force computed property re-evaluation + permissionUpdateTrigger += 1 } } } diff --git a/mac/VibeTunnel/Presentation/Views/Welcome/RequestPermissionsPageView.swift b/mac/VibeTunnel/Presentation/Views/Welcome/RequestPermissionsPageView.swift index 3c294b6e..978595b3 100644 --- a/mac/VibeTunnel/Presentation/Views/Welcome/RequestPermissionsPageView.swift +++ b/mac/VibeTunnel/Presentation/Views/Welcome/RequestPermissionsPageView.swift @@ -22,14 +22,20 @@ struct RequestPermissionsPageView: View { @State private var permissionManager = SystemPermissionManager.shared @State private var permissionUpdateTrigger = 0 + // IMPORTANT: These computed properties ensure the UI always shows current permission state. + // The permissionUpdateTrigger dependency forces SwiftUI to re-evaluate these properties + // when permissions change. Without this, the UI would not update when permissions are + // granted in System Settings while this view is visible. + // + // We use computed properties instead of @State to avoid UI flashing - the initial + // permission check in .task happens before the first render, ensuring correct state + // from the start. private var hasAppleScriptPermission: Bool { - // This will cause a re-read whenever permissionUpdateTrigger changes _ = permissionUpdateTrigger return permissionManager.hasPermission(.appleScript) } private var hasAccessibilityPermission: Bool { - // This will cause a re-read whenever permissionUpdateTrigger changes _ = permissionUpdateTrigger return permissionManager.hasPermission(.accessibility) } @@ -102,13 +108,19 @@ struct RequestPermissionsPageView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() - .onReceive(Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()) { _ in - // Force a re-render to check permissions - permissionUpdateTrigger += 1 - } .task { - // Check all permissions + // Check permissions before first render to avoid UI flashing await permissionManager.checkAllPermissions() + + // Register for continuous monitoring + permissionManager.registerForMonitoring() + } + .onDisappear { + permissionManager.unregisterFromMonitoring() + } + .onReceive(NotificationCenter.default.publisher(for: .permissionsUpdated)) { _ in + // Increment trigger to force computed property re-evaluation + permissionUpdateTrigger += 1 } } }