A native macOS tool to manage game controllers — see every connected pad, watch live input, and (over time) remap, calibrate, test, and manage drivers.
Built with SwiftUI + Apple's GameController framework, so it natively supports DualSense / DualShock, Xbox Wireless, Switch Pro, and other MFi / HID-compatible controllers over both USB and Bluetooth.
Live detection + input viewer + test tools.
- ✅ Auto-detects controllers as they connect / disconnect (USB & Bluetooth)
- ✅ Sidebar list with vendor accent, name, transport icon, and battery
- ✅ Overview — live schematic gamepad: face buttons, D-pad, shoulders, analog triggers, and analog sticks all highlight / move in real time, with raw analog readouts
- ✅ Button Test — every input lights up the first time it fires; tracks progress to "all inputs working"
- ✅ Rumble Test — fire CoreHaptics on any supported locality (left/right handle, triggers) with intensity / sharpness / duration sliders, plus pulse and ramp patterns
- ✅ Deadzone & Calibration — live stick plot (raw vs. deadzone-processed dot, inner/outer rings), adjustable inner deadzone + outer saturation, guided drift and range measurement, and a suggested-deadzone recommendation when resting drift exceeds the deadzone
- ✅ Button Remapper — two modes: In-App (reassign any input to another action or disable it, swap presets, live before/after preview) and System (bind inputs to keyboard/mouse events posted system-wide via CGEvent — works in any app/game, needs Accessibility permission). Profiles persist per controller model, and both modes support per-app overrides that switch automatically with the frontmost app (pick the app in the remapper's "Applies to" menu)
- ✅ Latency & polling — live report-rate (Hz), jitter, interval timeline, and a stimulus→press reaction test, timed on a high-priority queue
- ✅ Benchmark & examination — one-tap timed health check (polling, jitter, button coverage, stick drift) that grades the controller A–D
- ✅ Connection — transport (USB / Bluetooth / BT LE), VID, PID, serial, location ID, and battery, read from the IORegistry via IOKit
- ✅ Battery level + charging state (when the controller reports it)
- ✅ Menu-bar mini-view — every pad with battery/transport, the active profile, and a system-output toggle; the app stays resident when the window closes (Quit lives in the menu-bar panel)
- macOS 15+ (CoreHID virtual gamepad needs 15; the rest is 15-clean)
- Swift 6 / Xcode 26 toolchain
Two ways to build the same sources:
SPM (fast iteration, no signing) — everything works except the Virtual Pad, which needs a signed/entitled build:
swift run # build & launch
swift build # build onlyXcode app (for the Virtual Pad / signed build) — the project is generated
from project.yml via XcodeGen:
xcodegen generate # (re)create ControlDeck.xcodeproj
open ControlDeck.xcodeprojThen in Xcode → Signing & Capabilities, select your Team (Automatic signing).
The app carries the restricted com.apple.developer.hid.virtual.device
entitlement (Support/ControlDeck.entitlements) — it must be signed with your
team or macOS SIGKILLs it at launch. Build & run, approve the Accessibility
prompt, then open the Virtual Pad tab. ControlDeck.xcodeproj is generated —
edit project.yml, not the project.
A controller paired over Bluetooth or plugged in via USB-C appears
automatically. If a wireless pad doesn't show up, wake it (press a button) —
startWirelessControllerDiscovery is already running.
Sources/ControlDeck/
├── App/
│ └── ControlDeckApp.swift @main; AppDelegate forces foreground app policy
├── Services/
│ ├── ControllerManager.swift watches GCController connect/disconnect
│ ├── HapticsController.swift CoreHaptics rumble per locality
│ ├── HIDInfoProvider.swift IOKit registry lookup: transport, VID/PID, …
│ ├── LatencyMonitor.swift high-res report-rate / jitter / reaction timing
│ ├── SystemOutputEngine.swift CGEvent keyboard/mouse output (Accessibility)
│ ├── FrontmostAppMonitor.swift tracks the active app for per-app profiles
│ ├── VirtualPadService.swift CoreHID virtual gamepad (signed builds)
│ ├── AudioService.swift Core Audio device routing + test tone
│ └── RemapStore.swift persists remap profiles per controller model
├── Models/
│ ├── GamepadDevice.swift wraps one GCController, republishes live input
│ ├── InputSnapshot.swift normalized point-in-time input reading
│ ├── MotionSnapshot.swift gyro / accel / attitude reading
│ ├── RemapProfile.swift input → action remap + presets
│ └── Vendor.swift vendor detection → accent color + logo glyph
└── Views/
├── ContentView.swift 3-pane NavigationSplitView: controllers → tools → tool
├── Components.swift Card, FlowLayout, LabeledSlider, tokens
├── DeviceDetailView.swift device header + tool router
├── GamepadView.swift the live-highlighting schematic pad
├── ButtonTestView.swift "does every input register?" checklist
├── HapticsTestView.swift rumble test bench
├── DeadzoneCalibrationView.swift deadzone tuning + drift/range calibration
├── RemapperView.swift in-app remap + system key/mouse output
├── LatencyView.swift polling-rate / jitter + reaction test
├── BenchmarkView.swift timed examination + A–D scorecard
├── MotionView.swift / Motion3DView.swift gyro readout + 3D orientation
├── LightView.swift DualSense lightbar / player-LED color
├── AudioView.swift controller audio device routing
├── VirtualPadView.swift virtual-gamepad toggle + reach matrix
└── ConnectionInfoView.swift transport + hardware identity
The data flow is: ControllerManager (connect/disconnect) → GamepadDevice
(physicalInputProfile.valueDidChangeHandler on a high-priority queue → coalesced
InputSnapshot) → SwiftUI views observe and redraw. GamepadDevice and
ControllerManager use the @Observable macro, so SwiftUI tracks per-property
reads — views that don't read live input don't re-render at the input flush rate.
See FEATURES.md for the full prioritized roadmap (synthesized from a 10-lens feature brainstorm). Highlights:
- Keystone: virtual controller (DriverKit HID) — unlocks applying remaps, macros, gyro-aim, and co-pilot to real games. Today remap/calibration are computed and previewed in-app only.
- Near-term (no keystone needed) — latency/polling-rate tester, per-game profiles + auto-switch + menu-bar, battery-health history & drift trends, DualSense lightbar/adaptive triggers, profile import/export, OBS overlay, emulator config exporter.
- Real vendor logos — bitmap assets in an asset catalog.
- Real vendor logos aren't bundled yet —
Vendor.swiftuses an SF Symbol placeholder and a per-vendor accent color. Drop bitmaps into an asset catalog and swapVendor.symbolforImage(vendor)later. - Exact USB-vs-Bluetooth transport and VID/PID aren't exposed by the
GameController framework; that needs an
IOKit/hidpass (already linked).