Skip to content

Repository files navigation

🎮 ControlDeck

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.

Status

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)

Requirements

  • macOS 15+ (CoreHID virtual gamepad needs 15; the rest is 15-clean)
  • Swift 6 / Xcode 26 toolchain

Build & run

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 only

Xcode app (for the Virtual Pad / signed build) — the project is generated from project.yml via XcodeGen:

xcodegen generate                 # (re)create ControlDeck.xcodeproj
open ControlDeck.xcodeproj

Then 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.

Architecture

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.

Roadmap

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.

Notes

  • Real vendor logos aren't bundled yet — Vendor.swift uses an SF Symbol placeholder and a per-vendor accent color. Drop bitmaps into an asset catalog and swap Vendor.symbol for Image(vendor) later.
  • Exact USB-vs-Bluetooth transport and VID/PID aren't exposed by the GameController framework; that needs an IOKit/hid pass (already linked).

About

Native macOS app to manage game controllers — live input viewer, button remapper, system-wide & virtual-gamepad output, motion/gyro 3D, DualSense lightbar, controller audio, latency & benchmark tools. SwiftUI + GameController/IOKit/CoreHID.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages