You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
314 lines
11 KiB
314 lines
11 KiB
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 = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>com.soniox.keymapping</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/usr/bin/hidutil</string>
|
|
<string>property</string>
|
|
<string>--set</string>
|
|
<string>{"UserKeyMapping":[{"HIDKeyboardModifierMappingSrc":3221225679,"HIDKeyboardModifierMappingDst":1879048254},{"HIDKeyboardModifierMappingSrc":4294967451,"HIDKeyboardModifierMappingDst":1879048254}]}</string>
|
|
</array>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
</dict>
|
|
</plist>
|
|
"""
|
|
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 = """
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>com.soniox.voice</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>/Applications/Soniox Voice.app/Contents/MacOS/SonioxVoice</string>
|
|
</array>
|
|
<key>RunAtLoad</key>
|
|
<true/>
|
|
<key>KeepAlive</key>
|
|
<false/>
|
|
</dict>
|
|
</plist>
|
|
"""
|
|
try? plistContent.write(to: plistURL, atomically: true, encoding: .utf8)
|
|
} else {
|
|
try? FileManager.default.removeItem(at: plistURL)
|
|
}
|
|
}
|
|
}
|