├── WhispeAnywhere
├── WhispeAnywhere.app
│ └── Contents
│ │ ├── PkgInfo
│ │ ├── CodeResources
│ │ ├── MacOS
│ │ └── WhispeAnywhere
│ │ ├── Resources
│ │ ├── Assets.car
│ │ └── Info.plist
│ │ ├── Info.plist
│ │ └── _CodeSignature
│ │ └── CodeResources
├── Assets.xcassets
│ ├── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── 16.png
│ │ ├── 32.png
│ │ ├── 64.png
│ │ ├── 1024.png
│ │ ├── 128.png
│ │ ├── 256.png
│ │ ├── 32 1.png
│ │ ├── 512.png
│ │ ├── 256 1.png
│ │ ├── 512 1.png
│ │ └── Contents.json
│ ├── StatusBarIcon.imageset
│ │ ├── tray-icon-16.png
│ │ ├── tray-icon-24.png
│ │ ├── tray-icon-32.png
│ │ └── Contents.json
│ └── AccentColor.colorset
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── WhispeAnywhereApp.swift
├── ContentView.swift
├── Info.plist
├── WhispeAnywhere.entitlements
├── SettingsStore.swift
├── ClipboardManager.swift
├── Logger.swift
├── AppDelegateHelpers.swift
├── StatusBarController.swift
├── SpotlightChatView.swift
├── SettingsView.swift
├── LogViewerWindow.swift
├── OverlayWindow.swift
├── GroqAPI.swift
├── HotkeyManager.swift
├── AudioRecorder.swift
└── AppDelegate.swift
├── WhispeAnywhere.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── unclecode.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
└── .gitignore
/WhispeAnywhere/WhispeAnywhere.app/Contents/PkgInfo:
--------------------------------------------------------------------------------
1 | APPL????
--------------------------------------------------------------------------------
/WhispeAnywhere/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/WhispeAnywhere/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/16.png
--------------------------------------------------------------------------------
/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/32.png
--------------------------------------------------------------------------------
/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/64.png
--------------------------------------------------------------------------------
/WhispeAnywhere/WhispeAnywhere.app/Contents/CodeResources:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/WhispeAnywhere.app/Contents/CodeResources
--------------------------------------------------------------------------------
/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/128.png
--------------------------------------------------------------------------------
/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/256.png
--------------------------------------------------------------------------------
/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/32 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/32 1.png
--------------------------------------------------------------------------------
/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/512.png
--------------------------------------------------------------------------------
/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/256 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/256 1.png
--------------------------------------------------------------------------------
/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/512 1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/Assets.xcassets/AppIcon.appiconset/512 1.png
--------------------------------------------------------------------------------
/WhispeAnywhere/WhispeAnywhere.app/Contents/MacOS/WhispeAnywhere:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/WhispeAnywhere.app/Contents/MacOS/WhispeAnywhere
--------------------------------------------------------------------------------
/WhispeAnywhere/WhispeAnywhere.app/Contents/Resources/Assets.car:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/WhispeAnywhere.app/Contents/Resources/Assets.car
--------------------------------------------------------------------------------
/WhispeAnywhere/Assets.xcassets/StatusBarIcon.imageset/tray-icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/Assets.xcassets/StatusBarIcon.imageset/tray-icon-16.png
--------------------------------------------------------------------------------
/WhispeAnywhere/Assets.xcassets/StatusBarIcon.imageset/tray-icon-24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/Assets.xcassets/StatusBarIcon.imageset/tray-icon-24.png
--------------------------------------------------------------------------------
/WhispeAnywhere/Assets.xcassets/StatusBarIcon.imageset/tray-icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unclecode/whisperanywhere/HEAD/WhispeAnywhere/Assets.xcassets/StatusBarIcon.imageset/tray-icon-32.png
--------------------------------------------------------------------------------
/WhispeAnywhere.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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.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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/.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/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/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/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/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/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/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/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/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/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/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/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.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 |
--------------------------------------------------------------------------------