diff --git a/dhavnii/App/AppState.swift b/dhavnii/App/AppState.swift index 4b15d55..025411e 100644 --- a/dhavnii/App/AppState.swift +++ b/dhavnii/App/AppState.swift @@ -21,11 +21,20 @@ internal enum RecordingState: Equatable { @MainActor @Observable internal class AppState { + internal static let autoPasteEnabledDefaultsKey = "autoPasteEnabled" + internal var recordingState: RecordingState = .idle internal var lastTranscription: String = "" internal var hasMicrophonePermission: Bool = false internal var hasAccessibilityPermission: Bool = false internal var hasCompletedOnboarding: Bool = false + internal var autoPasteEnabled = UserDefaults.standard.object( + forKey: AppState.autoPasteEnabledDefaultsKey + ) as? Bool ?? true { + didSet { + UserDefaults.standard.set(autoPasteEnabled, forKey: Self.autoPasteEnabledDefaultsKey) + } + } /// Check if all required permissions are granted internal var hasAllPermissions: Bool { diff --git a/dhavnii/Features/Clipboard/ClipboardManager.swift b/dhavnii/Features/Clipboard/ClipboardManager.swift index 33b8541..73801e8 100644 --- a/dhavnii/Features/Clipboard/ClipboardManager.swift +++ b/dhavnii/Features/Clipboard/ClipboardManager.swift @@ -66,17 +66,27 @@ class ClipboardManager { func copyAndPasteIfPossible(_ text: String) { // Always copy to clipboard (mandatory) copyToClipboard(text) - - if AXIsProcessTrusted() { - // Ensure OpenWispher is not the active app (stealing focus) - if NSWorkspace.shared.frontmostApplication?.bundleIdentifier == Bundle.main.bundleIdentifier { - NSApp.hide(nil) - } - - // Delay to allow focus to settle/switch back - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in - self?.simulatePaste() - } + + let hasAccessibilityPermission = AXIsProcessTrusted() + + let frontmostBundleIdentifier = NSWorkspace.shared.frontmostApplication?.bundleIdentifier ?? "unknown" + print( + "📋 Auto-paste decision: accessibility=\(hasAccessibilityPermission), frontmostApp=\(frontmostBundleIdentifier)" + ) + + guard hasAccessibilityPermission else { + print("⚠️ Auto-paste skipped: accessibility permission unavailable") + return + } + + // Ensure OpenWispher is not the active app (stealing focus) + if NSWorkspace.shared.frontmostApplication?.bundleIdentifier == Bundle.main.bundleIdentifier { + NSApp.hide(nil) + } + + // Delay to allow focus to settle/switch back + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { [weak self] in + self?.simulatePaste() } } } diff --git a/dhavnii/Features/Permissions/PermissionManager.swift b/dhavnii/Features/Permissions/PermissionManager.swift index 98f5137..6b27d4d 100644 --- a/dhavnii/Features/Permissions/PermissionManager.swift +++ b/dhavnii/Features/Permissions/PermissionManager.swift @@ -240,6 +240,14 @@ internal class PermissionManager { } } } + + internal func accessibilityPermissionState(for reason: String) -> Bool { + let trusted = hasAccessibilityPermission + print( + "🔓 Accessibility permission queried for \(reason): cached=\(trusted), lastKnown=\(lastAccessibilityState), monitoring=\(isMonitoring)" + ) + return trusted + } /// Verify accessibility permission - use only the standard reliable API private func verifyAccessibilityPermission() -> Bool { diff --git a/dhavnii/Features/Settings/SettingsView.swift b/dhavnii/Features/Settings/SettingsView.swift index 9d48a30..29d8c74 100644 --- a/dhavnii/Features/Settings/SettingsView.swift +++ b/dhavnii/Features/Settings/SettingsView.swift @@ -1216,7 +1216,7 @@ private struct FallbackSettingsCard: View { private struct GeneralSettingsView: View { @Binding var showingResetAlert: Bool - var appState: AppState + @Bindable var appState: AppState var hotkeyManager: HotkeyManager? @AppStorage("autoLaunchEnabled") private var autoLaunchEnabled = false @@ -1243,6 +1243,17 @@ private struct GeneralSettingsView: View { } } + SettingsGroup(title: "Behavior") { + SettingsRow( + icon: "square.and.arrow.down.on.square", title: "Auto-paste to focused field", + subtitle: "Paste transcriptions automatically after copying them" + ) { + Toggle("", isOn: $appState.autoPasteEnabled) + .toggleStyle(.switch) + .controlSize(.small) + } + } + // Hotkey SettingsGroup(title: "Hotkey") { SettingsRow( @@ -1366,6 +1377,7 @@ private struct GeneralSettingsView: View { private func resetApp() { UserDefaults.standard.removeObject(forKey: "hasCompletedOnboarding") UserDefaults.standard.removeObject(forKey: "autoLaunchEnabled") + UserDefaults.standard.removeObject(forKey: AppState.autoPasteEnabledDefaultsKey) UserDefaults.standard.removeObject(forKey: "selectedTranscriptionProvider") UserDefaults.standard.removeObject(forKey: "fallbackTranscriptionProvider") UserDefaults.standard.removeObject(forKey: "transcriptionTimeoutSeconds") @@ -1383,6 +1395,7 @@ private struct GeneralSettingsView: View { appState.hasCompletedOnboarding = false appState.lastTranscription = "" appState.recordingState = .idle + appState.autoPasteEnabled = true NSApplication.shared.terminate(nil) } diff --git a/dhavnii/Features/Transcription/TranscriptionService.swift b/dhavnii/Features/Transcription/TranscriptionService.swift index 52f1b1b..3787c1a 100644 --- a/dhavnii/Features/Transcription/TranscriptionService.swift +++ b/dhavnii/Features/Transcription/TranscriptionService.swift @@ -13,6 +13,7 @@ import Foundation internal class TranscriptionService { private let audioRecorder = AudioRecorder() private let clipboardManager = ClipboardManager() + private let permissionManager: PermissionManager private var groqClient = GroqAPIClient() private var elevenLabsClient = ElevenLabsAPIClient() @@ -30,8 +31,13 @@ internal class TranscriptionService { internal static let transcriptionSavedNotification = Notification.Name( "OpenWispher.TranscriptionSaved") - internal init(appState: AppState, selectedProvider: TranscriptionProviderType = .groq) { + internal init( + appState: AppState, + permissionManager: PermissionManager, + selectedProvider: TranscriptionProviderType = .groq + ) { self.appState = appState + self.permissionManager = permissionManager self.selectedProvider = selectedProvider } @@ -218,7 +224,11 @@ internal class TranscriptionService { return } - clipboardManager.copyAndPasteIfPossible(transcription) + if appState.autoPasteEnabled { + clipboardManager.copyAndPasteIfPossible(transcription) + } else { + clipboardManager.copyToClipboard(transcription) + } historyManager?.saveTranscription(text: transcription, provider: provider) diff --git a/openwispher/openwispherApp.swift b/openwispher/openwispherApp.swift index a88baa0..c217c8a 100644 --- a/openwispher/openwispherApp.swift +++ b/openwispher/openwispherApp.swift @@ -203,7 +203,11 @@ private struct AppContentView: View { savedProviderRaw.flatMap { TranscriptionProviderType(rawValue: $0) } ?? .groq // Initialize transcription service with selected provider - let service = TranscriptionService(appState: appState, selectedProvider: selectedProvider) + let service = TranscriptionService( + appState: appState, + permissionManager: permissionManager, + selectedProvider: selectedProvider + ) service.historyManager = manager transcriptionService = service