use std::ffi::{c_void, CString};
use std::ptr;
pub type CFTypeRef = *const c_void;
pub type CFStringRef = *const c_void;
pub type CFMutableDictionaryRef = *mut c_void;
pub type CFDictionaryRef = *const c_void;
pub type CFAllocatorRef = *const c_void;
pub type CFDataRef = *const c_void;
pub type CFBooleanRef = *const c_void;
pub type CFNumberRef = *const c_void;
#[allow(non_upper_case_globals)]
pub const kCFNumberSInt32Type: i32 = 3;
#[allow(non_upper_case_globals)]
pub const kCFStringEncodingUTF8: u32 = 0x08000100;
#[link(name = "CoreFoundation", kind = "framework")]
extern "C" {
pub static kCFAllocatorDefault: CFAllocatorRef;
pub fn CFStringCreateWithCString(
alloc: CFAllocatorRef,
c_str: *const i8,
encoding: u32,
) -> CFStringRef;
pub fn CFStringGetCString(
the_string: CFStringRef,
buffer: *mut i8,
buffer_size: isize,
encoding: u32,
) -> bool;
pub fn CFStringGetLength(the_string: CFStringRef) -> isize;
pub fn CFDataGetBytePtr(the_data: CFDataRef) -> *const u8;
pub fn CFDataGetLength(the_data: CFDataRef) -> isize;
pub fn CFGetTypeID(cf: CFTypeRef) -> usize;
pub fn CFStringGetTypeID() -> usize;
pub fn CFDataGetTypeID() -> usize;
pub fn CFBooleanGetTypeID() -> usize;
pub fn CFNumberGetTypeID() -> usize;
pub fn CFBooleanGetValue(boolean: CFBooleanRef) -> bool;
pub fn CFNumberGetValue(number: CFNumberRef, the_type: i32, value_ptr: *mut c_void) -> bool;
pub fn CFNumberCreate(
alloc: CFAllocatorRef,
the_type: i32,
value_ptr: *const c_void,
) -> CFNumberRef;
pub fn CFDictionarySetValue(the_dict: CFMutableDictionaryRef, key: CFTypeRef, value: CFTypeRef);
pub fn CFRelease(cf: CFTypeRef);
pub static kCFPreferencesAnyApplication: CFStringRef;
pub static kCFPreferencesCurrentUser: CFStringRef;
pub static kCFPreferencesAnyHost: CFStringRef;
pub fn CFPreferencesCopyValue(
key: CFStringRef,
application_id: CFStringRef,
user_name: CFStringRef,
host_name: CFStringRef,
) -> CFTypeRef;
}
pub struct OwnedCF(pub CFTypeRef);
impl Drop for OwnedCF {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { CFRelease(self.0) };
}
}
}
pub unsafe fn cf_string_to_rust(s: CFStringRef) -> Option<String> {
if s.is_null() {
return None;
}
let len = CFStringGetLength(s);
if len <= 0 {
return None;
}
let buf_size = (len * 4 + 1) as usize;
let mut buf = vec![0i8; buf_size];
let ok = CFStringGetCString(
s,
buf.as_mut_ptr(),
buf_size as isize,
kCFStringEncodingUTF8,
);
if !ok {
return None;
}
let end = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
let bytes = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, end) };
String::from_utf8(bytes.to_vec()).ok()
}
pub unsafe fn cf_data_to_string(d: CFDataRef) -> Option<String> {
if d.is_null() {
return None;
}
let len = CFDataGetLength(d) as usize;
if len == 0 {
return None;
}
let ptr = CFDataGetBytePtr(d);
let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
let s = String::from_utf8_lossy(bytes);
let trimmed = s.trim_matches(|c: char| c == '\0' || !c.is_ascii_graphic() && c != ' ');
if trimmed.is_empty() {
None
} else {
Some(trimmed.to_string())
}
}
unsafe fn with_cfstring<F, R>(s: &str, f: F) -> R
where
F: FnOnce(CFStringRef) -> R,
{
let cs = CString::new(s).unwrap_or_default();
let cf = CFStringCreateWithCString(kCFAllocatorDefault, cs.as_ptr(), kCFStringEncodingUTF8);
let result = f(cf);
if !cf.is_null() {
CFRelease(cf as CFTypeRef);
}
result
}
pub type IOService = u32;
pub type IOIterator = u32;
pub const MACH_PORT_NULL: u32 = 0;
const IOKIT_MAIN_PORT: u32 = 0;
#[link(name = "IOKit", kind = "framework")]
extern "C" {
pub fn IOServiceMatching(name: *const i8) -> CFMutableDictionaryRef;
pub fn IOServiceGetMatchingService(main_port: u32, matching: CFDictionaryRef) -> IOService;
pub fn IOServiceGetMatchingServices(
main_port: u32,
matching: CFDictionaryRef,
existing: *mut IOIterator,
) -> i32;
pub fn IOIteratorNext(iter: IOIterator) -> IOService;
pub fn IOObjectRelease(obj: u32) -> i32;
pub fn IORegistryEntryCreateCFProperty(
entry: IOService,
key: CFStringRef,
allocator: CFAllocatorRef,
options: u32,
) -> CFTypeRef;
pub fn IORegistryEntryFromPath(main_port: u32, path: *const i8) -> IOService;
pub fn IORegistryEntryGetChildIterator(
entry: IOService,
plane: *const i8,
iter: *mut IOIterator,
) -> i32;
}
pub unsafe fn iokit_property_as_string(entry: IOService, key: &str) -> Option<String> {
if entry == MACH_PORT_NULL {
return None;
}
let val = with_cfstring(key, |k| {
IORegistryEntryCreateCFProperty(entry, k, kCFAllocatorDefault, 0)
});
if val.is_null() {
return None;
}
let _owned = OwnedCF(val);
let type_id = CFGetTypeID(val);
if type_id == CFStringGetTypeID() {
cf_string_to_rust(val as CFStringRef)
} else if type_id == CFDataGetTypeID() {
cf_data_to_string(val as CFDataRef)
} else {
None
}
}
pub unsafe fn iokit_property_as_u64(entry: IOService, key: &str) -> Option<u64> {
if entry == MACH_PORT_NULL {
return None;
}
let val = with_cfstring(key, |k| {
IORegistryEntryCreateCFProperty(entry, k, kCFAllocatorDefault, 0)
});
if val.is_null() {
return None;
}
let _owned = OwnedCF(val);
let type_id = CFGetTypeID(val);
if type_id == CFDataGetTypeID() {
let len = CFDataGetLength(val as CFDataRef) as usize;
let ptr = CFDataGetBytePtr(val as CFDataRef);
match len {
8 => {
let mut arr = [0u8; 8];
ptr::copy_nonoverlapping(ptr, arr.as_mut_ptr(), 8);
Some(u64::from_le_bytes(arr))
}
4 => {
let mut arr = [0u8; 4];
ptr::copy_nonoverlapping(ptr, arr.as_mut_ptr(), 4);
Some(u32::from_le_bytes(arr) as u64)
}
_ => None,
}
} else if type_id == CFNumberGetTypeID() {
let mut v: i64 = 0;
CFNumberGetValue(
val as CFNumberRef,
4,
&mut v as *mut i64 as *mut c_void,
);
if v > 0 {
Some(v as u64)
} else {
None
}
} else {
None
}
}
pub unsafe fn iokit_property_as_bool(entry: IOService, key: &str) -> Option<bool> {
if entry == MACH_PORT_NULL {
return None;
}
let val = with_cfstring(key, |k| {
IORegistryEntryCreateCFProperty(entry, k, kCFAllocatorDefault, 0)
});
if val.is_null() {
return None;
}
let _owned = OwnedCF(val);
if CFGetTypeID(val) == CFBooleanGetTypeID() {
Some(CFBooleanGetValue(val as CFBooleanRef))
} else {
None
}
}
pub fn get_firmware_version() -> Option<String> {
unsafe {
let path = b"IODeviceTree:/rom\0";
let entry = IORegistryEntryFromPath(IOKIT_MAIN_PORT, path.as_ptr() as *const i8);
if entry == MACH_PORT_NULL {
return None;
}
let result = iokit_property_as_string(entry, "version");
IOObjectRelease(entry);
result
}
}
pub fn get_gpus() -> Vec<(String, Option<u64>)> {
let mut gpus = Vec::new();
unsafe {
let agx_name = CString::new("AGXAccelerator").unwrap();
let matching = IOServiceMatching(agx_name.as_ptr());
if !matching.is_null() {
let mut iter: IOIterator = MACH_PORT_NULL;
if IOServiceGetMatchingServices(IOKIT_MAIN_PORT, matching as CFDictionaryRef, &mut iter)
== 0
{
loop {
let service = IOIteratorNext(iter);
if service == MACH_PORT_NULL {
break;
}
if let Some(name) = iokit_property_as_string(service, "model") {
gpus.push((name, None)); }
IOObjectRelease(service);
}
IOObjectRelease(iter);
}
}
let pci_name = CString::new("IOPCIDevice").unwrap();
let matching = IOServiceMatching(pci_name.as_ptr());
if !matching.is_null() {
let mut iter: IOIterator = MACH_PORT_NULL;
if IOServiceGetMatchingServices(IOKIT_MAIN_PORT, matching as CFDictionaryRef, &mut iter)
== 0
{
loop {
let service = IOIteratorNext(iter);
if service == MACH_PORT_NULL {
break;
}
let is_display = with_cfstring("class-code", |k| {
let v = IORegistryEntryCreateCFProperty(service, k, kCFAllocatorDefault, 0);
if v.is_null() {
return false;
}
let _owned = OwnedCF(v);
if CFGetTypeID(v) != CFDataGetTypeID() {
return false;
}
let len = CFDataGetLength(v as CFDataRef);
if len < 1 {
return false;
}
let ptr = CFDataGetBytePtr(v as CFDataRef);
*ptr.add((len - 1) as usize) == 0x03
});
if is_display {
if let Some(name) = iokit_property_as_string(service, "model") {
let vram =
iokit_property_as_u64(service, "VRAM,totalsize").or_else(|| {
iokit_property_as_u64(service, "VRAM,totalMB")
.map(|mb| mb * 1024 * 1024)
});
gpus.push((name, vram));
}
}
IOObjectRelease(service);
}
IOObjectRelease(iter);
}
}
}
gpus
}
#[repr(C)]
struct AudioObjectPropertyAddress {
selector: u32,
scope: u32,
element: u32,
}
const K_AUDIO_OBJECT_SYSTEM_OBJECT: u32 = 1;
const K_AUDIO_HARDWARE_PROPERTY_DEVICES: u32 = 0x6465_7623; const K_AUDIO_OBJECT_PROPERTY_NAME: u32 = 0x6c6e_616d; const K_AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL: u32 = 0x676c_6f62; const K_AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN: u32 = 0;
#[link(name = "CoreAudio", kind = "framework")]
extern "C" {
fn AudioObjectGetPropertyDataSize(
object_id: u32,
address: *const AudioObjectPropertyAddress,
qualifier_data_size: u32,
qualifier_data: *const c_void,
out_data_size: *mut u32,
) -> i32;
fn AudioObjectGetPropertyData(
object_id: u32,
address: *const AudioObjectPropertyAddress,
qualifier_data_size: u32,
qualifier_data: *const c_void,
io_data_size: *mut u32,
out_data: *mut c_void,
) -> i32;
}
pub fn get_audio_device_names() -> Vec<String> {
let mut names = Vec::new();
unsafe {
let devices_addr = AudioObjectPropertyAddress {
selector: K_AUDIO_HARDWARE_PROPERTY_DEVICES,
scope: K_AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
element: K_AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN,
};
let mut data_size: u32 = 0;
if AudioObjectGetPropertyDataSize(
K_AUDIO_OBJECT_SYSTEM_OBJECT,
&devices_addr,
0,
ptr::null(),
&mut data_size,
) != 0
|| data_size == 0
{
return names;
}
let count = (data_size / 4) as usize; let mut device_ids = vec![0u32; count];
if AudioObjectGetPropertyData(
K_AUDIO_OBJECT_SYSTEM_OBJECT,
&devices_addr,
0,
ptr::null(),
&mut data_size,
device_ids.as_mut_ptr() as *mut c_void,
) != 0
{
return names;
}
let name_addr = AudioObjectPropertyAddress {
selector: K_AUDIO_OBJECT_PROPERTY_NAME,
scope: K_AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
element: K_AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN,
};
for &device_id in &device_ids {
let mut name_ref: CFStringRef = ptr::null();
let mut name_size = std::mem::size_of::<CFStringRef>() as u32;
if AudioObjectGetPropertyData(
device_id,
&name_addr,
0,
ptr::null(),
&mut name_size,
&mut name_ref as *mut CFStringRef as *mut c_void,
) == 0
&& !name_ref.is_null()
{
if let Some(name) = cf_string_to_rust(name_ref) {
if !name.is_empty() && !names.contains(&name) {
names.push(name);
}
}
CFRelease(name_ref as CFTypeRef);
}
}
}
names
}
#[link(name = "CoreGraphics", kind = "framework")]
extern "C" {
fn CGGetActiveDisplayList(
max_displays: u32,
active_displays: *mut u32,
display_count: *mut u32,
) -> i32;
fn CGDisplayPixelsWide(display: u32) -> usize;
fn CGDisplayPixelsHigh(display: u32) -> usize;
fn CGDisplayCopyDisplayMode(display: u32) -> *mut c_void;
fn CGDisplayModeGetRefreshRate(mode: *mut c_void) -> f64;
fn CGDisplayModeRelease(mode: *mut c_void);
fn CGDisplayVendorNumber(display: u32) -> u32;
fn CGDisplayModelNumber(display: u32) -> u32;
}
fn get_active_displays() -> Vec<(usize, usize, f64, u32, u32)> {
let mut displays = Vec::new();
unsafe {
let mut count: u32 = 0;
if CGGetActiveDisplayList(0, ptr::null_mut(), &mut count) != 0 || count == 0 {
return displays;
}
let mut ids = vec![0u32; count as usize];
if CGGetActiveDisplayList(count, ids.as_mut_ptr(), &mut count) != 0 {
return displays;
}
for id in ids {
let w = CGDisplayPixelsWide(id);
let h = CGDisplayPixelsHigh(id);
let mode = CGDisplayCopyDisplayMode(id);
let refresh = if mode.is_null() {
0.0
} else {
let r = CGDisplayModeGetRefreshRate(mode);
CGDisplayModeRelease(mode);
r
};
let vendor = CGDisplayVendorNumber(id);
let model = CGDisplayModelNumber(id);
displays.push((w, h, refresh, vendor, model));
}
}
displays
}
unsafe fn iokit_display_name(vendor: u32, model: u32) -> Option<String> {
let matching_name = CString::new("IODisplayConnect").unwrap();
let matching = IOServiceMatching(matching_name.as_ptr());
if matching.is_null() {
return None;
}
let mut iter: IOIterator = MACH_PORT_NULL;
if IOServiceGetMatchingServices(IOKIT_MAIN_PORT, matching as CFDictionaryRef, &mut iter) != 0 {
return None;
}
let mut result = None;
loop {
let service = IOIteratorNext(iter);
if service == MACH_PORT_NULL {
break;
}
let v = iokit_property_as_u64(service, "DisplayVendorID").unwrap_or(0) as u32;
let m = iokit_property_as_u64(service, "DisplayProductID").unwrap_or(0) as u32;
if v == vendor && m == model {
result = iokit_property_as_string(service, "DisplayProductName");
IOObjectRelease(service);
break;
}
IOObjectRelease(service);
}
IOObjectRelease(iter);
result
}
pub fn get_displays() -> Vec<String> {
let raw = get_active_displays();
raw.into_iter()
.enumerate()
.map(|(i, (w, h, refresh, vendor, model))| {
let name = unsafe { iokit_display_name(vendor, model) }
.unwrap_or_else(|| format!("Display {}", i + 1));
let res = format!("{}x{}", w, h);
if refresh > 0.0 {
let rr = crate::display::format_refresh_rate(refresh);
format!("{} ({} @ {}Hz)", name, res, rr)
} else {
format!("{} ({})", name, res)
}
})
.collect()
}
const USB_VIDEO_CLASS: u64 = 0x0E;
pub fn get_usb_cameras() -> Vec<String> {
let mut cameras = Vec::new();
unsafe {
let usb_name = CString::new("IOUSBDevice").unwrap();
let matching = IOServiceMatching(usb_name.as_ptr());
if matching.is_null() {
return cameras;
}
let mut iter: IOIterator = MACH_PORT_NULL;
if IOServiceGetMatchingServices(IOKIT_MAIN_PORT, matching as CFDictionaryRef, &mut iter)
!= 0
{
return cameras;
}
loop {
let service = IOIteratorNext(iter);
if service == MACH_PORT_NULL {
break;
}
let has_video = has_video_interface(service);
if has_video {
if let Some(name) = iokit_property_as_string(service, "USB Product Name") {
if !name.is_empty() && !cameras.contains(&name) {
cameras.push(name);
}
}
}
IOObjectRelease(service);
}
IOObjectRelease(iter);
}
cameras
}
unsafe fn has_video_interface(device: IOService) -> bool {
let mut child_iter: IOIterator = MACH_PORT_NULL;
let plane = b"IOService\0";
if IORegistryEntryGetChildIterator(device, plane.as_ptr() as *const i8, &mut child_iter) != 0 {
return false;
}
let mut found = false;
loop {
let child = IOIteratorNext(child_iter);
if child == MACH_PORT_NULL {
break;
}
if let Some(class) = iokit_property_as_u64(child, "bInterfaceClass") {
if class == USB_VIDEO_CLASS {
found = true;
IOObjectRelease(child);
break;
}
}
IOObjectRelease(child);
}
IOObjectRelease(child_iter);
found
}
pub fn get_hid_gamepads() -> Vec<String> {
let mut gamepads = Vec::new();
for usage in [0x04u32, 0x05u32] {
let mut found = unsafe { enumerate_hid_usage(0x01, usage) };
for name in found.drain(..) {
if !gamepads.contains(&name) {
gamepads.push(name);
}
}
}
gamepads
}
unsafe fn enumerate_hid_usage(page: u32, usage: u32) -> Vec<String> {
let mut results = Vec::new();
let hid_name = CString::new("IOHIDDevice").unwrap();
let matching = IOServiceMatching(hid_name.as_ptr());
if matching.is_null() {
return results;
}
let page_key = CString::new("DeviceUsagePage").unwrap();
let usage_key = CString::new("DeviceUsage").unwrap();
let page_cf_key = CFStringCreateWithCString(
kCFAllocatorDefault,
page_key.as_ptr(),
kCFStringEncodingUTF8,
);
let usage_cf_key = CFStringCreateWithCString(
kCFAllocatorDefault,
usage_key.as_ptr(),
kCFStringEncodingUTF8,
);
let page_val = CFNumberCreate(
kCFAllocatorDefault,
kCFNumberSInt32Type,
&(page as i32) as *const i32 as *const c_void,
);
let usage_val = CFNumberCreate(
kCFAllocatorDefault,
kCFNumberSInt32Type,
&(usage as i32) as *const i32 as *const c_void,
);
CFDictionarySetValue(matching, page_cf_key as CFTypeRef, page_val as CFTypeRef);
CFDictionarySetValue(matching, usage_cf_key as CFTypeRef, usage_val as CFTypeRef);
CFRelease(page_cf_key as CFTypeRef);
CFRelease(usage_cf_key as CFTypeRef);
CFRelease(page_val as CFTypeRef);
CFRelease(usage_val as CFTypeRef);
let mut iter: IOIterator = MACH_PORT_NULL;
if IOServiceGetMatchingServices(IOKIT_MAIN_PORT, matching as CFDictionaryRef, &mut iter) != 0 {
return results;
}
loop {
let service = IOIteratorNext(iter);
if service == MACH_PORT_NULL {
break;
}
if let Some(name) = iokit_property_as_string(service, "Product") {
if !name.is_empty() {
results.push(name);
}
}
IOObjectRelease(service);
}
IOObjectRelease(iter);
results
}
pub fn get_bluetooth_state() -> Option<(bool, Option<String>)> {
unsafe {
let svc_name = CString::new("IOBluetoothHCIController").unwrap();
let matching = IOServiceMatching(svc_name.as_ptr());
if matching.is_null() {
return None;
}
let service = IOServiceGetMatchingService(IOKIT_MAIN_PORT, matching as CFDictionaryRef);
if service == MACH_PORT_NULL {
return None;
}
let power =
iokit_property_as_bool(service, "BluetoothControllerPowerIsOn").unwrap_or(false);
let chipset = iokit_property_as_string(service, "HardwareTransportCurrentSetting")
.or_else(|| iokit_property_as_string(service, "ProductName"))
.or_else(|| iokit_property_as_string(service, "ChipsetString"));
IOObjectRelease(service);
Some((power, chipset))
}
}
pub struct MacBatteryRaw {
pub current_mah: Option<u64>,
pub max_mah: Option<u64>,
pub raw_max_mah: Option<u64>,
pub design_mah: Option<u64>,
pub is_charging: bool,
pub fully_charged: bool,
pub time_remaining_mins: Option<u64>,
pub vendor: Option<String>,
pub model: Option<String>,
}
pub fn get_battery_raw() -> Option<MacBatteryRaw> {
unsafe {
let svc_name = CString::new("AppleSmartBattery").unwrap();
let matching = IOServiceMatching(svc_name.as_ptr());
if matching.is_null() {
return None;
}
let service = IOServiceGetMatchingService(IOKIT_MAIN_PORT, matching as CFDictionaryRef);
if service == MACH_PORT_NULL {
return None;
}
let current_mah = iokit_property_as_u64(service, "CurrentCapacity");
let max_mah = iokit_property_as_u64(service, "MaxCapacity");
let raw_max_mah = iokit_property_as_u64(service, "AppleRawMaxCapacity");
let design_mah = iokit_property_as_u64(service, "DesignCapacity");
let is_charging = iokit_property_as_bool(service, "IsCharging").unwrap_or(false);
let fully_charged = iokit_property_as_bool(service, "FullyCharged").unwrap_or(false);
let time_remaining_mins =
iokit_property_as_u64(service, "TimeRemaining").filter(|&m| m < 65535);
let vendor = iokit_property_as_string(service, "Manufacturer");
let model = iokit_property_as_string(service, "DeviceName");
IOObjectRelease(service);
if current_mah.is_none() && max_mah.is_none() {
return None;
}
Some(MacBatteryRaw {
current_mah,
max_mah,
raw_max_mah,
design_mah,
is_charging,
fully_charged,
time_remaining_mins,
vendor,
model,
})
}
}
pub fn get_macos_appearance() -> Option<String> {
unsafe {
with_cfstring("AppleInterfaceStyle", |key| {
let val = CFPreferencesCopyValue(
key,
kCFPreferencesAnyApplication,
kCFPreferencesCurrentUser,
kCFPreferencesAnyHost,
);
if val.is_null() {
return None;
}
let _owned = OwnedCF(val);
if CFGetTypeID(val) == CFStringGetTypeID() {
cf_string_to_rust(val as CFStringRef)
} else {
None
}
})
}
}
#[link(name = "CoreWLAN", kind = "framework")]
extern "C" {}
#[link(name = "objc")]
extern "C" {
fn objc_getClass(name: *const i8) -> *mut c_void;
fn sel_registerName(str: *const i8) -> *mut c_void;
fn objc_msgSend(self_: *mut c_void, op: *mut c_void, ...) -> *mut c_void;
}
#[allow(clashing_extern_declarations)]
extern "C" {
#[link_name = "objc_msgSend"]
fn objc_msgSend_f64(self_: *mut c_void, op: *mut c_void) -> f64;
}
pub fn get_wifi_info() -> Option<(String, Option<u64>)> {
unsafe {
let cls_name = CString::new("CWWiFiClient").unwrap();
let cls = objc_getClass(cls_name.as_ptr());
if cls.is_null() {
return None;
}
let shared_sel = sel_registerName(CString::new("sharedWiFiClient").unwrap().as_ptr());
let client = objc_msgSend(cls, shared_sel);
if client.is_null() {
return None;
}
let iface_sel = sel_registerName(CString::new("interface").unwrap().as_ptr());
let iface = objc_msgSend(client, iface_sel);
if iface.is_null() {
return None;
}
let rate_sel = sel_registerName(CString::new("transmitRate").unwrap().as_ptr());
let rate_f = objc_msgSend_f64(iface, rate_sel);
let rate = if rate_f > 0.0 {
Some(rate_f as u64)
} else {
None
};
let ssid_sel = sel_registerName(CString::new("ssid").unwrap().as_ptr());
let ssid_ns = objc_msgSend(iface, ssid_sel) as CFStringRef;
if let Some(ssid) = cf_string_to_rust(ssid_ns) {
return Some((ssid, rate));
}
let iface_name_sel = sel_registerName(CString::new("interfaceName").unwrap().as_ptr());
let iface_name_ns = objc_msgSend(iface, iface_name_sel) as CFStringRef;
let iface_bsd = cf_string_to_rust(iface_name_ns).unwrap_or_else(|| "en0".to_string());
if let Ok(out) = std::process::Command::new("/usr/sbin/ipconfig")
.args(["getsummary", &iface_bsd])
.output()
{
let text = String::from_utf8_lossy(&out.stdout);
if let Some(ssid) = parse_ipconfig_ssid(&text) {
return Some((ssid, rate));
}
}
rate.map(|r| ("Connected".to_string(), Some(r)))
}
}
pub fn parse_ipconfig_ssid(output: &str) -> Option<String> {
output
.lines()
.find_map(|l| {
l.trim()
.strip_prefix("SSID :")
.map(|s| s.trim().to_string())
})
.filter(|s| !s.is_empty() && s != "<redacted>")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_ipconfig_ssid() {
let normal = " SSID : MyNetwork\n BSSID : aa:bb:cc:dd:ee:ff\n";
assert_eq!(parse_ipconfig_ssid(normal), Some("MyNetwork".to_string()));
let redacted = " BSSID : <redacted>\n SSID : <redacted>\n";
assert_eq!(parse_ipconfig_ssid(redacted), None);
let spaces = " SSID : My Home Network\n";
assert_eq!(
parse_ipconfig_ssid(spaces),
Some("My Home Network".to_string())
);
let empty_value = " SSID : \n";
assert_eq!(parse_ipconfig_ssid(empty_value), None);
let no_ssid = " BSSID : aa:bb:cc:dd:ee:ff\n Channel : 149\n";
assert_eq!(parse_ipconfig_ssid(no_ssid), None);
}
}