1#[cfg(target_os = "macos")]
7pub fn parse_macos_gamepad(usb_stdout: &str, bt_stdout: &str) -> Vec<String> {
8 let mut gamepads = Vec::new();
9 let keywords = [
10 "controller",
11 "gamepad",
12 "joystick",
13 "xbox",
14 "playstation",
15 "dualshock",
16 "dualsense",
17 "nintendo",
18 "joy-con",
19 "joycon",
20 ];
21
22 let is_gamepad = |name: &str| -> bool {
23 let name_lower = name.to_lowercase();
24 keywords.iter().any(|&kw| name_lower.contains(kw))
25 };
26
27 for line in usb_stdout.lines() {
29 let trimmed = line.trim();
30 let indent = line.len() - line.trim_start().len();
31 if (indent == 4 || indent == 6 || indent == 8) && trimmed.ends_with(':') {
32 let name = trimmed.trim_end_matches(':').trim().to_string();
33 if is_gamepad(&name) && !gamepads.contains(&name) {
34 gamepads.push(name);
35 }
36 }
37 }
38
39 let mut current_device = None;
41 for line in bt_stdout.lines() {
42 let trimmed = line.trim();
43 let indent = line.len() - line.trim_start().len();
44 if indent >= 8 && trimmed.ends_with(':') {
45 current_device = Some(trimmed.trim_end_matches(':').trim().to_string());
46 } else if trimmed.starts_with("Connected: Yes") || trimmed.starts_with("Connection: Yes") {
47 if let Some(ref dev) = current_device {
48 if is_gamepad(dev) && !gamepads.contains(dev) {
49 gamepads.push(dev.clone());
50 }
51 }
52 }
53 }
54
55 gamepads
56}
57
58pub(crate) fn detect_gamepad() -> Vec<String> {
59 #[cfg(target_os = "linux")]
60 {
61 let mut gamepads = Vec::new();
62 if let Ok(entries) = std::fs::read_dir("/sys/class/input") {
63 for entry in entries.filter_map(|e| e.ok()) {
64 let name = entry.file_name().to_string_lossy().to_string();
65 if name.starts_with("js") {
66 let path = entry.path().join("device/name");
67 if path.exists() {
68 if let Ok(dev_name) = std::fs::read_to_string(path) {
69 let trimmed = dev_name.trim().to_string();
70 if !trimmed.is_empty() && !gamepads.contains(&trimmed) {
71 gamepads.push(trimmed);
72 }
73 }
74 }
75 }
76 }
77 }
78 gamepads
79 }
80
81 #[cfg(target_os = "macos")]
82 {
83 crate::macos_ffi::get_hid_gamepads()
84 }
85
86 #[cfg(target_os = "windows")]
87 {
88 let cmd = "Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue | Where-Object { $_.Class -eq 'HIDClass' -and ($_.HardwareID -match 'HID_DEVICE_SYSTEM_GAME' -or $_.HardwareID -match 'HID_DEVICE_GAME') -or $_.FriendlyName -match 'Xbox Controller|Gamepad|Joystick' } | Select-Object -ExpandProperty FriendlyName";
89 if let Ok(output) = std::process::Command::new("powershell")
90 .args(["-Command", cmd])
91 .output()
92 {
93 if let Ok(stdout) = String::from_utf8(output.stdout) {
94 let mut gamepads = Vec::new();
95 for line in stdout.lines() {
96 let trimmed = line.trim().to_string();
97 if !trimmed.is_empty() && !gamepads.contains(&trimmed) {
98 gamepads.push(trimmed);
99 }
100 }
101 return gamepads;
102 }
103 }
104 Vec::new()
105 }
106
107 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
108 {
109 Vec::new()
110 }
111}
112
113#[cfg(all(test, target_os = "macos"))]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn test_parse_macos_gamepad() {
119 let usb_sample = "USB 3.1 Bus:\n\n Xbox Wireless Controller:\n\n Product ID: 0x02e0\n Vendor ID: 0x045e\n";
120 let bt_sample = "Bluetooth:\n\n Devices (Connected):\n DualSense Wireless Controller:\n Address: AA-BB-CC\n Connected: Yes\n";
121 let parsed = parse_macos_gamepad(usb_sample, bt_sample);
122 assert_eq!(
123 parsed,
124 vec![
125 "Xbox Wireless Controller".to_string(),
126 "DualSense Wireless Controller".to_string()
127 ]
128 );
129 }
130}