From 9003e93b5d719905a5ce24a447a09b84f9af3ccb Mon Sep 17 00:00:00 2001 From: Beingpax Date: Wed, 26 Mar 2025 20:02:18 +0545 Subject: [PATCH] Switched to previous version of Double-press lock recording with brief delay consideration --- VoiceInk.xcodeproj/project.pbxproj | 14 +-- VoiceInk/HotkeyManager.swift | 147 +++++++++++------------------ 2 files changed, 65 insertions(+), 96 deletions(-) diff --git a/VoiceInk.xcodeproj/project.pbxproj b/VoiceInk.xcodeproj/project.pbxproj index 803ccab..aa0d63f 100644 --- a/VoiceInk.xcodeproj/project.pbxproj +++ b/VoiceInk.xcodeproj/project.pbxproj @@ -7,11 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + E129E77B2D943393009322D9 /* whisper.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E129E77A2D943393009322D9 /* whisper.xcframework */; }; + E129E77C2D943393009322D9 /* whisper.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E129E77A2D943393009322D9 /* whisper.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E1A261122CC143AC00B233D1 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = E1A261112CC143AC00B233D1 /* KeyboardShortcuts */; }; E1ADD45A2CC5352A00303ECB /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = E1ADD4592CC5352A00303ECB /* LaunchAtLogin */; }; E1ADD45F2CC544F100303ECB /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E1ADD45E2CC544F100303ECB /* Sparkle */; }; - E1B1FDBE2D8C403100ADD08E /* whisper.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1B1FDBD2D8C403100ADD08E /* whisper.xcframework */; }; - E1B1FDBF2D8C403100ADD08E /* whisper.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E1B1FDBD2D8C403100ADD08E /* whisper.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -32,13 +32,13 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ - E1B1FDC02D8C403100ADD08E /* Embed Frameworks */ = { + E129E77D2D943393009322D9 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( - E1B1FDBF2D8C403100ADD08E /* whisper.xcframework in Embed Frameworks */, + E129E77C2D943393009322D9 /* whisper.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -49,6 +49,7 @@ E11473B02CBE0F0A00318EE4 /* VoiceInk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VoiceInk.app; sourceTree = BUILT_PRODUCTS_DIR; }; E11473C32CBE0F0B00318EE4 /* VoiceInkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VoiceInkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; E11473CD2CBE0F0B00318EE4 /* VoiceInkUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VoiceInkUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + E129E77A2D943393009322D9 /* whisper.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = whisper.xcframework; path = "../whisper.cpp/build-apple/whisper.xcframework"; sourceTree = ""; }; E1B1FDBD2D8C403100ADD08E /* whisper.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = whisper.xcframework; path = "../../whisper.cpp/build-apple/whisper.xcframework"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -75,7 +76,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E1B1FDBE2D8C403100ADD08E /* whisper.xcframework in Frameworks */, + E129E77B2D943393009322D9 /* whisper.xcframework in Frameworks */, E1ADD45A2CC5352A00303ECB /* LaunchAtLogin in Frameworks */, E1ADD45F2CC544F100303ECB /* Sparkle in Frameworks */, E1A261122CC143AC00B233D1 /* KeyboardShortcuts in Frameworks */, @@ -123,6 +124,7 @@ E114741C2CBE1DE200318EE4 /* Frameworks */ = { isa = PBXGroup; children = ( + E129E77A2D943393009322D9 /* whisper.xcframework */, E1B1FDBD2D8C403100ADD08E /* whisper.xcframework */, ); name = Frameworks; @@ -138,7 +140,7 @@ E11473AC2CBE0F0A00318EE4 /* Sources */, E11473AD2CBE0F0A00318EE4 /* Frameworks */, E11473AE2CBE0F0A00318EE4 /* Resources */, - E1B1FDC02D8C403100ADD08E /* Embed Frameworks */, + E129E77D2D943393009322D9 /* Embed Frameworks */, ); buildRules = ( ); diff --git a/VoiceInk/HotkeyManager.swift b/VoiceInk/HotkeyManager.swift index 6bfb429..d122ca1 100644 --- a/VoiceInk/HotkeyManager.swift +++ b/VoiceInk/HotkeyManager.swift @@ -19,13 +19,6 @@ extension KeyboardShortcuts.Name { static let selectPrompt9 = Self("selectPrompt9") } -// State machine enum for recorder state -enum RecorderState { - case idle // Not recording, recorder not visible - case recording // Actively recording with key held down - case lockedRecording // Recording in locked mode (after double press) -} - @MainActor class HotkeyManager: ObservableObject { @Published var isListening = false @@ -50,11 +43,11 @@ class HotkeyManager: ObservableObject { private var runLoopSource: CFRunLoopSource? private var visibilityTask: Task? - // State machine properties - private var recorderState: RecorderState = .idle - private var lastKeyPressTime: Date? - private var keyPressStartTime: Date? // Track when key was pressed for duration calculation - private let doublePressThreshold = 0.3 // 300ms for double-press detection + // New properties for advanced key handling + private var keyPressStartTime: Date? + private var lastKeyPressEndTime: Date? + private var isLockedRecording = false // For toggle mode after double-press + private let doublePressThreshold = 0.3 // 300ms for faster double-press detection private let briefPressThreshold = 1.0 // 1000ms threshold for brief press enum PushToTalkKey: String, CaseIterable { @@ -103,8 +96,9 @@ class HotkeyManager: ObservableObject { private func resetKeyStates() { currentKeyState = false - lastKeyPressTime = nil - recorderState = .idle + keyPressStartTime = nil + lastKeyPressEndTime = nil + isLockedRecording = false } private func setupVisibilityObserver() { @@ -118,10 +112,6 @@ class HotkeyManager: ObservableObject { removeEscapeShortcut() removeEnhancementShortcut() removePromptShortcuts() - // Ensure state is reset when recorder is dismissed externally - if recorderState != .idle { - recorderState = .idle - } } } } @@ -186,73 +176,58 @@ class HotkeyManager: ObservableObject { currentKeyState = isKeyPressed + // Key is pressed down if isKeyPressed { - await handleKeyPress() - } else { - await handleKeyRelease() - } - } - - private func handleKeyPress() async { - let now = Date() - keyPressStartTime = now // Track when the key was pressed - - switch recorderState { - case .idle: - // Start recording - recorderState = .recording + // If we're in locked recording mode, key press should stop recording + if isLockedRecording && whisperState.isMiniRecorderVisible { + isLockedRecording = false + await whisperState.handleToggleMiniRecorder() + return + } + + // Start timing the key press + keyPressStartTime = Date() + + // Check for double press + if let lastEndTime = lastKeyPressEndTime, + Date().timeIntervalSince(lastEndTime) < doublePressThreshold { + // Double press detected - set locked recording mode + isLockedRecording = true + } + + // Show recorder if not already visible if !whisperState.isMiniRecorderVisible { await whisperState.handleToggleMiniRecorder() } + } + // Key is released + else { + let now = Date() + lastKeyPressEndTime = now - case .recording: - // This shouldn't happen in normal flow - break - - case .lockedRecording: - // If in locked recording, pressing the key again should stop recording - recorderState = .idle - await whisperState.handleToggleMiniRecorder() - } - - // Check for double press - if let lastPress = lastKeyPressTime, - now.timeIntervalSince(lastPress) < doublePressThreshold { - // Double press detected, transition to locked recording - recorderState = .lockedRecording - } - - lastKeyPressTime = now - } - - private func handleKeyRelease() async { - let now = Date() - - switch recorderState { - case .idle: - // This shouldn't happen in normal flow - break - - case .recording: - // Check if this was a brief press - if let startTime = keyPressStartTime, - now.timeIntervalSince(startTime) < briefPressThreshold { - // Brief press - dismiss without transcribing - recorderState = .idle - await whisperState.dismissMiniRecorder() - } else { - // Normal release - stop recording and transcribe - recorderState = .idle - await whisperState.handleToggleMiniRecorder() + // Calculate press duration + if let startTime = keyPressStartTime { + let pressDuration = now.timeIntervalSince(startTime) + + // 1. Brief press (< 1s): Delay dismissal to check for double-press + if pressDuration < briefPressThreshold && !isLockedRecording { + // Wait to see if this is part of a double-press + try? await Task.sleep(nanoseconds: 200_000_000) // 200ms delay + + // After waiting, check if we should still dismiss + if !isLockedRecording { + await whisperState.dismissMiniRecorder() + } + } + // 2. Normal press in non-locked mode: Use handleToggleMiniRecorder to stop and transcribe + else if !isLockedRecording && whisperState.isMiniRecorderVisible { + await whisperState.handleToggleMiniRecorder() + } + // 3. If in locked mode, we don't do anything on release } - case .lockedRecording: - // When in locked recording, key release does nothing - // Stay in locked recording state - break + keyPressStartTime = nil } - - keyPressStartTime = nil // Reset press start time } private func setupEscapeShortcut() { @@ -262,8 +237,8 @@ class HotkeyManager: ObservableObject { guard let self = self, await self.whisperState.isMiniRecorderVisible else { return } - // Reset state machine when using Escape key - self.recorderState = .idle + // Reset locked recording state when using Escape key + self.isLockedRecording = false SoundManager.shared.playEscSound() await self.whisperState.dismissMiniRecorder() @@ -285,6 +260,7 @@ class HotkeyManager: ObservableObject { } } } + private func setupPromptShortcuts() { // Set up Command+1 through Command+9 shortcuts with proper key definitions KeyboardShortcuts.setShortcut(.init(.one, modifiers: .command), for: .selectPrompt1) @@ -359,16 +335,7 @@ class HotkeyManager: ObservableObject { private func setupShortcutHandler() { KeyboardShortcuts.onKeyUp(for: .toggleMiniRecorder) { [weak self] in Task { @MainActor in - guard let self = self else { return } - - // Update state when using the main shortcut - if self.recorderState == .idle { - self.recorderState = .recording - } else { - self.recorderState = .idle - } - - await self.whisperState.handleToggleMiniRecorder() + await self?.whisperState.handleToggleMiniRecorder() } } } @@ -381,4 +348,4 @@ class HotkeyManager: ObservableObject { removeEnhancementShortcut() } } -} +} \ No newline at end of file