Safe Rust bindings for Apple's GameController framework on macOS.
Version 0.10.0 uses binary C records for every snapshot.
The safe snapshot and controller handle APIs stay the same.
The coverage audit counts top-level symbols, not methods. See COVERAGE.md.
[dependencies]
gamecontroller = "0.10"- macOS 12.0 or newer. Slope-feedback triggers report unavailable before macOS 12.3. Racing-wheel queries return an empty list before macOS 13.0.
input_names::LEFT_SIDE_BUTTON/RIGHT_SIDE_BUTTONare reported by macOS 26.4 and newer;input_names::side_buttons_available()checks the running system.- Connection notifications, discovery completion and controller input handlers run on the main queue, so the app needs a running main run loop to receive them.
use gamecontroller::prelude::*;
fn main() -> Result<(), GameControllerError> {
for controller in connected_controller_details()? {
println!(
"{} ({}) current={} buttons={}",
controller.vendor_name,
controller.product_category,
controller.is_current,
controller
.physical_input
.as_ref()
.map_or(0, |profile| profile.buttons.len())
);
if let Some(extended) = &controller.extended_gamepad {
println!(
" A={} B={} LT={:.2} RT={:.2}",
extended.button_a.pressed,
extended.button_b.pressed,
extended.left_trigger.value,
extended.right_trigger.value
);
}
if let Some(input) = &controller.input {
println!(
" live_input buttons={} changed_known={}",
input.live.button_count,
input.next.as_ref().is_some_and(|next| next.changed_aliases_known)
);
}
}
Ok(())
}connected_controllers()for lightweight polling snapshots.connected_controller_details()/current_controller_snapshot()for detailed controller and profile snapshots.current_controller_input_snapshot()forGCController.input/GCControllerLiveInput/GCControllerInputStatecapture data.current_controller_input_source()for directGCDevicePhysicalInputlive/captured/queued-diff snapshots.current_controller_elements()/current_controller_physical_input_elements()for legacyGCControllerElementmetadata plus generic physical-input/source/extents snapshots.connected_devices_snapshot()for genericGCDevicemetadata across controllers, keyboard, mouse, and racing wheels.key_codes,key_names,input_names,haptics_localities, andproduct_categoriesmodules for the auditedGCKeyCode*,GCKey*,GCInput*, haptics-locality, and product-category constants.keyboard_snapshot()/mouse_snapshot()for richerGCKeyboard/GCMousesnapshots.connected_racing_wheels()for macOS-onlyGCRacingWheelmetadata + captured input when already acquired.event_view_controller_snapshot()forGCEventViewControllerdefaults.first_controller_battery()/first_controller_light()/first_controller_haptics()for typed device-side snapshots.ControllerHandle::all()/current()/first()/first_dualsense()return handles to specific controllers withset_light(),set_player_index(),battery_level(),rumble()andset_dualsense_trigger()(a typedDualSenseTriggerEffect).ControllerHandle::extended_gamepad_state()reads all buttons, triggers, thumbsticks and thumbstick buttons of one controller without JSON. Handles are equal, and hash equally, when they refer to the same controller.set_first_controller_light_color()plusset_first_controller_light()forGCColor/GCDeviceLightmutation on the first controller.rumble_first_controller_with_locality()plusDeviceHapticsDetails::supported_locality_constants()for typed haptics locality coverage.watch_connections(),watch_current_controller(),watch_keyboard_connections(),watch_mouse_connections(),watch_mouse_current(),watch_racing_wheel_connections(), andwatch_controller_customizations()for notification coverage.should_monitor_background_events()/set_should_monitor_background_events().start_wireless_controller_discovery(),stop_wireless_controller_discovery(), andasync_api::start_wireless_controller_discovery().dualsense_trigger_*helpers (Apple's feedback, weapon, vibration and slope modes on the firstDualSense) plusdualsense_trigger_feedback_resistive_strengths()/dualsense_trigger_vibration_amplitudes()for raw adaptive-trigger array helpers.
Enable the async feature to await wireless discovery without wiring a callback, or to receive connection, gamepad, keyboard, mouse, motion and micro-gamepad events as bounded async streams:
[dependencies]
gamecontroller = { version = "0.10", features = ["async"] }#[cfg(feature = "async")]
async fn example() -> Result<(), gamecontroller::GameControllerError> {
gamecontroller::async_api::start_wireless_controller_discovery().await?;
Ok(())
}cargo run --example 01_list_controllerscargo run --example 02_poll_statecargo run --example 03_smoke_surfacecargo run --example 04_device_snapshotscargo run --example 05_input_surface
- The Swift bridge imports
GameControllerABI.h. The Rust bridge uses matching#[repr(C)]records. - Strings and arrays use a pointer and a length. Optional fields use nullable pointers. Enum fields use integer tags.
- Swift owns each snapshot arena. Rust copies its fields and releases the arena with
gc_snapshot_free, also after an error. - The raw snapshot functions replace the former JSON functions.
GameControllerError::InvalidBridgeDatareplaces the JSON error variant. - The crate has no serialization dependency. Snapshot fields retain their existing Rust types.
- Wireless-discovery completion callbacks and connection notifications fire on the main run loop.
gamepadsupport is included for completeness even though Apple prefersextendedGamepad.- The crate is snapshot-first. The async streams are the only code that installs
valueChangedHandler,keyChangedHandlerand the mouse handlers. They share one handler per device: several streams can subscribe at once, a handler the app installed earlier keeps being called, and it is restored when the last stream is dropped. Handler-queue mutation and exclusiveGCRacingWheelacquisition are not wrapped. - Dropping a watcher or stream stops its callbacks before
dropreturns; the closure or sender is freed once Swift has released it. GCRemoteis not present in the macOS SDK headers inspected for this release and is documented as skipped inCOVERAGE.md.
Licensed under either of Apache-2.0 or MIT at your option.