use objc::runtime::Object;
#[allow(unused_imports)]
use objc::{msg_send, sel, sel_impl};
use serde::{Deserialize, Serialize};
fn objc_class(name: &str) -> *const objc::runtime::Class {
use std::ffi::CString;
let c = CString::new(name).unwrap_or_default();
unsafe { objc::runtime::objc_getClass(c.as_ptr()) }
}
fn ns_string_to_rust(ns: *mut Object) -> String {
if ns.is_null() {
return String::new();
}
let utf8: *const u8 = unsafe { msg_send![ns, UTF8String] };
if utf8.is_null() {
return String::new();
}
unsafe {
std::ffi::CStr::from_ptr(utf8 as *const std::ffi::c_char)
.to_string_lossy()
.into_owned()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemContext {
pub battery_level: Option<f64>,
pub battery_charging: Option<bool>,
pub power_source: Option<String>,
pub dark_mode: bool,
pub screen_width: f64,
pub screen_height: f64,
pub screen_scale: f64,
pub system_volume: Option<f32>,
pub output_muted: Option<bool>,
pub locale: String,
pub language: String,
pub timezone: String,
pub timezone_offset_secs: i64,
pub macos_version: String,
pub hostname: String,
pub username: String,
pub uptime_secs: f64,
pub physical_memory_gb: f64,
pub wifi_enabled: Option<bool>,
pub wifi_ssid: Option<String>,
pub active_interfaces: Vec<NetworkInterface>,
pub keyboard_layout: Option<String>,
pub frontmost_app: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkInterface {
pub name: String,
pub ipv4: Option<String>,
}
#[must_use]
pub fn collect_system_context() -> SystemContext {
SystemContext {
battery_level: battery_level(),
battery_charging: battery_charging(),
power_source: power_source(),
dark_mode: is_dark_mode(),
screen_width: screen_width(),
screen_height: screen_height(),
screen_scale: screen_scale(),
system_volume: system_volume(),
output_muted: output_muted(),
locale: current_locale(),
language: current_language(),
timezone: current_timezone(),
timezone_offset_secs: timezone_offset(),
macos_version: macos_version(),
hostname: hostname(),
username: username(),
uptime_secs: system_uptime(),
physical_memory_gb: physical_memory_gb(),
wifi_enabled: wifi_enabled(),
wifi_ssid: wifi_ssid(),
active_interfaces: active_network_interfaces(),
keyboard_layout: keyboard_layout(),
frontmost_app: frontmost_app_name(),
}
}
fn battery_level() -> Option<f64> {
let output = std::process::Command::new("pmset")
.args(["-g", "batt"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() {
if let Some(pct_pos) = line.find('%') {
let before = &line[..pct_pos];
let num_start = before
.rfind(|c: char| !c.is_ascii_digit())
.map_or(0, |i| i + 1);
if let Ok(pct) = before[num_start..].parse::<f64>() {
return Some(pct);
}
}
}
None
}
fn battery_charging() -> Option<bool> {
let output = std::process::Command::new("pmset")
.args(["-g", "batt"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout);
if text.contains("charging") && !text.contains("discharging") {
Some(true)
} else if text.contains("discharging") || text.contains("charged") || text.contains("AC Power")
{
Some(false) } else {
None
}
}
fn power_source() -> Option<String> {
let output = std::process::Command::new("pmset")
.args(["-g", "batt"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout);
if text.contains("AC Power") {
Some("AC".to_string())
} else if text.contains("Battery Power") {
Some("Battery".to_string())
} else {
None
}
}
fn is_dark_mode() -> bool {
let cls = objc_class("NSAppearance");
if cls.is_null() {
return std::process::Command::new("defaults")
.args(["read", "-g", "AppleInterfaceStyle"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).contains("Dark"))
.unwrap_or(false);
}
let app_cls = objc_class("NSApplication");
if app_cls.is_null() {
return false;
}
let shared: *mut Object = unsafe { msg_send![app_cls, sharedApplication] };
if shared.is_null() {
return false;
}
let appearance: *mut Object = unsafe { msg_send![shared, effectiveAppearance] };
if appearance.is_null() {
return std::process::Command::new("defaults")
.args(["read", "-g", "AppleInterfaceStyle"])
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).contains("Dark"))
.unwrap_or(false);
}
let name: *mut Object = unsafe { msg_send![appearance, name] };
let name_str = ns_string_to_rust(name);
name_str.contains("Dark")
}
fn screen_width() -> f64 {
let cls = objc_class("NSScreen");
if cls.is_null() {
return 0.0;
}
let main: *mut Object = unsafe { msg_send![cls, mainScreen] };
if main.is_null() {
return 0.0;
}
let frame: (f64, f64, f64, f64) = unsafe { msg_send![main, frame] };
frame.2 }
fn screen_height() -> f64 {
let cls = objc_class("NSScreen");
if cls.is_null() {
return 0.0;
}
let main: *mut Object = unsafe { msg_send![cls, mainScreen] };
if main.is_null() {
return 0.0;
}
let frame: (f64, f64, f64, f64) = unsafe { msg_send![main, frame] };
frame.3 }
fn screen_scale() -> f64 {
let cls = objc_class("NSScreen");
if cls.is_null() {
return 1.0;
}
let main: *mut Object = unsafe { msg_send![cls, mainScreen] };
if main.is_null() {
return 1.0;
}
let scale: f64 = unsafe { msg_send![main, backingScaleFactor] };
if scale > 0.0 {
scale
} else {
1.0
}
}
fn system_volume() -> Option<f32> {
let output = std::process::Command::new("osascript")
.args(["-e", "output volume of (get volume settings)"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
text.parse::<f32>().ok().map(|v| v / 100.0) }
fn output_muted() -> Option<bool> {
let output = std::process::Command::new("osascript")
.args(["-e", "output muted of (get volume settings)"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
match text.as_str() {
"true" => Some(true),
"false" => Some(false),
_ => None,
}
}
fn current_locale() -> String {
let cls = objc_class("NSLocale");
if cls.is_null() {
return String::new();
}
let current: *mut Object = unsafe { msg_send![cls, currentLocale] };
if current.is_null() {
return String::new();
}
let ident: *mut Object = unsafe { msg_send![current, localeIdentifier] };
ns_string_to_rust(ident)
}
fn current_language() -> String {
let cls = objc_class("NSLocale");
if cls.is_null() {
return String::new();
}
let current: *mut Object = unsafe { msg_send![cls, currentLocale] };
if current.is_null() {
return String::new();
}
let lang: *mut Object = unsafe { msg_send![current, languageCode] };
ns_string_to_rust(lang)
}
fn current_timezone() -> String {
let cls = objc_class("NSTimeZone");
if cls.is_null() {
return String::new();
}
let tz: *mut Object = unsafe { msg_send![cls, localTimeZone] };
if tz.is_null() {
return String::new();
}
let name: *mut Object = unsafe { msg_send![tz, name] };
ns_string_to_rust(name)
}
fn timezone_offset() -> i64 {
let cls = objc_class("NSTimeZone");
if cls.is_null() {
return 0;
}
let tz: *mut Object = unsafe { msg_send![cls, localTimeZone] };
if tz.is_null() {
return 0;
}
unsafe { msg_send![tz, secondsFromGMT] }
}
fn macos_version() -> String {
let cls = objc_class("NSProcessInfo");
if cls.is_null() {
return String::new();
}
let info: *mut Object = unsafe { msg_send![cls, processInfo] };
if info.is_null() {
return String::new();
}
let ver: *mut Object = unsafe { msg_send![info, operatingSystemVersionString] };
ns_string_to_rust(ver)
}
fn hostname() -> String {
let cls = objc_class("NSProcessInfo");
if cls.is_null() {
return String::new();
}
let info: *mut Object = unsafe { msg_send![cls, processInfo] };
if info.is_null() {
return String::new();
}
let name: *mut Object = unsafe { msg_send![info, hostName] };
ns_string_to_rust(name)
}
fn username() -> String {
std::env::var("USER").unwrap_or_default()
}
fn system_uptime() -> f64 {
let cls = objc_class("NSProcessInfo");
if cls.is_null() {
return 0.0;
}
let info: *mut Object = unsafe { msg_send![cls, processInfo] };
if info.is_null() {
return 0.0;
}
unsafe { msg_send![info, systemUptime] }
}
fn physical_memory_gb() -> f64 {
let cls = objc_class("NSProcessInfo");
if cls.is_null() {
return 0.0;
}
let info: *mut Object = unsafe { msg_send![cls, processInfo] };
if info.is_null() {
return 0.0;
}
let bytes: u64 = unsafe { msg_send![info, physicalMemory] };
bytes as f64 / (1024.0 * 1024.0 * 1024.0)
}
fn wifi_enabled() -> Option<bool> {
let output = std::process::Command::new("networksetup")
.args(["-getairportpower", "en0"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout);
if text.contains("On") {
Some(true)
} else if text.contains("Off") {
Some(false)
} else {
None
}
}
fn wifi_ssid() -> Option<String> {
let output = std::process::Command::new("networksetup")
.args(["-getairportnetwork", "en0"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
text.strip_prefix("Current Wi-Fi Network: ")
.map(|s| s.to_string())
.filter(|s| !s.is_empty() && !s.contains("not associated"))
}
fn active_network_interfaces() -> Vec<NetworkInterface> {
let output = match std::process::Command::new("ifconfig").args(["-a"]).output() {
Ok(o) => o,
Err(_) => return vec![],
};
let text = String::from_utf8_lossy(&output.stdout);
let mut interfaces = Vec::new();
let mut current_name = String::new();
for line in text.lines() {
if !line.starts_with('\t') && !line.starts_with(' ') {
if let Some(colon) = line.find(':') {
current_name = line[..colon].to_string();
}
} else if line.contains("inet ") && !line.contains("inet6") && !current_name.is_empty() {
let parts: Vec<&str> = line.split_whitespace().collect();
if let Some(pos) = parts.iter().position(|&s| s == "inet") {
if let Some(&ip) = parts.get(pos + 1) {
if ip != "127.0.0.1" {
interfaces.push(NetworkInterface {
name: current_name.clone(),
ipv4: Some(ip.to_string()),
});
}
}
}
}
}
interfaces
}
fn keyboard_layout() -> Option<String> {
let output = std::process::Command::new("defaults")
.args(["read", "com.apple.HIToolbox", "AppleSelectedInputSources"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&output.stdout);
for line in text.lines() {
let trimmed = line.trim();
if trimmed.contains("KeyboardLayout Name") || trimmed.contains("Input Mode") {
if let Some(eq) = trimmed.find('=') {
let val = trimmed[eq + 1..]
.trim()
.trim_matches('"')
.trim_matches(';')
.trim_matches('"')
.trim()
.to_string();
if !val.is_empty() {
return Some(val);
}
}
}
}
None
}
fn frontmost_app_name() -> Option<String> {
let ws_cls = objc_class("NSWorkspace");
if ws_cls.is_null() {
return None;
}
let ws: *mut Object = unsafe { msg_send![ws_cls, sharedWorkspace] };
if ws.is_null() {
return None;
}
let app: *mut Object = unsafe { msg_send![ws, frontmostApplication] };
if app.is_null() {
return None;
}
let name: *mut Object = unsafe { msg_send![app, localizedName] };
let s = ns_string_to_rust(name);
if s.is_empty() {
None
} else {
Some(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn system_context_serializes_to_json() {
let ctx = SystemContext {
battery_level: Some(0.75),
battery_charging: Some(true),
power_source: Some("AC Power".to_string()),
dark_mode: true,
screen_width: 1440.0,
screen_height: 900.0,
screen_scale: 2.0,
system_volume: Some(0.5),
output_muted: Some(false),
locale: "en_US".to_string(),
language: "en".to_string(),
timezone: "Europe/Helsinki".to_string(),
timezone_offset_secs: 7200,
macos_version: "14.0".to_string(),
hostname: "macbook".to_string(),
username: "mikko".to_string(),
uptime_secs: 1234.0,
physical_memory_gb: 16.0,
wifi_enabled: Some(true),
wifi_ssid: Some("office".to_string()),
active_interfaces: vec![NetworkInterface {
name: "en0".to_string(),
ipv4: Some("192.168.1.10".to_string()),
}],
keyboard_layout: Some("ABC".to_string()),
frontmost_app: Some("Terminal".to_string()),
};
let json = serde_json::to_string(&ctx).unwrap();
assert!(json.contains("macos_version"));
assert!(json.contains("dark_mode"));
assert!(json.contains("wifi_enabled"));
}
#[test]
fn system_context_exposes_expected_fields() {
let ctx = SystemContext {
battery_level: Some(0.75),
battery_charging: Some(true),
power_source: Some("AC Power".to_string()),
dark_mode: true,
screen_width: 1440.0,
screen_height: 900.0,
screen_scale: 2.0,
system_volume: Some(0.5),
output_muted: Some(false),
locale: "en_US".to_string(),
language: "en".to_string(),
timezone: "Europe/Helsinki".to_string(),
timezone_offset_secs: 7200,
macos_version: "14.0".to_string(),
hostname: "macbook".to_string(),
username: "mikko".to_string(),
uptime_secs: 1234.0,
physical_memory_gb: 16.0,
wifi_enabled: Some(true),
wifi_ssid: Some("office".to_string()),
active_interfaces: vec![NetworkInterface {
name: "en0".to_string(),
ipv4: Some("192.168.1.10".to_string()),
}],
keyboard_layout: Some("ABC".to_string()),
frontmost_app: Some("Terminal".to_string()),
};
assert_eq!(ctx.locale, "en_US");
assert_eq!(ctx.active_interfaces.len(), 1);
assert_eq!(ctx.active_interfaces[0].name, "en0");
assert_eq!(ctx.frontmost_app.as_deref(), Some("Terminal"));
}
#[test]
fn network_interface_serializes_to_json() {
let iface = NetworkInterface {
name: "en0".to_string(),
ipv4: Some("192.168.1.10".to_string()),
};
let json = serde_json::to_string(&iface).unwrap();
assert!(json.contains("en0"));
assert!(json.contains("192.168.1.10"));
}
#[test]
#[ignore = "touches AppKit and system APIs that can abort under cargo test"]
fn collect_system_context_does_not_panic() {
let ctx = collect_system_context();
assert!(!ctx.macos_version.is_empty());
assert!(!ctx.hostname.is_empty());
assert!(!ctx.locale.is_empty());
assert!(!ctx.timezone.is_empty());
assert!(ctx.physical_memory_gb > 0.0);
assert!(ctx.uptime_secs > 0.0);
}
#[test]
#[ignore = "touches AppKit and system APIs that can abort under cargo test"]
fn screen_dimensions_are_positive() {
let w = screen_width();
let h = screen_height();
if w > 0.0 {
assert!(h > 0.0);
assert!(screen_scale() >= 1.0);
}
}
#[test]
#[ignore = "touches AppKit and system APIs that can abort under cargo test"]
fn dark_mode_returns_bool() {
let _ = is_dark_mode();
}
#[test]
#[ignore = "touches AppKit and system APIs that can abort under cargo test"]
fn locale_is_not_empty() {
assert!(!current_locale().is_empty());
}
#[test]
#[ignore = "touches AppKit and system APIs that can abort under cargo test"]
fn timezone_is_not_empty() {
assert!(!current_timezone().is_empty());
}
}