Files
voice-cat/clients/apple/Sources/VoiceCatCore/Enums.swift

156 lines
5.8 KiB
Swift
Raw Permalink Normal View History

feat(apple): VoiceCatCore Swift package + XCFramework build for macOS/iOS clients Lays the groundwork for the macOS (AppKit) and iOS (SwiftUI) clients with a shared Swift core wrapping the C ABI, mirroring the proven Windows VoiceCat.Interop layer. Architecture decision: macOS UI = AppKit (not SwiftUI) for the most mature VoiceOver accessibility story — same rationale as the Windows client's WinForms-over-WinUI-3 decision. iOS stays SwiftUI. Recorded in docs/roadmap.md §2. Build infrastructure (Phase 0): - clients/apple/scripts/build-xcframework.sh: runs cmake --preset apple-dev, merges libvoicecat.a + 107 vcpkg static deps into a single ~30 MB fat static library (libvoicecat-fat.a) via libtool -static (SPM binary targets link one .a per slice), stages voicecat.h + a generated module.modulemap (module VoiceCatC) into the headers, runs xcodebuild -create-xcframework -> clients/apple/VoiceCatCore.xcframework. VoiceCatCore Swift Package (Phase 1): - Package.swift: binary target (VoiceCatCoreXCF) + library (VoiceCatCore) + test target. - Sources/VoiceCatCore/: 7 files mirroring the C# VoiceCat.Interop patterns adapted to Swift native C interop — Enums (9 Swift mirrors of C enums, UInt32-backed), Config, Event (copies ev.text to String inside the callback — the #1 lifetime rule), Models (10 Swift value types), Marshaling (C arrays -> Swift + immediate vc_free_*), Callbacks (@convention(c) + Unmanaged.passUnretained, the Swift analog of C#'s [UnmanagedCallersOnly] + GCHandle), VoiceCatClient (owns vc_client* as OpaquePointer, all 38 C ABI functions, deinit -> vc_client_destroy then frees config CStrings, event delivery on main queue via coalesced DispatchQueue.main drain). Tests — 6/6 green (swift test against a real voicecat-server): - testConnectTofuAuthListChannelsRoundTrips, testAdminChannelCrudAccountCrudRoundTrips, testScreenAudioStreamStartsAndStops, testPerStreamRecvControlsRoundTrip, plus two static smoke tests. Catches Swift-specific interop bugs (@convention(c) callback lifetime, Unmanaged pointer resolution, CString memory management, enum raw-value bridging, struct layout) that C++ ctest cannot. C++ suite still 21/21 green. Docs updated (house rule): tech-stack.md §2, architecture.md §4, roadmap.md M4 + §2, clients/apple/README.md (full rewrite), PROGRESS.md, .gitignore.
2026-06-18 14:20:38 +02:00
// Swift-idiomatic mirrors of the voicecat.h C enums. Keep these in lockstep with
// core/include/voicecat.h values are append-only per the C ABI's house rule, so it's
// safe to add new cases at the end here too, but never renumber/remove existing ones.
//
// Swift imports the C enums directly via `import VoiceCatC` (e.g. VoiceCatC.VC_OK), but
// those case names are C-style (VC_ERR_NOT_IMPLEMENTED, VC_EVENT_SERVER_IDENTITY) these
// mirrors give the Swift UI and tests clean dot-syntax (VoiceCatResult.notImplemented,
// VoiceCatEventType.serverIdentity) and a typed bridge to/from the C values.
//
// NOTE: Swift's Clang importer brings C `typedef enum` types in as UInt32-backed enums
// (all our C enum values are non-negative), so these mirrors use UInt32 raw values too.
// The one signed field in the ABI `vc_event.result` is `int32_t` (not `vc_result`) is
// bridged via `UInt32(bitPattern:)` in Event.swift.
import VoiceCatC
/// Result codes mirrors `vc_result` (voicecat.h). Additive-only: new values go at the end.
public enum VoiceCatResult: UInt32, Sendable, Equatable {
case ok = 0
case notImplemented = 1
case invalidArg = 2
case notConnected = 3
case already = 4
case authFailed = 5
case permissionDenied = 6
case timeout = 7
case io = 8
case protocolError = 9
case crypto = 10
case audio = 11
case internalError = 12
/// Human-readable description from the core (vc_result_string returns a static literal).
public var description: String {
String(cString: vc_result_string(vc_result(rawValue)))
}
/// Bridge from the C enum.
public init(_ cValue: vc_result) { self = VoiceCatResult(rawValue: cValue.rawValue) ?? .internalError }
/// Bridge to the C enum.
public var cValue: vc_result { vc_result(rawValue) }
}
/// Log level mirrors `vc_log_level`.
public enum VoiceCatLogLevel: UInt32, Sendable, Equatable {
case trace = 0
case debug = 1
case info = 2
case warn = 3
case error = 4
case off = 5
public init(_ cValue: vc_log_level) { self = VoiceCatLogLevel(rawValue: cValue.rawValue) ?? .info }
public var cValue: vc_log_level { vc_log_level(rawValue) }
}
/// Connection state mirrors `vc_connection_state`.
public enum VoiceCatConnectionState: UInt32, Sendable, Equatable {
case disconnected = 0
case connecting = 1
case tlsHandshake = 2
case authenticating = 3
case connected = 4
/// M4: handshake succeeded, waiting on `confirmServerIdentity()`.
case verifyingIdentity = 5
public init(_ cValue: vc_connection_state) {
self = VoiceCatConnectionState(rawValue: cValue.rawValue) ?? .disconnected
}
public var cValue: vc_connection_state { vc_connection_state(rawValue) }
}
/// Text message scope mirrors `vc_text_scope`.
public enum VoiceCatTextScope: UInt32, Sendable, Equatable {
case channel = 0
case `private` = 1
case server = 2
public init(_ cValue: vc_text_scope) { self = VoiceCatTextScope(rawValue: cValue.rawValue) ?? .channel }
public var cValue: vc_text_scope { vc_text_scope(rawValue) }
}
/// Audio device kind mirrors `vc_device_kind`.
public enum VoiceCatDeviceKind: UInt32, Sendable, Equatable {
case input = 0
case output = 1
public init(_ cValue: vc_device_kind) { self = VoiceCatDeviceKind(rawValue: cValue.rawValue) ?? .input }
public var cValue: vc_device_kind { vc_device_kind(rawValue) }
}
/// Stream kind mirrors `vc_stream_kind`.
public enum VoiceCatStreamKind: UInt32, Sendable, Equatable {
case mic = 0
/// System/desktop audio (docs/voice.md §9).
case screenAudio = 1
case auxDevice = 2
public init(_ cValue: vc_stream_kind) { self = VoiceCatStreamKind(rawValue: cValue.rawValue) ?? .mic }
public var cValue: vc_stream_kind { vc_stream_kind(rawValue) }
}
/// Send-side input gate mode (docs/voice.md §11) mirrors `vc_input_mode`.
public enum VoiceCatInputMode: UInt32, Sendable, Equatable {
case voiceActivation = 0
case pushToTalk = 1
/// Transmit unconditionally, no VAD gate.
case alwaysOn = 2
public init(_ cValue: vc_input_mode) { self = VoiceCatInputMode(rawValue: cValue.rawValue) ?? .voiceActivation }
public var cValue: vc_input_mode { vc_input_mode(rawValue) }
}
/// Event type mirrors `vc_event_type`. Additive-only.
public enum VoiceCatEventType: UInt32, Sendable, Equatable {
case connectionState = 0
case authResult = 1
case channelList = 2
case userJoined = 3
case userLeft = 4
case userUpdated = 5
case textMessage = 6
case streamStarted = 7
case streamStopped = 8
case talkState = 9
case error = 10
case disconnected = 11
/// M4: reply to `joinChannel()` see `VoiceCatEvent.result` / `.channelId`.
case joinResult = 12
/// M4: the TOFU server-identity gate see `VoiceCatEvent.tofuStatus` / `.text`.
case serverIdentity = 13
/// M5: async result for moderation/admin/channel operations.
case genericResult = 14
/// M5: reply to `requestAccountList()` call `listAccounts()` to read.
case accountList = 15
public init(_ cValue: vc_event_type) {
self = VoiceCatEventType(rawValue: cValue.rawValue) ?? .error
}
public var cValue: vc_event_type { vc_event_type(rawValue) }
}
/// TOFU server-identity classification mirrors `vc_tofu_status`. Pins the TLS leaf
/// certificate's SHA-256 fingerprint (NOT the display-only Ed25519 value see
/// docs/security.md §1.1 and `VoiceCatServerIdentity`).
public enum VoiceCatTofuStatus: UInt32, Sendable, Equatable {
case firstConnect = 0
case matched = 1
case mismatch = 2
public init(_ cValue: vc_tofu_status) {
self = VoiceCatTofuStatus(rawValue: cValue.rawValue) ?? .firstConnect
}
public var cValue: vc_tofu_status { vc_tofu_status(rawValue) }
}