// xone_macos — macOS app entry point (SwiftUI) // TODO(phase 5): device discovery (IOKit notifications), pairing UI, // status display, preferences, menubar icon. import AppKit import Combine import SwiftUI struct XoneApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate var body: some Scene { WindowGroup { ContentView() } } } final class AppDelegate: NSObject, NSApplicationDelegate { func applicationDidFinishLaunching(_ notification: Notification) { // Present as a regular app (foreground window) even when launched // directly from the build directory instead of an .app bundle. NSApp.setActivationPolicy(.regular) NSApp.activate(ignoringOtherApps: true) } } private struct ControllerSnapshot: Identifiable { let id: String let buttons: UInt16 let guideDown: Bool let gipReady: Bool let inputActive: Bool let triggerLeft: UInt16 let triggerRight: UInt16 let stickLeftX: Int16 let stickLeftY: Int16 let stickRightX: Int16 let stickRightY: Int16 let sequence: UInt32 } struct ContentView: View { @State private var donglePresent = false @State private var session: OpaquePointer? = nil // Radio state polled from the background worker each tick; a change here // is what re-renders the debug section. @State private var radioState = XONE_STATE_IDLE // Connected controllers and their latest input snapshot. @State private var controllers: [ControllerSnapshot] = [] private let timer = Timer.publish(every: 0.02, on: .main, in: .common) .autoconnect() var body: some View { VStack(alignment: .leading, spacing: 16) { HStack(spacing: 8) { Image(systemName: "gamecontroller.fill") .font(.largeTitle) Text("xone_macos") .font(.title) .bold() } statusView if let session { debugView(session) } Divider() controllersView HStack { Button("Pair Controller") { if let s = session { xone_set_pairing(s, true) } } .disabled(radioState != XONE_STATE_READY) Spacer() Text("v\(String(cString: xone_version()))") .foregroundStyle(.secondary) } } .padding(24) .frame(width: 420) .frame(minHeight: 320) .onReceive(timer) { _ in let present = xone_dongle_present() if present && session == nil { // Probe is fast; firmware load + radio init run on a // background thread so the UI never blocks. if let s = xone_open() { session = s xone_start(s, nil) radioState = Int(xone_state(s)) } } else if !present, let s = session { xone_close(s) session = nil } else if let s = session { radioState = Int(xone_state(s)) refreshControllers(s) } donglePresent = present } } private func debugView(_ session: OpaquePointer) -> some View { VStack(alignment: .leading, spacing: 4) { Text("Debug") .font(.headline) debugRow("PID", String(format: "0x%04X", xone_pid(session))) debugRow("Chip ID", String(format: "0x%04X", xone_chip_id(session))) debugRow("MAC", String(cString: xone_mac_address(session))) debugRow("State", stateText(radioState)) if radioState == XONE_STATE_READY { debugRow("Firmware", firmwareBuildText(session)) } else if radioState == XONE_STATE_ERROR { debugRow("Error", String(cString: xone_error(session))) } } } private func debugRow(_ label: String, _ value: String) -> some View { HStack { Text(label) .foregroundStyle(.secondary) Spacer() Text(value) .font(.system(.body, design: .monospaced)) } } private func stateText(_ state: Int) -> String { switch state { case XONE_STATE_IDLE: return "idle" case XONE_STATE_STARTING: return "starting..." case XONE_STATE_READY: return "ready" case XONE_STATE_ERROR: return "error" default: return "unknown" } } private func firmwareBuildText(_ session: OpaquePointer) -> String { let build = String(cString: xone_firmware_build(session)) return build.isEmpty ? "not loaded" : build } // Query the C API for connected controllers and store their MACs. private func refreshControllers(_ session: OpaquePointer) { let count = Int(xone_controller_count(session)) var list: [ControllerSnapshot] = [] for i in 0.. some View { VStack(alignment: .leading, spacing: 8) { HStack { Image(systemName: "gamecontroller") .foregroundStyle(.secondary) Text(controller.id) .font(.system(.body, design: .monospaced)) Spacer() Text(controller.gipReady ? "ready" : "connecting") .foregroundStyle(controller.gipReady ? .green : .orange) } HStack(spacing: 4) { buttonIndicator("Guide", pressed: controller.guideDown) buttonIndicator("Start", pressed: isPressed(controller, 0x0004)) buttonIndicator("Select", pressed: isPressed(controller, 0x0008)) buttonIndicator("A", pressed: isPressed(controller, 0x0010)) buttonIndicator("B", pressed: isPressed(controller, 0x0020)) buttonIndicator("X", pressed: isPressed(controller, 0x0040)) buttonIndicator("Y", pressed: isPressed(controller, 0x0080)) } HStack(spacing: 4) { buttonIndicator("LB", pressed: isPressed(controller, 0x1000)) buttonIndicator("RB", pressed: isPressed(controller, 0x2000)) buttonIndicator("LS", pressed: isPressed(controller, 0x4000)) buttonIndicator("RS", pressed: isPressed(controller, 0x8000)) buttonIndicator("↑", pressed: isPressed(controller, 0x0100)) buttonIndicator("↓", pressed: isPressed(controller, 0x0200)) buttonIndicator("←", pressed: isPressed(controller, 0x0400)) buttonIndicator("→", pressed: isPressed(controller, 0x0800)) } HStack(spacing: 12) { VStack(alignment: .leading, spacing: 4) { Text("Left stick") .foregroundStyle(.secondary) signedAxis("X", controller.stickLeftX) signedAxis("Y", controller.stickLeftY) } VStack(alignment: .leading, spacing: 4) { Text("Right stick") .foregroundStyle(.secondary) signedAxis("X", controller.stickRightX) signedAxis("Y", controller.stickRightY) } } HStack(spacing: 12) { triggerAxis("Left trigger", controller.triggerLeft) triggerAxis("Right trigger", controller.triggerRight) } Text(controller.inputActive ? "Input #\(controller.sequence)" : "Waiting for input") .font(.caption) .foregroundStyle(.secondary) } .padding(10) .background(Color.secondary.opacity(0.08)) .clipShape(RoundedRectangle(cornerRadius: 8)) } private func isPressed(_ controller: ControllerSnapshot, _ mask: UInt16) -> Bool { (controller.buttons & mask) != 0 } private func buttonIndicator(_ title: String, pressed: Bool) -> some View { Text(title) .font(.caption2) .frame(minWidth: 28, minHeight: 22) .padding(.horizontal, 2) .background(pressed ? Color.green : Color.secondary.opacity(0.18)) .foregroundStyle(pressed ? .white : .primary) .clipShape(RoundedRectangle(cornerRadius: 4)) } private func signedAxis(_ label: String, _ value: Int16) -> some View { let normalized = Double(Int(value) + 32768) / 65535.0 return HStack(spacing: 4) { Text(label) .font(.caption2) .frame(width: 12, alignment: .leading) valueBar(normalized, width: 82) Text("\(value)") .font(.caption2.monospacedDigit()) .frame(width: 42, alignment: .trailing) } } private func triggerAxis(_ label: String, _ value: UInt16) -> some View { HStack(spacing: 4) { Text(label) .font(.caption2) .frame(width: 76, alignment: .leading) valueBar(Double(value) / 1023.0, width: 70) Text("\(value)") .font(.caption2.monospacedDigit()) .frame(width: 32, alignment: .trailing) } } private func valueBar(_ fraction: Double, width: CGFloat) -> some View { let clamped = min(max(fraction, 0), 1) return ZStack(alignment: .leading) { Color.secondary.opacity(0.18) .frame(width: width, height: 6) Color.accentColor .frame(width: width * CGFloat(clamped), height: 6) } .frame(width: width, height: 6) .clipShape(RoundedRectangle(cornerRadius: 3)) } } // Entry point: SwiftUI App provides a static main() (top-level code lives in // main.swift, which CMake's Swift driver expects for executables). XoneApp.main()