diff --git a/Sources/AudioRecorder.swift b/Sources/AudioRecorder.swift index b480969..ddee7c6 100644 --- a/Sources/AudioRecorder.swift +++ b/Sources/AudioRecorder.swift @@ -104,6 +104,7 @@ final class AudioRecorder: NSObject, ObservableObject { } func reset() { + Logger.debugLog("=== RESET CALLED (this blocks notifications) ===", log: Logger.audio) resetPending = true stop() wait() @@ -112,6 +113,7 @@ final class AudioRecorder: NSObject, ObservableObject { recorder = nil isRecording = false resetPending = false + Logger.log("Reset complete, resetPending now false", log: Logger.audio) } deinit { @@ -135,14 +137,17 @@ extension AudioRecorder: AVAudioRecorderDelegate { func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) { isRecording = false - Logger.log("Recording finished: \(flag)", log: Logger.audio) + Logger.log("Recording finished: \(flag), resetPending: \(resetPending)", log: Logger.audio) if !resetPending { if !flag { NotificationCenter.default.post(name: .recordingError, object: "Recording failed") } else { + Logger.log("Posting didFinishRecording notification with URL: \(outputURL?.path ?? "nil")", log: Logger.audio) NotificationCenter.default.post(name: .didFinishRecording, object: outputURL) } + } else { + Logger.log("NOTIFICATION BLOCKED: resetPending is true", log: Logger.audio, type: .error) } } } diff --git a/Sources/GenericHelper.swift b/Sources/GenericHelper.swift index 37efe09..ec0b20d 100644 --- a/Sources/GenericHelper.swift +++ b/Sources/GenericHelper.swift @@ -439,36 +439,99 @@ enum GenericHelper { } static func paste(text: String) -> Bool { + Logger.debugLog("=== PASTE FUNCTION CALLED ===", log: Logger.general) if GenericHelper.logSensitiveData() { Logger.log("Auto pasting: \(text)", log: Logger.general) } // Do not paste if WhisperClip is the active app if isWhisperClipActive() { - if GenericHelper.logSensitiveData() { - Logger.log("Paste skipped: WhisperClip is frontmost app", log: Logger.general) - } + Logger.log("Paste skipped: WhisperClip is frontmost app", log: Logger.general) return false } - // ⌘V in whichever app is active - let script = #""" - tell application "System Events" - key code 9 using {command down} - end tell - """# - var err: NSDictionary? - NSAppleScript(source: script)?.executeAndReturnError(&err) + // Get current frontmost app + if let frontApp = NSWorkspace.shared.frontmostApplication { + Logger.log("Current frontmost app: \(frontApp.localizedName ?? "unknown") [\(frontApp.bundleIdentifier ?? "unknown")]", log: Logger.general) + } - if let err { - Logger.log("AppleScript error: \(err.description)", log: Logger.general, type: .error) - return false - } else { - if GenericHelper.logSensitiveData() { - Logger.log("Auto pasted: \(text)", log: Logger.general) + // Wait for user to switch to target app (max 3 seconds) + Logger.log("Waiting for user to switch to target app...", log: Logger.general) + var waitCount = 0 + while isWhisperClipActive() && waitCount < 30 { + Thread.sleep(forTimeInterval: 0.1) + waitCount += 1 + } + Logger.log("Wait complete. Waited \(waitCount * 100)ms", log: Logger.general) + + // Additional small delay to ensure app is ready + Thread.sleep(forTimeInterval: 0.2) + + // Get current app bundle identifier + let frontApp = NSWorkspace.shared.frontmostApplication + let bundleId = frontApp?.bundleIdentifier ?? "" + + // Special handling for iTerm2 + if bundleId == "com.googlecode.iterm2" { + Logger.log("Detected iTerm2, using AppleScript injection", log: Logger.general) + let script = """ + tell application "iTerm2" + tell current session of current window + write text "\(text.replacingOccurrences(of: "\"", with: "\\\""))" + end tell + end tell + """ + var err: NSDictionary? + NSAppleScript(source: script)?.executeAndReturnError(&err) + + if let err = err { + Logger.log("iTerm2 AppleScript error: \(err.description)", log: Logger.general, type: .error) + } else { + Logger.log("Successfully sent text to iTerm2 via AppleScript", log: Logger.general) + return true } - return true } + + // Try to use Accessibility API to insert text directly + Logger.log("Attempting to paste using Accessibility API...", log: Logger.general) + + let systemWideElement = AXUIElementCreateSystemWide() + var focusedElement: CFTypeRef? + let result = AXUIElementCopyAttributeValue(systemWideElement, kAXFocusedUIElementAttribute as CFString, &focusedElement) + + if result == .success, let element = focusedElement { + Logger.log("Found focused element, attempting to insert text", log: Logger.general) + let axElement = element as! AXUIElement + + // Try to set the value directly + let valueResult = AXUIElementSetAttributeValue(axElement, kAXValueAttribute as CFString, text as CFTypeRef) + + if valueResult == .success { + Logger.log("Successfully inserted text via Accessibility API", log: Logger.general) + return true + } else { + Logger.log("Failed to set value via Accessibility (error: \(valueResult.rawValue)), falling back to keyboard events", log: Logger.general) + } + } else { + Logger.log("No focused element found (error: \(result.rawValue)), using keyboard events", log: Logger.general) + } + + // Fallback: Use CGEvent to simulate Cmd+V + Logger.log("Using CGEvent fallback to simulate Cmd+V...", log: Logger.general) + let source = CGEventSource(stateID: .combinedSessionState) + + let vKeyDown = CGEvent(keyboardEventSource: source, virtualKey: 9, keyDown: true) + let vKeyUp = CGEvent(keyboardEventSource: source, virtualKey: 9, keyDown: false) + + vKeyDown?.flags = .maskCommand + vKeyUp?.flags = .maskCommand + + vKeyDown?.post(tap: .cghidEventTap) + Thread.sleep(forTimeInterval: 0.05) + vKeyUp?.post(tap: .cghidEventTap) + + Logger.log("CGEvent paste complete", log: Logger.general) + return true } static func sendEnter() -> Bool { diff --git a/Sources/Logger.swift b/Sources/Logger.swift index a3424e0..cb4f58b 100644 --- a/Sources/Logger.swift +++ b/Sources/Logger.swift @@ -36,6 +36,27 @@ public enum Logger { } + /// Log a debug message (only if debug logging is enabled in settings) + /// - Parameters: + /// - message: The message to log + /// - log: The OSLog instance to use (defaults to general) + /// - type: The type of log message (defaults to .debug) + /// - file: The file name where the log was called (automatically captured) + /// - function: The function name where the log was called (automatically captured) + /// - line: The line number where the log was called (automatically captured) + public static func debugLog(_ message: String, + log: OSLog = Logger.general, + type: OSLogType = .debug, + file: String = #file, + function: String = #function, + line: Int = #line) { + // Only log if debug logging is enabled + guard SettingsStore.shared.debugLogging else { return } + + // Use the regular log function + self.log(message, log: log, type: type, file: file, function: function, line: line) + } + private static func formatLogMessage(_ message: String, log: OSLog, type: OSLogType, file: String, function: String, line: Int) -> String { let category: String switch log { diff --git a/Sources/MicrophoneView.swift b/Sources/MicrophoneView.swift index ec1f387..aec331f 100644 --- a/Sources/MicrophoneView.swift +++ b/Sources/MicrophoneView.swift @@ -260,19 +260,23 @@ struct MicrophoneView: View { startedByHotkey = false } .onReceive(NotificationCenter.default.publisher(for: .didFinishRecording)) { notif in + Logger.debugLog("=== RECEIVED didFinishRecording NOTIFICATION ===", log: Logger.audio) if overlayShown { RecordingOverlayManager.shared.hide() overlayShown = false } if let fileUrl = notif.object as? URL { + Logger.log("File URL: \(fileUrl.path)", log: Logger.audio) if !GenericHelper.fileExists(file: fileUrl) { Logger.log("Recording file \(fileUrl.path) not found", log: Logger.audio, type: .error) resetState(error: "Recording file not found. Please try again.") return } - + Logger.log("File exists, calling transcribeAudio", log: Logger.audio) transcribeAudio(url: fileUrl, source: .microphone) + } else { + Logger.log("ERROR: No file URL in notification", log: Logger.audio, type: .error) } } .onReceive(NotificationCenter.default.publisher(for: .recordingError)) { notif in @@ -334,17 +338,22 @@ struct MicrophoneView: View { } private func transcribeAudio(url: URL, source: TranscriptionSource, filename: String? = nil) { + Logger.debugLog("=== TRANSCRIBE AUDIO CALLED ===", log: Logger.audio) if isTranscribing { Logger.log("Already transcribing", log: Logger.audio) resetState(error: "Already transcribing. Please wait for the current transcription to finish.") return } isTranscribing = true + Logger.log("Starting transcription task", log: Logger.audio) Task { do { + Logger.log("Creating voice to text model", log: Logger.audio) let voiceToTextModel = VoiceToTextFactory.createVoiceToText() + Logger.log("Processing audio file: \(url.path)", log: Logger.audio) let text = try await voiceToTextModel.process(filepath: url.path) + Logger.log("Transcription complete, text length: \(text.count)", log: Logger.audio) audio.reset() if GenericHelper.logSensitiveData() { @@ -403,16 +412,27 @@ struct MicrophoneView: View { } private func processText(text: String, source: TranscriptionSource, filename: String? = nil) async { + Logger.debugLog("=== PROCESS TEXT CALLED ===", log: Logger.audio) + Logger.log("Text length: \(text.count)", log: Logger.audio) defer { self.isTranscribing = false self.isProcessing = false } + Logger.log("Copying to clipboard", log: Logger.audio) GenericHelper.copyToClipboard(text: text) - let pasted = GenericHelper.paste(text: text) - - if pasted && settings.autoEnter { - _ = GenericHelper.sendEnter() + + var pasted = false + if settings.autoPaste { + Logger.log("Calling paste function", log: Logger.audio) + pasted = GenericHelper.paste(text: text) + Logger.log("Paste returned: \(pasted)", log: Logger.audio) + + if pasted && settings.autoEnter { + _ = GenericHelper.sendEnter() + } + } else { + Logger.log("Auto-paste disabled in settings", log: Logger.audio) } self.resultText = text diff --git a/Sources/SettingsStore.swift b/Sources/SettingsStore.swift index a0fb1fb..94dec2d 100644 --- a/Sources/SettingsStore.swift +++ b/Sources/SettingsStore.swift @@ -18,7 +18,9 @@ struct DefaultSettings { static let hasCompletedOnboarding = false static let language = "auto" static let sttEngine = STTEngine.parakeet + static let autoPaste = true static let autoEnter = false + static let debugLogging = false static let startMinimized = false static let displayRecordingOverlay = false static let overlayPosition = "topRight" @@ -52,7 +54,9 @@ class SettingsStore: ObservableObject { case hasCompletedOnboarding = "hasCompletedOnboarding" case language = "language" case sttEngine = "sttEngine" + case autoPaste = "autoPaste" case autoEnter = "autoEnter" + case debugLogging = "debugLogging" case startMinimized = "startMinimized" case displayRecordingOverlay = "displayRecordingOverlay" case overlayPosition = "overlayPosition" @@ -94,13 +98,25 @@ class SettingsStore: ObservableObject { defaults.set(sttEngine.rawValue, forKey: Keys.sttEngine.rawValue) } } - + + @Published var autoPaste: Bool = DefaultSettings.autoPaste { + didSet { + defaults.set(autoPaste, forKey: Keys.autoPaste.rawValue) + } + } + @Published var autoEnter: Bool = DefaultSettings.autoEnter { didSet { defaults.set(autoEnter, forKey: Keys.autoEnter.rawValue) } } - + + @Published var debugLogging: Bool = DefaultSettings.debugLogging { + didSet { + defaults.set(debugLogging, forKey: Keys.debugLogging.rawValue) + } + } + @Published var startMinimized: Bool = DefaultSettings.startMinimized { didSet { defaults.set(startMinimized, forKey: Keys.startMinimized.rawValue) @@ -247,7 +263,9 @@ class SettingsStore: ObservableObject { } else { self.sttEngine = DefaultSettings.sttEngine } + self.autoPaste = defaults.object(forKey: Keys.autoPaste.rawValue) == nil ? DefaultSettings.autoPaste : defaults.bool(forKey: Keys.autoPaste.rawValue) self.autoEnter = defaults.object(forKey: Keys.autoEnter.rawValue) == nil ? DefaultSettings.autoEnter : defaults.bool(forKey: Keys.autoEnter.rawValue) + self.debugLogging = defaults.object(forKey: Keys.debugLogging.rawValue) == nil ? DefaultSettings.debugLogging : defaults.bool(forKey: Keys.debugLogging.rawValue) self.startMinimized = defaults.object(forKey: Keys.startMinimized.rawValue) == nil ? DefaultSettings.startMinimized : defaults.bool(forKey: Keys.startMinimized.rawValue) self.displayRecordingOverlay = defaults.object(forKey: Keys.displayRecordingOverlay.rawValue) == nil ? DefaultSettings.displayRecordingOverlay : defaults.bool(forKey: Keys.displayRecordingOverlay.rawValue) self.overlayPosition = defaults.string(forKey: Keys.overlayPosition.rawValue) ?? DefaultSettings.overlayPosition @@ -336,7 +354,9 @@ class SettingsStore: ObservableObject { hasCompletedOnboarding = DefaultSettings.hasCompletedOnboarding language = DefaultSettings.language sttEngine = DefaultSettings.sttEngine + autoPaste = DefaultSettings.autoPaste autoEnter = DefaultSettings.autoEnter + debugLogging = DefaultSettings.debugLogging startMinimized = DefaultSettings.startMinimized displayRecordingOverlay = DefaultSettings.displayRecordingOverlay overlayPosition = DefaultSettings.overlayPosition diff --git a/Sources/SettingsView.swift b/Sources/SettingsView.swift index d0cd687..9fa9538 100644 --- a/Sources/SettingsView.swift +++ b/Sources/SettingsView.swift @@ -167,17 +167,30 @@ struct SettingsView: View { Text("Auto Actions") .font(.headline) .foregroundColor(.white) - + + Toggle("Auto-paste after transcription", isOn: Binding( + get: { settings.autoPaste }, + set: { newValue in + settings.autoPaste = newValue + } + )) + + Text("Automatically paste transcribed text into the active application.") + .font(.caption) + .foregroundColor(.gray) + Toggle("Auto-press Enter after paste", isOn: Binding( get: { settings.autoEnter }, set: { newValue in settings.autoEnter = newValue } )) - + .disabled(!settings.autoPaste) + Text("Automatically press Enter after pasting transcribed text into the active application.") .font(.caption) .foregroundColor(.gray) + .opacity(settings.autoPaste ? 1.0 : 0.5) } .padding() }