import Foundation import Cocoa public enum RecognitionLanguage: String, CaseIterable { case multi = "multi" case persian = "fa" case english = "en" case persianEnglish = "fa_en" public var displayName: String { switch self { case .multi: return "Multilingual (Persian, English, Arabic)" case .persian: return "Persian Only (فارسی)" case .english: return "English Only" case .persianEnglish: return "Persian + English" } } public var hints: [String] { switch self { case .multi: return ["fa", "en", "ar"] case .persian: return ["fa"] case .english: return ["en"] case .persianEnglish: return ["fa", "en"] } } } public enum TranslationTargetLanguage: String, CaseIterable { case off = "off" case english = "en" case arabic = "ar" case german = "de" case french = "fr" case spanish = "es" case turkish = "tr" case russian = "ru" case chinese = "zh" case japanese = "ja" case italian = "it" case persian = "fa" public var displayName: String { switch self { case .off: return "Off (Original Spoken Language)" case .english: return "🇺🇸 English (انگلیسی)" case .arabic: return "🇸🇦 Arabic (عربی)" case .german: return "🇩🇪 German (آلمانی)" case .french: return "🇫🇷 French (فرانسوی)" case .spanish: return "🇪🇸 Spanish (اسپانیایی)" case .turkish: return "🇹🇷 Turkish (ترکی استانبولی)" case .russian: return "🇷🇺 Russian (روسی)" case .chinese: return "🇨🇳 Chinese (چینی)" case .japanese: return "🇯🇵 Japanese (ژاپنی)" case .italian: return "🇮🇹 Italian (ایتالیایی)" case .persian: return "🇮🇷 Persian (فارسی)" } } public var isEnabled: Bool { return self != .off } } public enum NumberFormatOption: String, CaseIterable { case digits = "digits" case persianDigits = "persianDigits" case words = "words" case raw = "raw" public var displayName: String { switch self { case .digits: return "English Digits (1, 2, 3...)" case .persianDigits: return "Persian Digits (۱، ۲، ۳...)" case .words: return "Persian Words (یک، دو، سه...)" case .raw: return "As Recognized (Raw)" } } } public final class SonioxSettings { public static let shared = SonioxSettings() private let defaults = UserDefaults.standard private init() { ensureHardwareKeyRemapping() } public var language: RecognitionLanguage { get { let val = defaults.string(forKey: "SonioxLanguage") ?? RecognitionLanguage.multi.rawValue return RecognitionLanguage(rawValue: val) ?? .multi } set { defaults.set(newValue.rawValue, forKey: "SonioxLanguage") SonioxSessionPool.shared.reconnectAll() } } public var targetLanguage: TranslationTargetLanguage { get { let val = defaults.string(forKey: "SonioxTargetLanguage") ?? TranslationTargetLanguage.off.rawValue return TranslationTargetLanguage(rawValue: val) ?? .off } set { defaults.set(newValue.rawValue, forKey: "SonioxTargetLanguage") SonioxSessionPool.shared.reconnectAll() } } public var numberFormat: NumberFormatOption { get { let val = defaults.string(forKey: "SonioxNumberFormat") ?? NumberFormatOption.digits.rawValue return NumberFormatOption(rawValue: val) ?? .digits } set { defaults.set(newValue.rawValue, forKey: "SonioxNumberFormat") } } public var voicePunctuation: Bool { get { if defaults.object(forKey: "SonioxVoicePunctuation") == nil { return true } return defaults.bool(forKey: "SonioxVoicePunctuation") } set { defaults.set(newValue, forKey: "SonioxVoicePunctuation") } } public var voiceCommands: Bool { get { if defaults.object(forKey: "SonioxVoiceCommands") == nil { return true } return defaults.bool(forKey: "SonioxVoiceCommands") } set { defaults.set(newValue, forKey: "SonioxVoiceCommands") } } public var domainVocabulary: Bool { get { if defaults.object(forKey: "SonioxDomainVocabulary") == nil { return true } return defaults.bool(forKey: "SonioxDomainVocabulary") } set { defaults.set(newValue, forKey: "SonioxDomainVocabulary") } } public var addTrailingSpace: Bool { get { if defaults.object(forKey: "SonioxAddTrailingSpace") == nil { return true } return defaults.bool(forKey: "SonioxAddTrailingSpace") } set { defaults.set(newValue, forKey: "SonioxAddTrailingSpace") } } public var normalizeText: Bool { get { if defaults.object(forKey: "SonioxNormalizeText") == nil { return true } return defaults.bool(forKey: "SonioxNormalizeText") } set { defaults.set(newValue, forKey: "SonioxNormalizeText") } } public var playSounds: Bool { get { if defaults.object(forKey: "SonioxPlaySounds") == nil { return true } return defaults.bool(forKey: "SonioxPlaySounds") } set { defaults.set(newValue, forKey: "SonioxPlaySounds") } } public var launchAtLogin: Bool { get { return defaults.bool(forKey: "SonioxLaunchAtLogin") } set { defaults.set(newValue, forKey: "SonioxLaunchAtLogin") updateLaunchAtLogin(enabled: newValue) } } public var preferredAudioDeviceID: String? { get { return defaults.string(forKey: "SonioxPreferredAudioDevice") } set { if let val = newValue { defaults.set(val, forKey: "SonioxPreferredAudioDevice") } else { defaults.removeObject(forKey: "SonioxPreferredAudioDevice") } } } public func resetToDefaults() { defaults.removeObject(forKey: "SonioxHotkeyPreset") defaults.removeObject(forKey: "SonioxDictationMode") defaults.removeObject(forKey: "SonioxLanguage") defaults.removeObject(forKey: "SonioxTargetLanguage") defaults.removeObject(forKey: "SonioxNumberFormat") defaults.removeObject(forKey: "SonioxVoicePunctuation") defaults.removeObject(forKey: "SonioxDomainVocabulary") defaults.removeObject(forKey: "SonioxAddTrailingSpace") defaults.removeObject(forKey: "SonioxNormalizeText") defaults.removeObject(forKey: "SonioxPlaySounds") defaults.removeObject(forKey: "SonioxLaunchAtLogin") defaults.removeObject(forKey: "SonioxPreferredAudioDevice") updateLaunchAtLogin(enabled: false) ensureHardwareKeyRemapping() HotkeyManager.shared.registerHotkeys() SonioxSessionPool.shared.reconnectAll() } public func ensureHardwareKeyRemapping() { let script = "/usr/bin/hidutil property --set '{\"UserKeyMapping\":[{\"HIDKeyboardModifierMappingSrc\":3221225679,\"HIDKeyboardModifierMappingDst\":1879048254},{\"HIDKeyboardModifierMappingSrc\":4294967451,\"HIDKeyboardModifierMappingDst\":1879048254}]}' >/dev/null 2>&1" let task = Process() task.executableURL = URL(fileURLWithPath: "/bin/sh") task.arguments = ["-c", script] try? task.run() installRemappingLaunchAgent() } private func installRemappingLaunchAgent() { let launchAgentDir = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/LaunchAgents") let plistURL = launchAgentDir.appendingPathComponent("com.soniox.keymapping.plist") try? FileManager.default.createDirectory(at: launchAgentDir, withIntermediateDirectories: true) let plistContent = """ Label com.soniox.keymapping ProgramArguments /usr/bin/hidutil property --set {"UserKeyMapping":[{"HIDKeyboardModifierMappingSrc":3221225679,"HIDKeyboardModifierMappingDst":1879048254},{"HIDKeyboardModifierMappingSrc":4294967451,"HIDKeyboardModifierMappingDst":1879048254}]} RunAtLoad """ try? plistContent.write(to: plistURL, atomically: true, encoding: .utf8) } private func updateLaunchAtLogin(enabled: Bool) { let launchAgentDir = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/LaunchAgents") let plistURL = launchAgentDir.appendingPathComponent("com.soniox.voice.plist") if enabled { try? FileManager.default.createDirectory(at: launchAgentDir, withIntermediateDirectories: true) let plistContent = """ Label com.soniox.voice ProgramArguments /Applications/Soniox Voice.app/Contents/MacOS/SonioxVoice RunAtLoad KeepAlive """ try? plistContent.write(to: plistURL, atomically: true, encoding: .utf8) } else { try? FileManager.default.removeItem(at: plistURL) } } }