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)
}
}
}