├── .gitignore ├── WhispeAnywhere.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── unclecode.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist └── WhispeAnywhere ├── AppDelegate.swift ├── AppDelegateHelpers.swift ├── Assets.xcassets ├── AccentColor.colorset │ └── Contents.json ├── AppIcon.appiconset │ ├── 1024.png │ ├── 128.png │ ├── 16.png │ ├── 256 1.png │ ├── 256.png │ ├── 32 1.png │ ├── 32.png │ ├── 512 1.png │ ├── 512.png │ ├── 64.png │ └── Contents.json ├── Contents.json └── StatusBarIcon.imageset │ ├── Contents.json │ ├── tray-icon-16.png │ ├── tray-icon-24.png │ └── tray-icon-32.png ├── AudioRecorder.swift ├── ClipboardManager.swift ├── ContentView.swift ├── GroqAPI.swift ├── HotkeyManager.swift ├── Info.plist ├── LogViewerWindow.swift ├── Logger.swift ├── OverlayWindow.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json ├── SettingsStore.swift ├── SettingsView.swift ├── SpotlightChatView.swift ├── StatusBarController.swift ├── WhispeAnywhere.app └── Contents │ ├── CodeResources │ ├── Info.plist │ ├── MacOS │ └── WhispeAnywhere │ ├── PkgInfo │ ├── Resources │ ├── Assets.car │ └── Info.plist │ └── _CodeSignature │ └── CodeResources ├── WhispeAnywhere.entitlements └── WhispeAnywhereApp.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## Obj-C/Swift specific 9 | *.hmap 10 | 11 | ## App packaging 12 | *.ipa 13 | *.dSYM.zip 14 | *.dSYM 15 | 16 | ## Playgrounds 17 | timeline.xctimeline 18 | playground.xcworkspace 19 | 20 | # Swift Package Manager 21 | # 22 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 23 | # Packages/ 24 | # Package.pins 25 | # Package.resolved 26 | # *.xcodeproj 27 | # 28 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 29 | # hence it is not needed unless you have added a package configuration file to your project 30 | # .swiftpm 31 | 32 | .build/ 33 | 34 | # CocoaPods 35 | # 36 | # We recommend against adding the Pods directory to your .gitignore. However 37 | # you should judge for yourself, the pros and cons are mentioned at: 38 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 39 | # 40 | # Pods/ 41 | # 42 | # Add this line if you want to avoid checking in source code from the Xcode workspace 43 | # *.xcworkspace 44 | 45 | # Carthage 46 | # 47 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 48 | # Carthage/Checkouts 49 | 50 | Carthage/Build/ 51 | 52 | # fastlane 53 | # 54 | # It is recommended to not store the screenshots in the git repo. 55 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 56 | # For more information about the recommended setup visit: 57 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 58 | 59 | fastlane/report.xml 60 | fastlane/Preview.html 61 | fastlane/screenshots/**/*.png 62 | fastlane/test_output 63 | -------------------------------------------------------------------------------- /WhispeAnywhere.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E3927F372C96B39800661FEB /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E3927F362C96B39800661FEB /* AVFoundation.framework */; }; 11 | E3927F3A2C96C7A500661FEB /* Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = E3927F382C96B3C500661FEB /* Info.plist */; }; 12 | E3927F3D2C96CC5C00661FEB /* SettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3927F3C2C96CC5C00661FEB /* SettingsStore.swift */; }; 13 | E3927F3F2C96E11000661FEB /* AppDelegateHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3927F3E2C96E11000661FEB /* AppDelegateHelpers.swift */; }; 14 | E3927F422C97240200661FEB /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3927F412C97240200661FEB /* Logger.swift */; }; 15 | E3927F442C97270B00661FEB /* LogViewerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3927F432C97270B00661FEB /* LogViewerWindow.swift */; }; 16 | E3927F462C98035F00661FEB /* SpotlightChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3927F452C98035F00661FEB /* SpotlightChatView.swift */; }; 17 | E3970C632C96A06700E3689D /* WhispeAnywhereApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3970C622C96A06700E3689D /* WhispeAnywhereApp.swift */; }; 18 | E3970C652C96A06700E3689D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3970C642C96A06700E3689D /* ContentView.swift */; }; 19 | E3970C672C96A06800E3689D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E3970C662C96A06800E3689D /* Assets.xcassets */; }; 20 | E3970C6A2C96A06800E3689D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E3970C692C96A06800E3689D /* Preview Assets.xcassets */; }; 21 | E3970C722C96A0F500E3689D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3970C712C96A0F500E3689D /* AppDelegate.swift */; }; 22 | E3970C742C96A10200E3689D /* StatusBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3970C732C96A10200E3689D /* StatusBarController.swift */; }; 23 | E3970C762C96A10F00E3689D /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3970C752C96A10F00E3689D /* SettingsView.swift */; }; 24 | E3970C782C96A11800E3689D /* AudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3970C772C96A11800E3689D /* AudioRecorder.swift */; }; 25 | E3970C7A2C96A12100E3689D /* OverlayWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3970C792C96A12100E3689D /* OverlayWindow.swift */; }; 26 | E3970C7C2C96A12C00E3689D /* HotkeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3970C7B2C96A12C00E3689D /* HotkeyManager.swift */; }; 27 | E3970C7E2C96A13700E3689D /* GroqAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3970C7D2C96A13700E3689D /* GroqAPI.swift */; }; 28 | E3970C802C96A52200E3689D /* ClipboardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3970C7F2C96A52200E3689D /* ClipboardManager.swift */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | E3927F362C96B39800661FEB /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; }; 33 | E3927F382C96B3C500661FEB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 34 | E3927F3C2C96CC5C00661FEB /* SettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStore.swift; sourceTree = ""; }; 35 | E3927F3E2C96E11000661FEB /* AppDelegateHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegateHelpers.swift; sourceTree = ""; }; 36 | E3927F412C97240200661FEB /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 37 | E3927F432C97270B00661FEB /* LogViewerWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewerWindow.swift; sourceTree = ""; }; 38 | E3927F452C98035F00661FEB /* SpotlightChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpotlightChatView.swift; sourceTree = ""; }; 39 | E3970C5F2C96A06700E3689D /* WhispeAnywhere.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WhispeAnywhere.app; sourceTree = BUILT_PRODUCTS_DIR; }; 40 | E3970C622C96A06700E3689D /* WhispeAnywhereApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhispeAnywhereApp.swift; sourceTree = ""; }; 41 | E3970C642C96A06700E3689D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 42 | E3970C662C96A06800E3689D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | E3970C692C96A06800E3689D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 44 | E3970C6B2C96A06800E3689D /* WhispeAnywhere.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WhispeAnywhere.entitlements; sourceTree = ""; }; 45 | E3970C712C96A0F500E3689D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 46 | E3970C732C96A10200E3689D /* StatusBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarController.swift; sourceTree = ""; }; 47 | E3970C752C96A10F00E3689D /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 48 | E3970C772C96A11800E3689D /* AudioRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecorder.swift; sourceTree = ""; }; 49 | E3970C792C96A12100E3689D /* OverlayWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverlayWindow.swift; sourceTree = ""; }; 50 | E3970C7B2C96A12C00E3689D /* HotkeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyManager.swift; sourceTree = ""; }; 51 | E3970C7D2C96A13700E3689D /* GroqAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroqAPI.swift; sourceTree = ""; }; 52 | E3970C7F2C96A52200E3689D /* ClipboardManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardManager.swift; sourceTree = ""; }; 53 | /* End PBXFileReference section */ 54 | 55 | /* Begin PBXFrameworksBuildPhase section */ 56 | E3970C5C2C96A06700E3689D /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | E3927F372C96B39800661FEB /* AVFoundation.framework in Frameworks */, 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | E3927F352C96B39800661FEB /* Frameworks */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | E3927F362C96B39800661FEB /* AVFoundation.framework */, 71 | ); 72 | name = Frameworks; 73 | sourceTree = ""; 74 | }; 75 | E3970C562C96A06700E3689D = { 76 | isa = PBXGroup; 77 | children = ( 78 | E3970C612C96A06700E3689D /* WhispeAnywhere */, 79 | E3970C602C96A06700E3689D /* Products */, 80 | E3927F352C96B39800661FEB /* Frameworks */, 81 | ); 82 | sourceTree = ""; 83 | }; 84 | E3970C602C96A06700E3689D /* Products */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | E3970C5F2C96A06700E3689D /* WhispeAnywhere.app */, 88 | ); 89 | name = Products; 90 | sourceTree = ""; 91 | }; 92 | E3970C612C96A06700E3689D /* WhispeAnywhere */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | E3970C622C96A06700E3689D /* WhispeAnywhereApp.swift */, 96 | E3970C642C96A06700E3689D /* ContentView.swift */, 97 | E3970C662C96A06800E3689D /* Assets.xcassets */, 98 | E3970C6B2C96A06800E3689D /* WhispeAnywhere.entitlements */, 99 | E3970C682C96A06800E3689D /* Preview Content */, 100 | E3970C712C96A0F500E3689D /* AppDelegate.swift */, 101 | E3970C732C96A10200E3689D /* StatusBarController.swift */, 102 | E3970C752C96A10F00E3689D /* SettingsView.swift */, 103 | E3970C792C96A12100E3689D /* OverlayWindow.swift */, 104 | E3970C772C96A11800E3689D /* AudioRecorder.swift */, 105 | E3970C7B2C96A12C00E3689D /* HotkeyManager.swift */, 106 | E3970C7D2C96A13700E3689D /* GroqAPI.swift */, 107 | E3970C7F2C96A52200E3689D /* ClipboardManager.swift */, 108 | E3927F382C96B3C500661FEB /* Info.plist */, 109 | E3927F3C2C96CC5C00661FEB /* SettingsStore.swift */, 110 | E3927F3E2C96E11000661FEB /* AppDelegateHelpers.swift */, 111 | E3927F412C97240200661FEB /* Logger.swift */, 112 | E3927F432C97270B00661FEB /* LogViewerWindow.swift */, 113 | E3927F452C98035F00661FEB /* SpotlightChatView.swift */, 114 | ); 115 | path = WhispeAnywhere; 116 | sourceTree = ""; 117 | }; 118 | E3970C682C96A06800E3689D /* Preview Content */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | E3970C692C96A06800E3689D /* Preview Assets.xcassets */, 122 | ); 123 | path = "Preview Content"; 124 | sourceTree = ""; 125 | }; 126 | /* End PBXGroup section */ 127 | 128 | /* Begin PBXNativeTarget section */ 129 | E3970C5E2C96A06700E3689D /* WhispeAnywhere */ = { 130 | isa = PBXNativeTarget; 131 | buildConfigurationList = E3970C6E2C96A06800E3689D /* Build configuration list for PBXNativeTarget "WhispeAnywhere" */; 132 | buildPhases = ( 133 | E3970C5B2C96A06700E3689D /* Sources */, 134 | E3970C5C2C96A06700E3689D /* Frameworks */, 135 | E3970C5D2C96A06700E3689D /* Resources */, 136 | ); 137 | buildRules = ( 138 | ); 139 | dependencies = ( 140 | ); 141 | name = WhispeAnywhere; 142 | productName = WhispeAnywhere; 143 | productReference = E3970C5F2C96A06700E3689D /* WhispeAnywhere.app */; 144 | productType = "com.apple.product-type.application"; 145 | }; 146 | /* End PBXNativeTarget section */ 147 | 148 | /* Begin PBXProject section */ 149 | E3970C572C96A06700E3689D /* Project object */ = { 150 | isa = PBXProject; 151 | attributes = { 152 | BuildIndependentTargetsInParallel = 1; 153 | LastSwiftUpdateCheck = 1540; 154 | LastUpgradeCheck = 1540; 155 | TargetAttributes = { 156 | E3970C5E2C96A06700E3689D = { 157 | CreatedOnToolsVersion = 15.4; 158 | }; 159 | }; 160 | }; 161 | buildConfigurationList = E3970C5A2C96A06700E3689D /* Build configuration list for PBXProject "WhispeAnywhere" */; 162 | compatibilityVersion = "Xcode 14.0"; 163 | developmentRegion = en; 164 | hasScannedForEncodings = 0; 165 | knownRegions = ( 166 | en, 167 | Base, 168 | ); 169 | mainGroup = E3970C562C96A06700E3689D; 170 | productRefGroup = E3970C602C96A06700E3689D /* Products */; 171 | projectDirPath = ""; 172 | projectRoot = ""; 173 | targets = ( 174 | E3970C5E2C96A06700E3689D /* WhispeAnywhere */, 175 | ); 176 | }; 177 | /* End PBXProject section */ 178 | 179 | /* Begin PBXResourcesBuildPhase section */ 180 | E3970C5D2C96A06700E3689D /* Resources */ = { 181 | isa = PBXResourcesBuildPhase; 182 | buildActionMask = 2147483647; 183 | files = ( 184 | E3927F3A2C96C7A500661FEB /* Info.plist in Resources */, 185 | E3970C6A2C96A06800E3689D /* Preview Assets.xcassets in Resources */, 186 | E3970C672C96A06800E3689D /* Assets.xcassets in Resources */, 187 | ); 188 | runOnlyForDeploymentPostprocessing = 0; 189 | }; 190 | /* End PBXResourcesBuildPhase section */ 191 | 192 | /* Begin PBXSourcesBuildPhase section */ 193 | E3970C5B2C96A06700E3689D /* Sources */ = { 194 | isa = PBXSourcesBuildPhase; 195 | buildActionMask = 2147483647; 196 | files = ( 197 | E3970C802C96A52200E3689D /* ClipboardManager.swift in Sources */, 198 | E3970C782C96A11800E3689D /* AudioRecorder.swift in Sources */, 199 | E3927F3F2C96E11000661FEB /* AppDelegateHelpers.swift in Sources */, 200 | E3970C7A2C96A12100E3689D /* OverlayWindow.swift in Sources */, 201 | E3927F442C97270B00661FEB /* LogViewerWindow.swift in Sources */, 202 | E3970C7E2C96A13700E3689D /* GroqAPI.swift in Sources */, 203 | E3970C762C96A10F00E3689D /* SettingsView.swift in Sources */, 204 | E3970C652C96A06700E3689D /* ContentView.swift in Sources */, 205 | E3927F462C98035F00661FEB /* SpotlightChatView.swift in Sources */, 206 | E3970C742C96A10200E3689D /* StatusBarController.swift in Sources */, 207 | E3970C7C2C96A12C00E3689D /* HotkeyManager.swift in Sources */, 208 | E3927F422C97240200661FEB /* Logger.swift in Sources */, 209 | E3970C722C96A0F500E3689D /* AppDelegate.swift in Sources */, 210 | E3970C632C96A06700E3689D /* WhispeAnywhereApp.swift in Sources */, 211 | E3927F3D2C96CC5C00661FEB /* SettingsStore.swift in Sources */, 212 | ); 213 | runOnlyForDeploymentPostprocessing = 0; 214 | }; 215 | /* End PBXSourcesBuildPhase section */ 216 | 217 | /* Begin XCBuildConfiguration section */ 218 | E3970C6C2C96A06800E3689D /* Debug */ = { 219 | isa = XCBuildConfiguration; 220 | buildSettings = { 221 | ALWAYS_SEARCH_USER_PATHS = NO; 222 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 223 | CLANG_ANALYZER_NONNULL = YES; 224 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 225 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 226 | CLANG_ENABLE_MODULES = YES; 227 | CLANG_ENABLE_OBJC_ARC = YES; 228 | CLANG_ENABLE_OBJC_WEAK = YES; 229 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 230 | CLANG_WARN_BOOL_CONVERSION = YES; 231 | CLANG_WARN_COMMA = YES; 232 | CLANG_WARN_CONSTANT_CONVERSION = YES; 233 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 234 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 235 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 236 | CLANG_WARN_EMPTY_BODY = YES; 237 | CLANG_WARN_ENUM_CONVERSION = YES; 238 | CLANG_WARN_INFINITE_RECURSION = YES; 239 | CLANG_WARN_INT_CONVERSION = YES; 240 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 241 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 242 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 243 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 244 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 245 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 246 | CLANG_WARN_STRICT_PROTOTYPES = YES; 247 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 248 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 249 | CLANG_WARN_UNREACHABLE_CODE = YES; 250 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 251 | COPY_PHASE_STRIP = NO; 252 | DEBUG_INFORMATION_FORMAT = dwarf; 253 | ENABLE_STRICT_OBJC_MSGSEND = YES; 254 | ENABLE_TESTABILITY = YES; 255 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 256 | GCC_C_LANGUAGE_STANDARD = gnu17; 257 | GCC_DYNAMIC_NO_PIC = NO; 258 | GCC_NO_COMMON_BLOCKS = YES; 259 | GCC_OPTIMIZATION_LEVEL = 0; 260 | GCC_PREPROCESSOR_DEFINITIONS = ( 261 | "DEBUG=1", 262 | "$(inherited)", 263 | ); 264 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 265 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 266 | GCC_WARN_UNDECLARED_SELECTOR = YES; 267 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 268 | GCC_WARN_UNUSED_FUNCTION = YES; 269 | GCC_WARN_UNUSED_VARIABLE = YES; 270 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 271 | MACOSX_DEPLOYMENT_TARGET = 14.5; 272 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 273 | MTL_FAST_MATH = YES; 274 | ONLY_ACTIVE_ARCH = YES; 275 | SDKROOT = macosx; 276 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 277 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 278 | }; 279 | name = Debug; 280 | }; 281 | E3970C6D2C96A06800E3689D /* Release */ = { 282 | isa = XCBuildConfiguration; 283 | buildSettings = { 284 | ALWAYS_SEARCH_USER_PATHS = NO; 285 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 286 | CLANG_ANALYZER_NONNULL = YES; 287 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 288 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 289 | CLANG_ENABLE_MODULES = YES; 290 | CLANG_ENABLE_OBJC_ARC = YES; 291 | CLANG_ENABLE_OBJC_WEAK = YES; 292 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 293 | CLANG_WARN_BOOL_CONVERSION = YES; 294 | CLANG_WARN_COMMA = YES; 295 | CLANG_WARN_CONSTANT_CONVERSION = YES; 296 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 297 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 298 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 299 | CLANG_WARN_EMPTY_BODY = YES; 300 | CLANG_WARN_ENUM_CONVERSION = YES; 301 | CLANG_WARN_INFINITE_RECURSION = YES; 302 | CLANG_WARN_INT_CONVERSION = YES; 303 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 304 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 305 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 306 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 307 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 308 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 309 | CLANG_WARN_STRICT_PROTOTYPES = YES; 310 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 311 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 312 | CLANG_WARN_UNREACHABLE_CODE = YES; 313 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 314 | COPY_PHASE_STRIP = NO; 315 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 316 | ENABLE_NS_ASSERTIONS = NO; 317 | ENABLE_STRICT_OBJC_MSGSEND = YES; 318 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 319 | GCC_C_LANGUAGE_STANDARD = gnu17; 320 | GCC_NO_COMMON_BLOCKS = YES; 321 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 322 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 323 | GCC_WARN_UNDECLARED_SELECTOR = YES; 324 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 325 | GCC_WARN_UNUSED_FUNCTION = YES; 326 | GCC_WARN_UNUSED_VARIABLE = YES; 327 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 328 | MACOSX_DEPLOYMENT_TARGET = 14.5; 329 | MTL_ENABLE_DEBUG_INFO = NO; 330 | MTL_FAST_MATH = YES; 331 | SDKROOT = macosx; 332 | SWIFT_COMPILATION_MODE = wholemodule; 333 | }; 334 | name = Release; 335 | }; 336 | E3970C6F2C96A06800E3689D /* Debug */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 340 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 341 | CODE_SIGN_ENTITLEMENTS = WhispeAnywhere/WhispeAnywhere.entitlements; 342 | CODE_SIGN_STYLE = Automatic; 343 | COMBINE_HIDPI_IMAGES = YES; 344 | CURRENT_PROJECT_VERSION = 1; 345 | DEVELOPMENT_ASSET_PATHS = "\"WhispeAnywhere/Preview Content\""; 346 | DEVELOPMENT_TEAM = TPP52TWEWR; 347 | ENABLE_HARDENED_RUNTIME = YES; 348 | ENABLE_PREVIEWS = YES; 349 | GENERATE_INFOPLIST_FILE = YES; 350 | INFOPLIST_FILE = /Users/unclecode/devs/whisperanywhere/WhispeAnywhere/WhispeAnywhere/info.plist; 351 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 352 | LD_RUNPATH_SEARCH_PATHS = ( 353 | "$(inherited)", 354 | "@executable_path/../Frameworks", 355 | ); 356 | MACOSX_DEPLOYMENT_TARGET = 14.5; 357 | MARKETING_VERSION = 1.0; 358 | PRODUCT_BUNDLE_IDENTIFIER = com.unclecode.WhispeAnywhere; 359 | PRODUCT_NAME = "$(TARGET_NAME)"; 360 | SWIFT_EMIT_LOC_STRINGS = YES; 361 | SWIFT_VERSION = 5.0; 362 | }; 363 | name = Debug; 364 | }; 365 | E3970C702C96A06800E3689D /* Release */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 369 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 370 | CODE_SIGN_ENTITLEMENTS = WhispeAnywhere/WhispeAnywhere.entitlements; 371 | CODE_SIGN_STYLE = Automatic; 372 | COMBINE_HIDPI_IMAGES = YES; 373 | CURRENT_PROJECT_VERSION = 1; 374 | DEVELOPMENT_ASSET_PATHS = "\"WhispeAnywhere/Preview Content\""; 375 | DEVELOPMENT_TEAM = TPP52TWEWR; 376 | ENABLE_HARDENED_RUNTIME = YES; 377 | ENABLE_PREVIEWS = YES; 378 | GENERATE_INFOPLIST_FILE = YES; 379 | INFOPLIST_FILE = /Users/unclecode/devs/whisperanywhere/WhispeAnywhere/WhispeAnywhere/info.plist; 380 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 381 | LD_RUNPATH_SEARCH_PATHS = ( 382 | "$(inherited)", 383 | "@executable_path/../Frameworks", 384 | ); 385 | MACOSX_DEPLOYMENT_TARGET = 14.5; 386 | MARKETING_VERSION = 1.0; 387 | PRODUCT_BUNDLE_IDENTIFIER = com.unclecode.WhispeAnywhere; 388 | PRODUCT_NAME = "$(TARGET_NAME)"; 389 | SWIFT_EMIT_LOC_STRINGS = YES; 390 | SWIFT_VERSION = 5.0; 391 | }; 392 | name = Release; 393 | }; 394 | /* End XCBuildConfiguration section */ 395 | 396 | /* Begin XCConfigurationList section */ 397 | E3970C5A2C96A06700E3689D /* Build configuration list for PBXProject "WhispeAnywhere" */ = { 398 | isa = XCConfigurationList; 399 | buildConfigurations = ( 400 | E3970C6C2C96A06800E3689D /* Debug */, 401 | E3970C6D2C96A06800E3689D /* Release */, 402 | ); 403 | defaultConfigurationIsVisible = 0; 404 | defaultConfigurationName = Release; 405 | }; 406 | E3970C6E2C96A06800E3689D /* Build configuration list for PBXNativeTarget "WhispeAnywhere" */ = { 407 | isa = XCConfigurationList; 408 | buildConfigurations = ( 409 | E3970C6F2C96A06800E3689D /* Debug */, 410 | E3970C702C96A06800E3689D /* Release */, 411 | ); 412 | defaultConfigurationIsVisible = 0; 413 | defaultConfigurationName = Release; 414 | }; 415 | /* End XCConfigurationList section */ 416 | }; 417 | rootObject = E3970C572C96A06700E3689D /* Project object */; 418 | } 419 | -------------------------------------------------------------------------------- /WhispeAnywhere.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /WhispeAnywhere.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /WhispeAnywhere.xcodeproj/xcuserdata/unclecode.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | WhispeAnywhere.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /WhispeAnywhere/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import SwiftUI 3 | import Carbon 4 | import AVFoundation 5 | 6 | class AppDelegate: NSObject, NSApplicationDelegate, HotkeyManagerDelegate { 7 | var statusBarController: StatusBarController? 8 | var hotkeyManager: HotkeyManager? 9 | var audioRecorder: AudioRecorder? 10 | var overlayWindow: OverlayWindow? 11 | var groqAPI: GroqAPI? 12 | var settingsWindowController: NSWindowController? 13 | let settingsStore = SettingsStore() 14 | var overlayUpdateTimer: Timer? 15 | private var logViewerWindow: LogViewerWindow? 16 | 17 | @AppStorage("selectedModel") var selectedModel = "Groq" 18 | @AppStorage("groqAPIKey") var groqAPIKey = "" 19 | @AppStorage("hotkey") var hotkey = "Cmd+Shift+K" 20 | @AppStorage("autoInsert") var autoInsert = true 21 | @AppStorage("showOverlay") var showOverlay = true 22 | 23 | 24 | 25 | func applicationDidFinishLaunching(_ aNotification: Notification) { 26 | Logger.log("Application did finish launching") 27 | setupErrorHandling() 28 | AppDelegateHelpers.checkMicrophoneUsageDescription() 29 | setupComponents() 30 | } 31 | 32 | private func setupErrorHandling() { 33 | NSSetUncaughtExceptionHandler { exception in 34 | Logger.log("Uncaught exception: \(exception)") 35 | Logger.log("Call stack: \(exception.callStackSymbols)") 36 | } 37 | } 38 | 39 | private func setupComponents() { 40 | Logger.log("Setting up components") 41 | setupStatusBar() 42 | setupHotkey() 43 | setupAudioRecorder() 44 | setupOverlayWindow() 45 | setupGroqAPI() 46 | startOverlayUpdateTimer() 47 | } 48 | 49 | private func setupStatusBar() { 50 | statusBarController = StatusBarController() 51 | statusBarController?.onPreferencesClicked = { [weak self] in 52 | self?.showSettings() 53 | } 54 | statusBarController?.onStartStopRecording = { [weak self] in 55 | self?.toggleRecording() 56 | } 57 | 58 | // Add this new line to handle the log viewer 59 | statusBarController?.showLogWindow = { [weak self] in 60 | self?.showLogViewer() 61 | } 62 | 63 | Logger.log("Status bar setup completed") 64 | } 65 | 66 | // Add this new method to your AppDelegate class 67 | private func showLogViewer() { 68 | if logViewerWindow == nil { 69 | logViewerWindow = LogViewerWindow() 70 | } 71 | logViewerWindow?.updateLogContent() 72 | logViewerWindow?.makeKeyAndOrderFront(nil) 73 | } 74 | private func setupHotkey() { 75 | Logger.log("Setting up hotkey...") 76 | hotkeyManager = HotkeyManager(settingsStore: settingsStore, delegate: self) 77 | } 78 | 79 | 80 | func hotkeyTriggered() { 81 | Logger.log("Hotkey triggered, toggling recording") 82 | toggleRecording() 83 | } 84 | 85 | 86 | private func setupAudioRecorder() { 87 | AVCaptureDevice.requestAccess(for: .audio) { [weak self] granted in 88 | DispatchQueue.main.async { 89 | if granted { 90 | Logger.log("Microphone access granted") 91 | self?.audioRecorder = AudioRecorder() 92 | } else { 93 | Logger.log("Microphone access denied") 94 | AppDelegateHelpers.showMicrophoneAccessDeniedAlert() 95 | } 96 | } 97 | } 98 | } 99 | 100 | private func setupOverlayWindow() { 101 | overlayWindow = OverlayWindow() 102 | } 103 | 104 | private func setupGroqAPI() { 105 | let apiKey = ProcessInfo.processInfo.environment["GROQ_API_KEY"] ?? settingsStore.groqAPIKey 106 | groqAPI = GroqAPI(apiKey: apiKey) 107 | Logger.log("GroqAPI setup completed") 108 | } 109 | 110 | private func startOverlayUpdateTimer() { 111 | overlayUpdateTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in 112 | self?.updateOverlayPosition() 113 | } 114 | } 115 | 116 | private func updateOverlayPosition() { 117 | guard let overlayWindow = overlayWindow, overlayWindow.isVisible else { return } 118 | overlayWindow.updatePosition(with: NSEvent.mouseLocation) 119 | } 120 | 121 | func toggleRecording() { 122 | if let audioRecorder = audioRecorder, audioRecorder.isRecording { 123 | stopRecording() 124 | } else { 125 | startRecording() 126 | } 127 | } 128 | 129 | private func startRecording() { 130 | audioRecorder?.startRecording { [weak self] success in 131 | DispatchQueue.main.async { 132 | if success { 133 | Logger.log("Recording started successfully") 134 | self?.updateOverlayStatus(.recording) 135 | self?.showOverlayAtMousePosition() 136 | } else { 137 | Logger.log("Failed to start recording") 138 | self?.updateOverlayStatus(.error) 139 | } 140 | } 141 | } 142 | } 143 | 144 | private func stopRecording() { 145 | audioRecorder?.stopRecording { [weak self] url in 146 | DispatchQueue.main.async { 147 | if let url = url { 148 | Logger.log("Recording stopped, processing audio file: \(url.lastPathComponent)") 149 | self?.updateOverlayStatus(.processing) 150 | self?.processAudio(url: url) 151 | } else { 152 | Logger.log("Failed to stop recording or no audio file produced") 153 | self?.updateOverlayStatus(.error) 154 | } 155 | } 156 | } 157 | } 158 | 159 | private func processAudio(url: URL) { 160 | Logger.log("Processing audio file: \(url.lastPathComponent)") 161 | groqAPI?.transcribe(audioFileURL: url, improveGrammar: settingsStore.improveGrammar) { [weak self] result in 162 | DispatchQueue.main.async { 163 | switch result { 164 | case .success(let transcription): 165 | Logger.log("Transcription successful") 166 | self?.handleSuccessfulTranscription(transcription) 167 | case .failure(let error): 168 | Logger.log("Transcription failed: \(error.localizedDescription)") 169 | self?.handleTranscriptionError(error) 170 | } 171 | } 172 | } 173 | } 174 | 175 | private func handleSuccessfulTranscription(_ transcription: String) { 176 | Logger.log("Handling successful transcription") 177 | updateOverlayStatus(.done) 178 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 179 | self.hideOverlay() 180 | ClipboardManager.shared.copyToClipboard(transcription) 181 | if self.settingsStore.autoInsert { 182 | if AppDelegateHelpers.checkAccessibilityPermissions() { 183 | Logger.log("Auto-inserting transcription") 184 | ClipboardManager.shared.insertText(transcription) 185 | } else { 186 | Logger.log("Accessibility permissions not granted, prompting user") 187 | self.promptForAccessibilityPermissions() 188 | } 189 | } else { 190 | Logger.log("Transcription copied to clipboard") 191 | } 192 | } 193 | } 194 | 195 | private func promptForAccessibilityPermissions() { 196 | let alert = NSAlert() 197 | alert.messageText = "Accessibility Permissions Required" 198 | alert.informativeText = "To auto-insert text, this app needs accessibility permissions. Would you like to open System Preferences to grant these permissions?" 199 | alert.alertStyle = .warning 200 | alert.addButton(withTitle: "Open System Preferences") 201 | alert.addButton(withTitle: "Cancel") 202 | 203 | if alert.runModal() == .alertFirstButtonReturn { 204 | NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")!) 205 | } 206 | } 207 | 208 | private func handleTranscriptionError(_ error: Error) { 209 | Logger.log("Handling transcription error: \(error.localizedDescription)") 210 | updateOverlayStatus(.error) 211 | AppDelegateHelpers.showTranscriptionErrorAlert(error: error) 212 | } 213 | 214 | private func updateOverlayStatus(_ status: RecordingStatus) { 215 | if showOverlay { 216 | overlayWindow?.updateStatus(status) 217 | } 218 | } 219 | 220 | private func showOverlayAtMousePosition() { 221 | guard settingsStore.showOverlay, let overlayWindow = overlayWindow else { return } 222 | overlayWindow.updatePosition(with: NSEvent.mouseLocation) 223 | overlayWindow.makeKeyAndOrderFront(nil) 224 | } 225 | 226 | private func hideOverlay() { 227 | overlayWindow?.orderOut(nil) 228 | } 229 | 230 | func showSettings() { 231 | if settingsWindowController == nil { 232 | let contentView = SettingsView(settingsStore: self.settingsStore) 233 | let window = NSWindow( 234 | contentRect: NSRect(x: 20, y: 20, width: 375, height: 250), 235 | styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], 236 | backing: .buffered, 237 | defer: false) 238 | window.center() 239 | window.setFrameAutosaveName("Settings") 240 | window.contentView = NSHostingView(rootView: contentView) 241 | window.title = "Settings" 242 | window.level = .floating 243 | window.isMovableByWindowBackground = true 244 | settingsWindowController = NSWindowController(window: window) 245 | } 246 | settingsWindowController?.showWindow(nil) 247 | settingsWindowController?.window?.makeKeyAndOrderFront(nil) 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /WhispeAnywhere/AppDelegateHelpers.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import AVFoundation 3 | 4 | class AppDelegateHelpers { 5 | static func checkMicrophoneUsageDescription() { 6 | if let usageDescription = Bundle.main.object(forInfoDictionaryKey: "NSMicrophoneUsageDescription") as? String { 7 | print("Microphone Usage Description: \(usageDescription)") 8 | } else { 9 | print("WARNING: NSMicrophoneUsageDescription not found in Info.plist") 10 | } 11 | } 12 | 13 | static func showMicrophoneAccessDeniedAlert() { 14 | let alert = NSAlert() 15 | alert.messageText = "Microphone Access Denied" 16 | alert.informativeText = "WhisperAnywhere needs access to your microphone to function properly. Please grant microphone access in System Preferences > Security & Privacy > Privacy > Microphone." 17 | alert.alertStyle = .warning 18 | alert.addButton(withTitle: "Open System Preferences") 19 | alert.addButton(withTitle: "OK") 20 | 21 | if alert.runModal() == .alertFirstButtonReturn { 22 | NSWorkspace.shared.open(URL(fileURLWithPath: "/System/Library/PreferencePanes/Security.prefPane")) 23 | } 24 | } 25 | 26 | static func checkAccessibilityPermissions() -> Bool { 27 | let checkOptPrompt = kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString 28 | let options = [checkOptPrompt: false] 29 | return AXIsProcessTrustedWithOptions(options as CFDictionary) 30 | } 31 | 32 | static func showAccessibilityAlert() { 33 | let alert = NSAlert() 34 | alert.messageText = "Accessibility Permissions Required" 35 | alert.informativeText = "To automatically insert text, WhisperAnywhere needs accessibility permissions. Please grant these permissions in System Preferences > Security & Privacy > Privacy > Accessibility." 36 | alert.alertStyle = .warning 37 | alert.addButton(withTitle: "Open System Preferences") 38 | alert.addButton(withTitle: "OK") 39 | 40 | if alert.runModal() == .alertFirstButtonReturn { 41 | NSWorkspace.shared.open(URL(fileURLWithPath: "/System/Library/PreferencePanes/Security.prefPane")) 42 | } 43 | } 44 | 45 | static func showTranscriptionErrorAlert(error: Error) { 46 | let alert = NSAlert() 47 | alert.messageText = "Transcription Error" 48 | alert.informativeText = "An error occurred during transcription: \(error.localizedDescription)" 49 | alert.alertStyle = .critical 50 | alert.addButton(withTitle: "OK") 51 | alert.runModal() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/256 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/256 1.png -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/32 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/32 1.png -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/512 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/512 1.png -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "16.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "32 1.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "32.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "64.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "128.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "256 1.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "256.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "512 1.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "512.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "1024.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/StatusBarIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "tray-icon-16.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "tray-icon-24.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "tray-icon-32.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/StatusBarIcon.imageset/tray-icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/Assets.xcassets/StatusBarIcon.imageset/tray-icon-16.png -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/StatusBarIcon.imageset/tray-icon-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/Assets.xcassets/StatusBarIcon.imageset/tray-icon-24.png -------------------------------------------------------------------------------- /WhispeAnywhere/Assets.xcassets/StatusBarIcon.imageset/tray-icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/Assets.xcassets/StatusBarIcon.imageset/tray-icon-32.png -------------------------------------------------------------------------------- /WhispeAnywhere/AudioRecorder.swift: -------------------------------------------------------------------------------- 1 | import AVFoundation 2 | import Cocoa 3 | 4 | class AudioRecorder: NSObject, AVCaptureAudioDataOutputSampleBufferDelegate { 5 | private var captureSession: AVCaptureSession? 6 | private var audioOutput: AVCaptureAudioDataOutput? 7 | private var audioWriter: AVAssetWriter? 8 | private var audioWriterInput: AVAssetWriterInput? 9 | private var audioDeviceInput: AVCaptureDeviceInput? 10 | 11 | public private(set) var isRecording = false 12 | private var isWriterReady = false 13 | private var recordingURL: URL? 14 | 15 | private let sessionQueue = DispatchQueue(label: "SessionQueue") 16 | private let writerQueue = DispatchQueue(label: "WriterQueue") 17 | private let writingSemaphore = DispatchSemaphore(value: 0) 18 | 19 | private var debugBufferCount = 0 20 | private var bytesWritten: Int64 = 0 21 | 22 | override init() { 23 | super.init() 24 | Logger.log("AudioRecorder initialized") 25 | setupCaptureSession() 26 | } 27 | 28 | private func setupCaptureSession() { 29 | sessionQueue.async { [weak self] in 30 | guard let self = self else { return } 31 | 32 | Logger.log("Setting up capture session") 33 | self.captureSession = AVCaptureSession() 34 | 35 | guard let audioDevice = AVCaptureDevice.default(for: .audio) else { 36 | Logger.log("No audio device available") 37 | return 38 | } 39 | 40 | do { 41 | self.audioDeviceInput = try AVCaptureDeviceInput(device: audioDevice) 42 | if self.captureSession!.canAddInput(self.audioDeviceInput!) { 43 | self.captureSession!.addInput(self.audioDeviceInput!) 44 | Logger.log("Added audio input: \(audioDevice.localizedName)") 45 | } 46 | } catch { 47 | Logger.log("Failed to set audio input: \(error.localizedDescription)") 48 | return 49 | } 50 | 51 | self.audioOutput = AVCaptureAudioDataOutput() 52 | if let audioOutput = self.audioOutput, self.captureSession!.canAddOutput(audioOutput) { 53 | self.captureSession!.addOutput(audioOutput) 54 | Logger.log("Added audio output") 55 | } 56 | 57 | Logger.log("Capture session setup completed") 58 | } 59 | } 60 | 61 | func startRecording(completion: @escaping (Bool) -> Void) { 62 | sessionQueue.async { [weak self] in 63 | guard let self = self else { return completion(false) } 64 | 65 | if self.isRecording { 66 | Logger.log("Recording already in progress") 67 | return completion(false) 68 | } 69 | 70 | let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] 71 | self.recordingURL = documentsPath.appendingPathComponent("recording_\(Date().timeIntervalSince1970).m4a") 72 | 73 | guard let recordingURL = self.recordingURL else { 74 | Logger.log("Failed to create recording URL") 75 | return completion(false) 76 | } 77 | 78 | Logger.log("Starting recording to file: \(recordingURL.lastPathComponent)") 79 | 80 | do { 81 | self.audioWriter = try AVAssetWriter(url: recordingURL, fileType: .m4a) 82 | 83 | let audioSettings: [String: Any] = [ 84 | AVFormatIDKey: Int(kAudioFormatMPEG4AAC), 85 | AVSampleRateKey: 44100, 86 | AVNumberOfChannelsKey: 1, 87 | AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue, 88 | AVEncoderBitRateKey: 128000 89 | ] 90 | 91 | self.audioWriterInput = AVAssetWriterInput(mediaType: .audio, outputSettings: audioSettings) 92 | self.audioWriterInput?.expectsMediaDataInRealTime = true 93 | 94 | if self.audioWriter!.canAdd(self.audioWriterInput!) { 95 | self.audioWriter!.add(self.audioWriterInput!) 96 | Logger.log("Audio writer input added to asset writer") 97 | } else { 98 | Logger.log("Cannot add audio writer input to asset writer") 99 | return completion(false) 100 | } 101 | 102 | self.audioOutput?.setSampleBufferDelegate(self, queue: self.writerQueue) 103 | 104 | self.audioWriter!.startWriting() 105 | self.captureSession?.startRunning() 106 | self.isRecording = true 107 | self.isWriterReady = false 108 | self.debugBufferCount = 0 109 | self.bytesWritten = 0 110 | 111 | Logger.log("Recording started successfully") 112 | completion(true) 113 | } catch { 114 | Logger.log("Failed to start recording: \(error.localizedDescription)") 115 | completion(false) 116 | } 117 | } 118 | } 119 | 120 | func stopRecording(completion: @escaping (URL?) -> Void) { 121 | sessionQueue.async { [weak self] in 122 | guard let self = self, self.isRecording else { 123 | Logger.log("No active recording to stop") 124 | return completion(nil) 125 | } 126 | 127 | Logger.log("Stopping recording") 128 | self.isRecording = false 129 | self.captureSession?.stopRunning() 130 | 131 | self.writerQueue.async { 132 | self.writingSemaphore.wait() 133 | self.audioWriterInput?.markAsFinished() 134 | self.audioWriter?.finishWriting { [weak self] in 135 | guard let self = self, let recordingURL = self.recordingURL else { 136 | Logger.log("Failed to finish writing audio") 137 | return completion(nil) 138 | } 139 | 140 | Logger.log("Total audio buffers processed: \(self.debugBufferCount)") 141 | Logger.log("Total bytes written: \(self.bytesWritten)") 142 | 143 | do { 144 | let attributes = try FileManager.default.attributesOfItem(atPath: recordingURL.path) 145 | let fileSize = attributes[.size] as? Int ?? 0 146 | 147 | Logger.log("Recorded file size: \(fileSize) bytes") 148 | 149 | if fileSize > 0 { 150 | Logger.log("Recording completed successfully: \(recordingURL.lastPathComponent)") 151 | completion(recordingURL) 152 | } else { 153 | Logger.log("Recording file is empty") 154 | try? FileManager.default.removeItem(at: recordingURL) 155 | completion(nil) 156 | } 157 | } catch { 158 | Logger.log("Failed to verify recording file: \(error.localizedDescription)") 159 | completion(nil) 160 | } 161 | 162 | self.resetRecordingState() 163 | } 164 | } 165 | } 166 | } 167 | 168 | func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { 169 | guard isRecording else { return } 170 | 171 | if !isWriterReady { 172 | let timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer) 173 | audioWriter?.startSession(atSourceTime: timestamp) 174 | isWriterReady = true 175 | writingSemaphore.signal() 176 | Logger.log("Audio writer session started") 177 | } 178 | 179 | guard let audioWriterInput = audioWriterInput, audioWriterInput.isReadyForMoreMediaData else { 180 | Logger.log("AudioWriterInput not ready for more data") 181 | return 182 | } 183 | 184 | if audioWriterInput.append(sampleBuffer) { 185 | debugBufferCount += 1 186 | let totalSampleSize = CMSampleBufferGetTotalSampleSize(sampleBuffer) 187 | bytesWritten += Int64(totalSampleSize) 188 | if debugBufferCount % 1000 == 0 { 189 | Logger.log("Processed \(debugBufferCount) audio buffers, total bytes written: \(bytesWritten)") 190 | } 191 | } else { 192 | Logger.log("Failed to append sample buffer") 193 | if let error = audioWriter?.error { 194 | Logger.log("AudioWriter error: \(error.localizedDescription)") 195 | } 196 | } 197 | } 198 | 199 | private func resetRecordingState() { 200 | Logger.log("Resetting recording state") 201 | audioWriter = nil 202 | audioWriterInput = nil 203 | recordingURL = nil 204 | isWriterReady = false 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /WhispeAnywhere/ClipboardManager.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class ClipboardManager { 4 | static let shared = ClipboardManager() 5 | 6 | private init() {} 7 | 8 | func copyToClipboard(_ text: String) { 9 | let pasteboard = NSPasteboard.general 10 | pasteboard.clearContents() 11 | pasteboard.setString(text, forType: .string) 12 | } 13 | 14 | func insertText(_ text: String) { 15 | // First, copy the text to clipboard 16 | Logger.log("First, copy the text to clipboard") 17 | copyToClipboard(text) 18 | 19 | // Then simulate Command+V to paste 20 | Logger.log("Then simulate Command+V to paste") 21 | let source = CGEventSource(stateID: .hidSystemState) 22 | 23 | let cmdDown = CGEvent(keyboardEventSource: source, virtualKey: 0x37, keyDown: true) 24 | let vDown = CGEvent(keyboardEventSource: source, virtualKey: 0x09, keyDown: true) 25 | let vUp = CGEvent(keyboardEventSource: source, virtualKey: 0x09, keyDown: false) 26 | let cmdUp = CGEvent(keyboardEventSource: source, virtualKey: 0x37, keyDown: false) 27 | 28 | cmdDown?.flags = .maskCommand 29 | vDown?.flags = .maskCommand 30 | vUp?.flags = .maskCommand 31 | 32 | cmdDown?.post(tap: .cgAnnotatedSessionEventTap) 33 | vDown?.post(tap: .cgAnnotatedSessionEventTap) 34 | vUp?.post(tap: .cgAnnotatedSessionEventTap) 35 | cmdUp?.post(tap: .cgAnnotatedSessionEventTap) 36 | Logger.log("Donw with paste") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /WhispeAnywhere/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // WhispeAnywhere 4 | // 5 | // Created by Unclecode on 15/09/2024. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ContentView: View { 11 | var body: some View { 12 | VStack { 13 | Image(systemName: "globe") 14 | .imageScale(.large) 15 | .foregroundStyle(.tint) 16 | Text("Hello, world!") 17 | } 18 | .padding() 19 | } 20 | } 21 | 22 | #Preview { 23 | ContentView() 24 | } 25 | -------------------------------------------------------------------------------- /WhispeAnywhere/GroqAPI.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class GroqAPI { 4 | private let apiKey: String 5 | private let baseURL = "https://api.groq.com/openai/v1" 6 | 7 | init(apiKey: String) { 8 | self.apiKey = apiKey 9 | Logger.log("GroqAPI initialized with API key: \(apiKey)") 10 | } 11 | 12 | func transcribe(audioFileURL: URL, improveGrammar: Bool, completion: @escaping (Result) -> Void) { 13 | Logger.log("Starting transcription for audio file: \(audioFileURL.lastPathComponent)") 14 | 15 | let transcriptionURL = URL(string: baseURL + "/audio/transcriptions")! 16 | var request = URLRequest(url: transcriptionURL) 17 | request.httpMethod = "POST" 18 | request.setValue("Bearer \(self.apiKey)", forHTTPHeaderField: "Authorization") 19 | 20 | let boundary = UUID().uuidString 21 | request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") 22 | 23 | var body = Data() 24 | 25 | // Add file data 26 | body.append("--\(boundary)\r\n".data(using: .utf8)!) 27 | body.append("Content-Disposition: form-data; name=\"file\"; filename=\"audio.m4a\"\r\n".data(using: .utf8)!) 28 | body.append("Content-Type: audio/m4a\r\n\r\n".data(using: .utf8)!) 29 | body.append(try! Data(contentsOf: audioFileURL)) 30 | body.append("\r\n".data(using: .utf8)!) 31 | 32 | // Add model parameter 33 | body.append("--\(boundary)\r\n".data(using: .utf8)!) 34 | body.append("Content-Disposition: form-data; name=\"model\"\r\n\r\n".data(using: .utf8)!) 35 | body.append("distil-whisper-large-v3-en\r\n".data(using: .utf8)!) 36 | 37 | // Add closing boundary 38 | body.append("--\(boundary)--\r\n".data(using: .utf8)!) 39 | 40 | request.httpBody = body 41 | 42 | Logger.log("Sending transcription request to Groq API") 43 | 44 | URLSession.shared.dataTask(with: request) { [weak self] data, response, error in 45 | // Function to remove the audio file 46 | let removeAudioFile = { 47 | do { 48 | try FileManager.default.removeItem(at: audioFileURL) 49 | Logger.log("Audio file removed successfully: \(audioFileURL.lastPathComponent)") 50 | } catch { 51 | Logger.log("Error removing audio file: \(error.localizedDescription)") 52 | } 53 | } 54 | 55 | if let error = error { 56 | Logger.log("Error during API request: \(error.localizedDescription)") 57 | removeAudioFile() 58 | completion(.failure(error)) 59 | return 60 | } 61 | 62 | guard let data = data else { 63 | Logger.log("No data received from API") 64 | removeAudioFile() 65 | completion(.failure(NSError(domain: "GroqAPI", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"]))) 66 | return 67 | } 68 | 69 | do { 70 | if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], 71 | let text = json["text"] as? String { 72 | Logger.log("Transcription successful. Received text of length: \(text.count)") 73 | removeAudioFile() 74 | 75 | if improveGrammar { 76 | self?.improveGrammar(text: text, completion: completion) 77 | } else { 78 | completion(.success(text)) 79 | } 80 | } else { 81 | throw NSError(domain: "GroqAPI", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response format"]) 82 | } 83 | } catch { 84 | Logger.log("Error parsing API response: \(error.localizedDescription)") 85 | removeAudioFile() 86 | completion(.failure(error)) 87 | } 88 | }.resume() 89 | } 90 | 91 | func improveGrammar(text: String, completion: @escaping (Result) -> Void) { 92 | Logger.log("Starting grammar improvement for text of length: \(text.count)") 93 | 94 | let chatCompletionURL = URL(string: baseURL + "/chat/completions")! 95 | var request = URLRequest(url: chatCompletionURL) 96 | request.httpMethod = "POST" 97 | request.setValue("Bearer \(self.apiKey)", forHTTPHeaderField: "Authorization") 98 | request.setValue("application/json", forHTTPHeaderField: "Content-Type") 99 | 100 | let requestBody: [String: Any] = [ 101 | "messages": [ 102 | ["role": "system", "content": "Your task is to improve grammatically for a given text, and return in JSON, following this format:\n\n{\"result\": \"[edited text]\"}"], 103 | ["role": "user", "content": "\nText to edit: \(text)"] 104 | ], 105 | "model": "llama-3.1-8b-instant", 106 | "temperature": 1, 107 | "max_tokens": 1024, 108 | "top_p": 1, 109 | "stream": false, 110 | "response_format": ["type": "json_object"], 111 | "stop": NSNull() 112 | ] 113 | 114 | request.httpBody = try? JSONSerialization.data(withJSONObject: requestBody) 115 | 116 | Logger.log("Sending grammar improvement request to Groq API") 117 | 118 | URLSession.shared.dataTask(with: request) { data, response, error in 119 | if let error = error { 120 | Logger.log("Error during API request: \(error.localizedDescription)") 121 | completion(.failure(error)) 122 | return 123 | } 124 | 125 | guard let data = data else { 126 | Logger.log("No data received from API") 127 | completion(.failure(NSError(domain: "GroqAPI", code: 0, userInfo: [NSLocalizedDescriptionKey: "No data received"]))) 128 | return 129 | } 130 | 131 | do { 132 | if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], 133 | let choices = json["choices"] as? [[String: Any]], 134 | let firstChoice = choices.first, 135 | let message = firstChoice["message"] as? [String: Any], 136 | let content = message["content"] as? String, 137 | let contentData = content.data(using: .utf8), 138 | let contentJson = try JSONSerialization.jsonObject(with: contentData, options: []) as? [String: String], 139 | let improvedText = contentJson["result"] { 140 | Logger.log("Grammar improvement successful. Received improved text of length: \(improvedText.count)") 141 | completion(.success(improvedText)) 142 | } else { 143 | throw NSError(domain: "GroqAPI", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response format"]) 144 | } 145 | } catch { 146 | Logger.log("Error parsing API response: \(error.localizedDescription)") 147 | completion(.failure(error)) 148 | } 149 | }.resume() 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /WhispeAnywhere/HotkeyManager.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import Carbon 3 | 4 | extension String { 5 | var fourCharCodeValue: Int { 6 | var result: Int = 0 7 | if let data = self.data(using: .macOSRoman) { 8 | data.withUnsafeBytes { (rawBytes) in 9 | let bytes = rawBytes.bindMemory(to: UInt8.self) 10 | for i in 0 ..< data.count { 11 | result = result << 8 + Int(bytes[i]) 12 | } 13 | } 14 | } 15 | return result 16 | } 17 | } 18 | 19 | class HotkeyManager: ObservableObject { 20 | private var hotKeyRef: EventHotKeyRef? 21 | private var hotKeyID: EventHotKeyID 22 | private weak var delegate: HotkeyManagerDelegate? 23 | 24 | @Published var currentHotkey: String { 25 | didSet { 26 | print("Hotkey changed to: \(currentHotkey)") 27 | updateHotkey() 28 | } 29 | } 30 | 31 | private static weak var sharedInstance: HotkeyManager? 32 | 33 | init(settingsStore: SettingsStore, delegate: HotkeyManagerDelegate) { 34 | self.delegate = delegate 35 | self.currentHotkey = settingsStore.hotkey 36 | self.hotKeyID = EventHotKeyID(signature: OSType("swat".fourCharCodeValue), id: 1) 37 | 38 | print("Initializing HotkeyManager with hotkey: \(settingsStore.hotkey)") 39 | 40 | HotkeyManager.sharedInstance = self 41 | 42 | // Observe changes in SettingsStore 43 | settingsStore.$hotkey.assign(to: &$currentHotkey) 44 | 45 | updateHotkey() 46 | } 47 | 48 | private func updateHotkey() { 49 | print("Updating hotkey...") 50 | unregisterHotkey() 51 | registerHotkey() 52 | } 53 | 54 | private func registerHotkey() { 55 | let (keyCode, modifiers) = parseHotkeyString(currentHotkey) 56 | let modifierFlags = getCarbonFlagsFromCocoaFlags(cocoaFlags: modifiers) 57 | 58 | print("Registering hotkey with keyCode: \(keyCode), modifiers: \(modifiers)") 59 | 60 | var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: OSType(kEventHotKeyPressed)) 61 | 62 | // Use a static function as the event handler 63 | let status = InstallEventHandler(GetApplicationEventTarget(), Self.eventHandler, 1, &eventType, nil, nil) 64 | if status != noErr { 65 | print("Failed to install event handler. Status: \(status)") 66 | return 67 | } 68 | 69 | let registerStatus = RegisterEventHotKey(UInt32(keyCode), 70 | modifierFlags, 71 | hotKeyID, 72 | GetApplicationEventTarget(), 73 | 0, 74 | &hotKeyRef) 75 | 76 | if registerStatus == noErr { 77 | print("Hotkey registered successfully") 78 | } else { 79 | print("Failed to register hotkey. Status: \(registerStatus)") 80 | } 81 | } 82 | 83 | private func unregisterHotkey() { 84 | if let hotKeyRef = hotKeyRef { 85 | print("Unregistering previous hotkey") 86 | UnregisterEventHotKey(hotKeyRef) 87 | self.hotKeyRef = nil 88 | } 89 | } 90 | 91 | private static let eventHandler: EventHandlerUPP = { (nextHandler, eventRef, userData) -> OSStatus in 92 | guard let eventRef = eventRef else { return noErr } 93 | print("Hotkey event received") 94 | DispatchQueue.main.async { 95 | HotkeyManager.sharedInstance?.delegate?.hotkeyTriggered() 96 | } 97 | return noErr 98 | } 99 | 100 | private func parseHotkeyString(_ hotkeyString: String) -> (keyCode: UInt16, modifiers: NSEvent.ModifierFlags) { 101 | let components = hotkeyString.components(separatedBy: "+") 102 | var modifiers: NSEvent.ModifierFlags = [] 103 | var keyCode: UInt16 = 0 104 | 105 | for component in components { 106 | switch component.lowercased() { 107 | case "cmd", "command": 108 | modifiers.insert(.command) 109 | case "ctrl", "control": 110 | modifiers.insert(.control) 111 | case "alt", "option": 112 | modifiers.insert(.option) 113 | case "shift": 114 | modifiers.insert(.shift) 115 | default: 116 | keyCode = keyCodeForChar(component) 117 | } 118 | } 119 | 120 | return (keyCode, modifiers) 121 | } 122 | 123 | private func keyCodeForChar(_ char: String) -> UInt16 { 124 | switch char.uppercased() { 125 | case "A": return UInt16(kVK_ANSI_A) 126 | case "S": return UInt16(kVK_ANSI_S) 127 | case "D": return UInt16(kVK_ANSI_D) 128 | case "F": return UInt16(kVK_ANSI_F) 129 | case "H": return UInt16(kVK_ANSI_H) 130 | case "G": return UInt16(kVK_ANSI_G) 131 | case "Z": return UInt16(kVK_ANSI_Z) 132 | case "X": return UInt16(kVK_ANSI_X) 133 | case "C": return UInt16(kVK_ANSI_C) 134 | case "V": return UInt16(kVK_ANSI_V) 135 | case "B": return UInt16(kVK_ANSI_B) 136 | case "Q": return UInt16(kVK_ANSI_Q) 137 | case "W": return UInt16(kVK_ANSI_W) 138 | case "E": return UInt16(kVK_ANSI_E) 139 | case "R": return UInt16(kVK_ANSI_R) 140 | case "Y": return UInt16(kVK_ANSI_Y) 141 | case "T": return UInt16(kVK_ANSI_T) 142 | case "1": return UInt16(kVK_ANSI_1) 143 | case "2": return UInt16(kVK_ANSI_2) 144 | case "3": return UInt16(kVK_ANSI_3) 145 | case "4": return UInt16(kVK_ANSI_4) 146 | case "6": return UInt16(kVK_ANSI_6) 147 | case "5": return UInt16(kVK_ANSI_5) 148 | case "=": return UInt16(kVK_ANSI_Equal) 149 | case "9": return UInt16(kVK_ANSI_9) 150 | case "7": return UInt16(kVK_ANSI_7) 151 | case "-": return UInt16(kVK_ANSI_Minus) 152 | case "8": return UInt16(kVK_ANSI_8) 153 | case "0": return UInt16(kVK_ANSI_0) 154 | case "]": return UInt16(kVK_ANSI_RightBracket) 155 | case "O": return UInt16(kVK_ANSI_O) 156 | case "U": return UInt16(kVK_ANSI_U) 157 | case "[": return UInt16(kVK_ANSI_LeftBracket) 158 | case "I": return UInt16(kVK_ANSI_I) 159 | case "P": return UInt16(kVK_ANSI_P) 160 | case "L": return UInt16(kVK_ANSI_L) 161 | case "J": return UInt16(kVK_ANSI_J) 162 | case "'": return UInt16(kVK_ANSI_Quote) 163 | case "K": return UInt16(kVK_ANSI_K) 164 | case ";": return UInt16(kVK_ANSI_Semicolon) 165 | case "\\": return UInt16(kVK_ANSI_Backslash) 166 | case ",": return UInt16(kVK_ANSI_Comma) 167 | case "/": return UInt16(kVK_ANSI_Slash) 168 | case "N": return UInt16(kVK_ANSI_N) 169 | case "M": return UInt16(kVK_ANSI_M) 170 | case ".": return UInt16(kVK_ANSI_Period) 171 | case "`": return UInt16(kVK_ANSI_Grave) 172 | case "Space": return UInt16(kVK_Space) 173 | default: return 0 174 | } 175 | } 176 | 177 | private func getCarbonFlagsFromCocoaFlags(cocoaFlags: NSEvent.ModifierFlags) -> UInt32 { 178 | var carbonFlags: UInt32 = 0 179 | if cocoaFlags.contains(.command) { carbonFlags |= UInt32(cmdKey) } 180 | if cocoaFlags.contains(.option) { carbonFlags |= UInt32(optionKey) } 181 | if cocoaFlags.contains(.control) { carbonFlags |= UInt32(controlKey) } 182 | if cocoaFlags.contains(.shift) { carbonFlags |= UInt32(shiftKey) } 183 | return carbonFlags 184 | } 185 | } 186 | 187 | protocol HotkeyManagerDelegate: AnyObject { 188 | func hotkeyTriggered() 189 | } 190 | -------------------------------------------------------------------------------- /WhispeAnywhere/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSMicrophoneUsageDescription 6 | WhisperAnywhere needs access to your microphone to record audio for transcription. 7 | NSAppleEventsUsageDescription 8 | WhisperAnywhere needs to control other applications to insert transcribed text. 9 | 10 | 11 | -------------------------------------------------------------------------------- /WhispeAnywhere/LogViewerWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class LogViewerWindow: NSWindow { 4 | private var textView: NSTextView! 5 | 6 | init() { 7 | super.init(contentRect: NSRect(x: 100, y: 100, width: 800, height: 600), 8 | styleMask: [.titled, .closable, .miniaturizable, .resizable], 9 | backing: .buffered, 10 | defer: false) 11 | 12 | self.title = "Application Log" 13 | self.minSize = NSSize(width: 400, height: 300) 14 | 15 | setupTextView() 16 | setupToolbar() 17 | 18 | Logger.log("Log viewer window initialized") 19 | } 20 | 21 | private func setupTextView() { 22 | textView = NSTextView(frame: self.contentView!.bounds) 23 | textView.isEditable = false 24 | textView.autoresizingMask = [.width, .height] 25 | 26 | let scrollView = NSScrollView(frame: self.contentView!.bounds) 27 | scrollView.documentView = textView 28 | scrollView.hasVerticalScroller = true 29 | scrollView.autoresizingMask = [.width, .height] 30 | 31 | self.contentView?.addSubview(scrollView) 32 | } 33 | 34 | private func setupToolbar() { 35 | let toolbar = NSToolbar(identifier: "LogViewerToolbar") 36 | toolbar.allowsUserCustomization = false 37 | toolbar.displayMode = .iconAndLabel 38 | toolbar.delegate = self 39 | self.toolbar = toolbar 40 | } 41 | 42 | func updateLogContent() { 43 | if let logContent = Logger.viewLogFile() { 44 | textView.string = logContent 45 | textView.scrollToEndOfDocument(nil) 46 | } else { 47 | textView.string = "Unable to load log content." 48 | } 49 | } 50 | 51 | @objc func clearLog() { 52 | Logger.clearLog() 53 | updateLogContent() 54 | Logger.log("Log cleared") 55 | } 56 | } 57 | 58 | extension LogViewerWindow: NSToolbarDelegate { 59 | func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { 60 | switch itemIdentifier { 61 | case .clearLog: 62 | let item = NSToolbarItem(itemIdentifier: itemIdentifier) 63 | item.label = "Clear Log" 64 | item.paletteLabel = "Clear Log" 65 | item.toolTip = "Clear the log content" 66 | item.image = NSImage(systemSymbolName: "trash", accessibilityDescription: "Clear") 67 | item.target = self 68 | item.action = #selector(clearLog) 69 | return item 70 | default: 71 | return nil 72 | } 73 | } 74 | 75 | func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 76 | return [.flexibleSpace, .clearLog] 77 | } 78 | 79 | func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { 80 | return [.flexibleSpace, .clearLog] 81 | } 82 | } 83 | 84 | extension NSToolbarItem.Identifier { 85 | static let clearLog = NSToolbarItem.Identifier("ClearLog") 86 | } 87 | -------------------------------------------------------------------------------- /WhispeAnywhere/Logger.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Logger { 4 | static var printToConsole = true // Set this to false in release builds if desired 5 | 6 | static func log(_ message: String) { 7 | let dateFormatter = DateFormatter() 8 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 9 | let timestamp = dateFormatter.string(from: Date()) 10 | let logMessage = "\(timestamp): \(message)\n" 11 | 12 | if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { 13 | let logFileURL = documentsDirectory.appendingPathComponent("app.log") 14 | if let fileHandle = try? FileHandle(forWritingTo: logFileURL) { 15 | fileHandle.seekToEndOfFile() 16 | fileHandle.write(logMessage.data(using: .utf8)!) 17 | fileHandle.closeFile() 18 | } else { 19 | try? logMessage.write(to: logFileURL, atomically: true, encoding: .utf8) 20 | } 21 | } 22 | 23 | // Print to console if printToConsole is true 24 | if printToConsole { 25 | print(logMessage) 26 | } 27 | } 28 | 29 | static func clearLog() { 30 | if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { 31 | let logFileURL = documentsDirectory.appendingPathComponent("app.log") 32 | do { 33 | try "".write(to: logFileURL, atomically: true, encoding: .utf8) 34 | log("Log cleared") 35 | } catch { 36 | log("Failed to clear log: \(error.localizedDescription)") 37 | } 38 | } 39 | } 40 | 41 | static func viewLogFile() -> String? { 42 | if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { 43 | let logFileURL = documentsDirectory.appendingPathComponent("app.log") 44 | return try? String(contentsOf: logFileURL, encoding: .utf8) 45 | } 46 | return nil 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /WhispeAnywhere/OverlayWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import SwiftUI 3 | import QuartzCore 4 | 5 | class StatusManager: ObservableObject { 6 | @Published var status: RecordingStatus = .recording 7 | } 8 | 9 | import Cocoa 10 | import QuartzCore 11 | 12 | class OverlayWindow: NSPanel { 13 | private var hostingView: NSHostingView? 14 | private var trackingArea: NSTrackingArea? 15 | 16 | private var statusManager = StatusManager() 17 | 18 | init() { 19 | super.init(contentRect: NSRect(x: 100, y: 100, width: 60, height: 60), 20 | styleMask: [.borderless, .nonactivatingPanel], 21 | backing: .buffered, 22 | defer: false) 23 | 24 | self.level = .floating 25 | self.isFloatingPanel = true 26 | self.isMovableByWindowBackground = false 27 | self.isReleasedWhenClosed = false 28 | self.isOpaque = false 29 | self.backgroundColor = NSColor.clear 30 | self.hasShadow = false 31 | 32 | let contentView = OverlayContentView(statusManager: statusManager) 33 | self.hostingView = NSHostingView(rootView: contentView) 34 | self.contentView = self.hostingView 35 | 36 | setupTrackingArea() 37 | } 38 | 39 | func updateStatus(_ newStatus: RecordingStatus) { 40 | statusManager.status = newStatus 41 | } 42 | 43 | private func setupTrackingArea() { 44 | guard let contentView = self.contentView else { return } 45 | 46 | if let trackingArea = trackingArea { 47 | contentView.removeTrackingArea(trackingArea) 48 | } 49 | 50 | let options: NSTrackingArea.Options = [.activeAlways, .mouseMoved, .inVisibleRect] 51 | trackingArea = NSTrackingArea(rect: contentView.bounds, options: options, owner: self, userInfo: nil) 52 | contentView.addTrackingArea(trackingArea!) 53 | } 54 | 55 | override func mouseMoved(with event: NSEvent) { 56 | // Update the overlay position directly 57 | updatePosition(with: event.locationInWindow) 58 | } 59 | 60 | func updatePosition(with point: NSPoint) { 61 | guard let screenFrame = NSScreen.main?.frame else { return } 62 | 63 | let windowSize = self.frame.size 64 | let newOrigin = NSPoint( 65 | x: min(max(point.x + 10, 0), screenFrame.width - windowSize.width), 66 | y: min(max(point.y + 10, 0), screenFrame.height - windowSize.height) 67 | ) 68 | 69 | // Directly update the frame without animation for smoother movement 70 | self.setFrameOrigin(newOrigin) 71 | } 72 | 73 | override func setFrame(_ frameRect: NSRect, display flag: Bool) { 74 | super.setFrame(frameRect, display: flag) 75 | setupTrackingArea() 76 | } 77 | } 78 | 79 | 80 | struct OverlayContentView: View { 81 | @ObservedObject var statusManager: StatusManager 82 | @State private var isAnimating = false 83 | 84 | var body: some View { 85 | ZStack { 86 | Circle() 87 | .fill(statusManager.status.backgroundColor) 88 | .frame(width: 50, height: 50) 89 | 90 | Text(statusManager.status.emoji) 91 | .font(.system(size: 30)) 92 | .scaleEffect(isAnimating ? 1.2 : 1.0) 93 | } 94 | .frame(width: 60, height: 60) 95 | .onAppear { 96 | withAnimation(Animation.easeInOut(duration: 0.5).repeatForever(autoreverses: true)) { 97 | isAnimating = true 98 | } 99 | } 100 | } 101 | } 102 | 103 | 104 | 105 | enum RecordingStatus: Int { 106 | case recording = 0 107 | case processing = 1 108 | case done = 2 109 | case error = 3 110 | 111 | var emoji: String { 112 | switch self { 113 | case .recording: return "🎙️" 114 | case .processing: return "⏳" 115 | case .done: return "✅" 116 | case .error: return "❌" 117 | } 118 | } 119 | 120 | var backgroundColor: Color { 121 | switch self { 122 | case .recording: return .red.opacity(0.7) 123 | case .processing: return .orange.opacity(0.7) 124 | case .done: return .green.opacity(0.7) 125 | case .error: return .red.opacity(0.7) 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /WhispeAnywhere/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /WhispeAnywhere/SettingsStore.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | class SettingsStore: ObservableObject { 4 | @Published var selectedModel: String { 5 | didSet { UserDefaults.standard.set(selectedModel, forKey: "selectedModel") } 6 | } 7 | @Published var groqAPIKey: String { 8 | didSet { UserDefaults.standard.set(groqAPIKey, forKey: "groqAPIKey") } 9 | } 10 | @Published var hotkey: String { 11 | didSet { UserDefaults.standard.set(hotkey, forKey: "hotkey") } 12 | } 13 | @Published var autoInsert: Bool { 14 | didSet { UserDefaults.standard.set(autoInsert, forKey: "autoInsert") } 15 | } 16 | @Published var showOverlay: Bool { 17 | didSet { UserDefaults.standard.set(showOverlay, forKey: "showOverlay") } 18 | } 19 | @Published var improveGrammar: Bool { 20 | didSet { UserDefaults.standard.set(improveGrammar, forKey: "improveGrammar") } 21 | } 22 | 23 | init() { 24 | self.selectedModel = UserDefaults.standard.string(forKey: "selectedModel") ?? "Groq" 25 | self.groqAPIKey = UserDefaults.standard.string(forKey: "groqAPIKey") ?? "" 26 | self.hotkey = UserDefaults.standard.string(forKey: "hotkey") ?? "Cmd+Shift+K" 27 | self.autoInsert = UserDefaults.standard.bool(forKey: "autoInsert") || true // Default to true 28 | self.showOverlay = UserDefaults.standard.bool(forKey: "showOverlay") || true // Default to true 29 | self.improveGrammar = UserDefaults.standard.bool(forKey: "improveGrammar") || false // Default to false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /WhispeAnywhere/SettingsView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SettingsView: View { 4 | @ObservedObject var settingsStore: SettingsStore 5 | 6 | var body: some View { 7 | TabView { 8 | ModelSettingsView(selectedModel: $settingsStore.selectedModel, groqAPIKey: $settingsStore.groqAPIKey) 9 | .tabItem { 10 | Label("Models", systemImage: "cpu") 11 | } 12 | 13 | HotkeySettingsView(hotkey: $settingsStore.hotkey) 14 | .tabItem { 15 | Label("Hotkey", systemImage: "keyboard") 16 | } 17 | 18 | BehaviorSettingsView(autoInsert: $settingsStore.autoInsert, showOverlay: $settingsStore.showOverlay, improveGrammar: $settingsStore.improveGrammar) 19 | .tabItem { 20 | Label("Behavior", systemImage: "gearshape") 21 | } 22 | } 23 | .frame(width: 375, height: 250) 24 | .padding() 25 | } 26 | } 27 | 28 | struct ModelSettingsView: View { 29 | @Binding var selectedModel: String 30 | @Binding var groqAPIKey: String 31 | 32 | let models = ["Groq", "OpenAI", "Anthropic"] // Add more models as needed 33 | 34 | var body: some View { 35 | Form { 36 | Picker("Select Model", selection: $selectedModel) { 37 | ForEach(models, id: \.self) { 38 | Text($0) 39 | } 40 | } 41 | .pickerStyle(PopUpButtonPickerStyle()) 42 | 43 | if selectedModel == "Groq" { 44 | SecureField("Groq API Key", text: $groqAPIKey) 45 | Text("If not set, the app will use the GROQ_API_KEY environment variable.") 46 | .font(.caption) 47 | .foregroundColor(.secondary) 48 | } 49 | // Add similar sections for other models when selected 50 | } 51 | .padding(10) 52 | } 53 | } 54 | 55 | struct HotkeySettingsView: View { 56 | @Binding var hotkey: String 57 | 58 | var body: some View { 59 | Form { 60 | TextField("Hotkey", text: $hotkey) 61 | Text("Click to record a new hotkey") 62 | .font(.caption) 63 | .foregroundColor(.secondary) 64 | } 65 | .padding(10) 66 | } 67 | } 68 | 69 | struct BehaviorSettingsView: View { 70 | @Binding var autoInsert: Bool 71 | @Binding var showOverlay: Bool 72 | @Binding var improveGrammar: Bool 73 | 74 | var body: some View { 75 | Form { 76 | Toggle("Auto-insert transcribed text", isOn: $autoInsert) 77 | Toggle("Show overlay during recording", isOn: $showOverlay) 78 | Toggle("Improve grammar", isOn: $improveGrammar) 79 | } 80 | .padding(10) 81 | } 82 | } 83 | 84 | struct SettingsView_Previews: PreviewProvider { 85 | static var previews: some View { 86 | SettingsView(settingsStore: SettingsStore()) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /WhispeAnywhere/SpotlightChatView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct SpotlightChatView: View { 4 | @Binding var isVisible: Bool 5 | @State private var inputText = "" 6 | @State private var messages: [ChatMessage] = [] 7 | @State private var isLoading = false 8 | 9 | var body: some View { 10 | VStack { 11 | ScrollView { 12 | LazyVStack(alignment: .leading, spacing: 10) { 13 | ForEach(messages) { message in 14 | ChatMessageView(message: message) 15 | } 16 | } 17 | .padding() 18 | } 19 | 20 | HStack { 21 | TextField("Type your message...", text: $inputText, onCommit: sendMessage) 22 | .textFieldStyle(RoundedBorderTextFieldStyle()) 23 | 24 | Button(action: sendMessage) { 25 | Image(systemName: "paperplane.fill") 26 | } 27 | .disabled(inputText.isEmpty || isLoading) 28 | } 29 | .padding() 30 | } 31 | .frame(width: 600, height: 400) 32 | .background(Color(NSColor.windowBackgroundColor)) 33 | .cornerRadius(20) 34 | .shadow(radius: 10) 35 | } 36 | 37 | private func sendMessage() { 38 | guard !inputText.isEmpty else { return } 39 | let userMessage = ChatMessage(content: inputText, isUser: true) 40 | messages.append(userMessage) 41 | let userInput = inputText 42 | inputText = "" 43 | isLoading = true 44 | 45 | // TODO: Implement API call to language model 46 | // For now, we'll just simulate a response 47 | DispatchQueue.main.asyncAfter(deadline: .now() + 1) { 48 | let aiResponse = ChatMessage(content: "This is a simulated AI response to: \(userInput)", isUser: false) 49 | messages.append(aiResponse) 50 | isLoading = false 51 | } 52 | } 53 | } 54 | 55 | struct ChatMessage: Identifiable { 56 | let id = UUID() 57 | let content: String 58 | let isUser: Bool 59 | } 60 | 61 | struct ChatMessageView: View { 62 | let message: ChatMessage 63 | 64 | var body: some View { 65 | HStack { 66 | if message.isUser { 67 | Spacer() 68 | } 69 | Text(message.content) 70 | .padding() 71 | .background(message.isUser ? Color.blue : Color.gray) 72 | .foregroundColor(.white) 73 | .cornerRadius(10) 74 | if !message.isUser { 75 | Spacer() 76 | } 77 | } 78 | } 79 | } 80 | 81 | struct SpotlightChatView_Previews: PreviewProvider { 82 | static var previews: some View { 83 | SpotlightChatView(isVisible: .constant(true)) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /WhispeAnywhere/StatusBarController.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | class StatusBarController { 4 | private var statusBar: NSStatusBar 5 | private var statusItem: NSStatusItem 6 | private var menu: NSMenu 7 | 8 | var onPreferencesClicked: (() -> Void)? 9 | var onStartStopRecording: (() -> Void)? 10 | var showLogWindow: (() -> Void)? 11 | 12 | init() { 13 | statusBar = NSStatusBar.system 14 | statusItem = statusBar.statusItem(withLength: NSStatusItem.squareLength) 15 | menu = NSMenu() 16 | 17 | setupMenuItems() 18 | setupStatusBarIcon() 19 | 20 | Logger.log("StatusBarController initialized") 21 | } 22 | 23 | private func setupStatusBarIcon() { 24 | if let button = statusItem.button { 25 | if let image = NSImage(named: "StatusBarIcon") { 26 | button.image = image 27 | } else { 28 | Logger.log("StatusBarIcon not found, using default image") 29 | button.title = "🎙️" // Microphone emoji as a fallback 30 | } 31 | } 32 | statusItem.menu = menu 33 | } 34 | 35 | private func setupMenuItems() { 36 | let startStopItem = NSMenuItem(title: "Start/Stop Recording", action: #selector(startStopRecording), keyEquivalent: "") 37 | startStopItem.target = self 38 | 39 | let viewLogItem = NSMenuItem(title: "View Log", action: #selector(viewLog), keyEquivalent: "l") 40 | viewLogItem.target = self 41 | 42 | let preferencesItem = NSMenuItem(title: "Preferences", action: #selector(openPreferences), keyEquivalent: ",") 43 | preferencesItem.target = self 44 | 45 | let quitItem = NSMenuItem(title: "Quit", action: #selector(NSApplication.shared.terminate(_:)), keyEquivalent: "q") 46 | 47 | menu.addItem(startStopItem) 48 | menu.addItem(viewLogItem) 49 | menu.addItem(NSMenuItem.separator()) 50 | menu.addItem(preferencesItem) 51 | menu.addItem(quitItem) 52 | 53 | Logger.log("Menu items set up") 54 | } 55 | 56 | @objc private func startStopRecording() { 57 | Logger.log("Start/Stop Recording menu item clicked") 58 | onStartStopRecording?() 59 | } 60 | 61 | @objc private func viewLog() { 62 | Logger.log("View Log menu item clicked") 63 | showLogWindow?() 64 | } 65 | 66 | @objc private func openPreferences() { 67 | Logger.log("Preferences menu item clicked") 68 | onPreferencesClicked?() 69 | } 70 | 71 | func updateRecordingStatus(isRecording: Bool) { 72 | if let startStopItem = menu.item(at: 0) { 73 | startStopItem.title = isRecording ? "Stop Recording" : "Start Recording" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /WhispeAnywhere/WhispeAnywhere.app/Contents/CodeResources: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/WhispeAnywhere.app/Contents/CodeResources -------------------------------------------------------------------------------- /WhispeAnywhere/WhispeAnywhere.app/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildMachineOSBuild 6 | 23G93 7 | CFBundleDevelopmentRegion 8 | en 9 | CFBundleExecutable 10 | WhispeAnywhere 11 | CFBundleIdentifier 12 | com.unclecode.WhispeAnywhere 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | WhispeAnywhere 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSupportedPlatforms 22 | 23 | MacOSX 24 | 25 | CFBundleVersion 26 | 1 27 | DTCompiler 28 | com.apple.compilers.llvm.clang.1_0 29 | DTPlatformBuild 30 | 31 | DTPlatformName 32 | macosx 33 | DTPlatformVersion 34 | 14.5 35 | DTSDKBuild 36 | 23F73 37 | DTSDKName 38 | macosx14.5 39 | DTXcode 40 | 1540 41 | DTXcodeBuild 42 | 15F31d 43 | LSMinimumSystemVersion 44 | 14.5 45 | NSAppleEventsUsageDescription 46 | WhisperAnywhere needs to control other applications to insert transcribed text. 47 | NSMicrophoneUsageDescription 48 | WhisperAnywhere needs access to your microphone to record audio for transcription. 49 | 50 | 51 | -------------------------------------------------------------------------------- /WhispeAnywhere/WhispeAnywhere.app/Contents/MacOS/WhispeAnywhere: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/WhispeAnywhere.app/Contents/MacOS/WhispeAnywhere -------------------------------------------------------------------------------- /WhispeAnywhere/WhispeAnywhere.app/Contents/PkgInfo: -------------------------------------------------------------------------------- 1 | APPL???? -------------------------------------------------------------------------------- /WhispeAnywhere/WhispeAnywhere.app/Contents/Resources/Assets.car: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unclecode/whisperanywhere/1cbd8afed711fccc0d9ce9553a642e7819e86234/WhispeAnywhere/WhispeAnywhere.app/Contents/Resources/Assets.car -------------------------------------------------------------------------------- /WhispeAnywhere/WhispeAnywhere.app/Contents/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSMicrophoneUsageDescription 6 | WhisperAnywhere needs access to your microphone to record audio for transcription. 7 | NSAppleEventsUsageDescription 8 | WhisperAnywhere needs to control other applications to insert transcribed text. 9 | 10 | 11 | -------------------------------------------------------------------------------- /WhispeAnywhere/WhispeAnywhere.app/Contents/_CodeSignature/CodeResources: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | files 6 | 7 | Resources/Assets.car 8 | 9 | ts49jksDdQ2Gm/uA22TwcadHork= 10 | 11 | Resources/Info.plist 12 | 13 | st7k4A9AWfg4Ajoq1cZyKn/a2aI= 14 | 15 | 16 | files2 17 | 18 | Resources/Assets.car 19 | 20 | hash2 21 | 22 | e7nOvB/do1wLMVMyXh9+8+Gn8Ya5lu7x2e18ZEG+LO4= 23 | 24 | 25 | Resources/Info.plist 26 | 27 | hash2 28 | 29 | x8sPvASG3lTsgVBxOCeTkauo0XxlgLrump+6txjZz7c= 30 | 31 | 32 | 33 | rules 34 | 35 | ^Resources/ 36 | 37 | ^Resources/.*\.lproj/ 38 | 39 | optional 40 | 41 | weight 42 | 1000 43 | 44 | ^Resources/.*\.lproj/locversion.plist$ 45 | 46 | omit 47 | 48 | weight 49 | 1100 50 | 51 | ^Resources/Base\.lproj/ 52 | 53 | weight 54 | 1010 55 | 56 | ^version.plist$ 57 | 58 | 59 | rules2 60 | 61 | .*\.dSYM($|/) 62 | 63 | weight 64 | 11 65 | 66 | ^(.*/)?\.DS_Store$ 67 | 68 | omit 69 | 70 | weight 71 | 2000 72 | 73 | ^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/ 74 | 75 | nested 76 | 77 | weight 78 | 10 79 | 80 | ^.* 81 | 82 | ^Info\.plist$ 83 | 84 | omit 85 | 86 | weight 87 | 20 88 | 89 | ^PkgInfo$ 90 | 91 | omit 92 | 93 | weight 94 | 20 95 | 96 | ^Resources/ 97 | 98 | weight 99 | 20 100 | 101 | ^Resources/.*\.lproj/ 102 | 103 | optional 104 | 105 | weight 106 | 1000 107 | 108 | ^Resources/.*\.lproj/locversion.plist$ 109 | 110 | omit 111 | 112 | weight 113 | 1100 114 | 115 | ^Resources/Base\.lproj/ 116 | 117 | weight 118 | 1010 119 | 120 | ^[^/]+$ 121 | 122 | nested 123 | 124 | weight 125 | 10 126 | 127 | ^embedded\.provisionprofile$ 128 | 129 | weight 130 | 20 131 | 132 | ^version\.plist$ 133 | 134 | weight 135 | 20 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /WhispeAnywhere/WhispeAnywhere.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.device.audio-input 6 | 7 | com.apple.security.device.microphone 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.files.user-selected.read-only 12 | 13 | 14 | -------------------------------------------------------------------------------- /WhispeAnywhere/WhispeAnywhereApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct WhisperAnywhereApp: App { 5 | @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate 6 | 7 | var body: some Scene { 8 | Settings { 9 | EmptyView() 10 | } 11 | } 12 | } 13 | --------------------------------------------------------------------------------