import Foundation import Security final class AppSettings: @unchecked Sendable { static let shared = AppSettings() private let defaults = UserDefaults.standard var localBinaryPath: String { get { defaults.string(forKey: "localBinaryPath") ?? "\(NSHomeDirectory())/Library/Application Support/WhisperKey/whisper-cli" } set { defaults.set(newValue, forKey: "localBinaryPath") } } var localModelPath: String { get { defaults.string(forKey: "localModelPath") ?? "\(NSHomeDirectory())/Library/Application Support/WhisperKey/ggml-large-v3-turbo.bin" } set { defaults.set(newValue, forKey: "localModelPath") } } var glossary: String { get { defaults.string(forKey: "glossary") ?? "CampusAgro, PocketBase, Caddy, SvelteKit, Bitrix, Hetzner, Timeweb, OpenAI, Codex, Claude, VLESS, Reality, Hiddify, CNS8000D, RX8000, RX8500, BX8000, BX8500, BX8500S" } set { defaults.set(newValue, forKey: "glossary") } } var removeFillers: Bool { get { defaults.object(forKey: "removeFillers") as? Bool ?? true } set { defaults.set(newValue, forKey: "removeFillers") } } var cloudModel: String { get { defaults.string(forKey: "cloudModel") ?? "gpt-4o-transcribe" } set { defaults.set(newValue, forKey: "cloudModel") } } func apiKey() -> String? { KeychainStore.read(account: "openai-api-key") } func saveAPIKey(_ value: String) throws { try KeychainStore.write(value, account: "openai-api-key") } } enum KeychainStore { private static let service = "com.romanv.WhisperKey" static func write(_ value: String, account: String) throws { let data = Data(value.utf8) let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, ] SecItemDelete(query as CFDictionary) var add = query add[kSecValueData as String] = data let status = SecItemAdd(add as CFDictionary, nil) guard status == errSecSuccess else { throw WhisperKeyError.processFailed("Keychain: \(status)") } } static func read(account: String) -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne, ] var item: CFTypeRef? guard SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess, let data = item as? Data else { return nil } return String(data: data, encoding: .utf8) } }