use log::{debug, error}; use mpris::PlayerFinder; use std::sync::{Arc, Mutex}; use tokio::sync::mpsc; use zbus::Connection; #[derive(Clone, Default)] pub struct SystemStatus { pub battery_percent: Option, pub is_charging: bool, pub media_title: Option, pub media_artist: Option, pub media_playing: bool, pub media_art_url: Option, pub media_art_data: Option>>, pub wifi_ssid: Option, pub wifi_strength: Option, pub bluetooth_connected: bool, pub bluetooth_devices: Vec, pub keyboard_layout: Option, } #[derive(Debug, Clone, Copy)] pub enum SystemCommand { PowerOff, Reboot, Suspend, } pub struct SystemManager { status: Arc>, cmd_tx: mpsc::UnboundedSender, } impl SystemManager { pub fn new() -> Self { let status = Arc::new(Mutex::new(SystemStatus::default())); let s_clone = status.clone(); let (cmd_tx, mut cmd_rx) = mpsc::unbounded_channel::(); // Spawn a thread to update status periodically and handle commands std::thread::spawn(move || { let rt = match tokio::runtime::Runtime::new() { Ok(rt) => rt, Err(e) => { error!("Failed to create tokio runtime for SystemManager: {}", e); return; } }; rt.block_on(async { let mut conn: Option = None; let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(2)); let mut last_art_url: Option = None; let mut last_art_data: Option>> = None; loop { // Try to connect to system DBus if not connected if conn.is_none() { match Connection::system().await { Ok(c) => conn = Some(c), Err(e) => { error!("Failed to connect to system DBus: {}", e); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; } } } tokio::select! { _ = interval.tick() => { let mut new_status = SystemStatus::default(); if let Some(ref c) = conn { // 1. Battery status if let Ok(proxy) = upower_dbus::UPowerProxy::new(c).await { if let Ok(display_device) = proxy.get_display_device().await { new_status.battery_percent = display_device.percentage().await.ok(); if let Ok(state) = display_device.state().await { new_status.is_charging = format!("{:?}", state).contains("Charging"); } } } } // 2. MPRIS status if let Ok(finder) = PlayerFinder::new() { if let Ok(player) = finder.find_active() { if let Ok(metadata) = player.get_metadata() { new_status.media_title = metadata.title().map(|s| s.to_string()); new_status.media_artist = metadata.artists().map(|a| a.join(", ")); new_status.media_art_url = metadata.art_url().map(|u| u.to_string()); if new_status.media_art_url != last_art_url { last_art_url = new_status.media_art_url.clone(); last_art_data = None; if let Some(ref url) = last_art_url { if url.starts_with("file://") { let path = url.trim_start_matches("file://"); if let Ok(data) = std::fs::read(path) { last_art_data = Some(Arc::new(data)); } } else if url.starts_with("http") { #[cfg(feature = "networking")] if let Ok(resp) = reqwest::get(url).await { if let Ok(bytes) = resp.bytes().await { last_art_data = Some(Arc::new(bytes.to_vec())); } } #[cfg(not(feature = "networking"))] { log::debug!("Networking disabled, skipping remote album art: {}", url); } } } } new_status.media_art_data = last_art_data.clone(); } new_status.media_playing = player.get_playback_status().map(|s| matches!(s, mpris::PlaybackStatus::Playing)).unwrap_or(false); } } // 3. WiFi status (NetworkManager) if let Some(ref c) = conn { if let Ok(reply) = c.call_method( Some("org.freedesktop.NetworkManager"), "/org/freedesktop/NetworkManager", Some("org.freedesktop.NetworkManager"), "GetDevices", &(), ).await { let devices: Vec = reply.body().unwrap(); for dev_path in devices { if let Ok(dev_type_reply) = c.call_method( Some("org.freedesktop.NetworkManager"), &dev_path, Some("org.freedesktop.DBus.Properties"), "Get", &("org.freedesktop.NetworkManager.Device", "DeviceType"), ).await { let dev_type: u32 = dev_type_reply.body::().unwrap().downcast().unwrap(); if dev_type == 2 { // WiFi if let Ok(active_ap_reply) = c.call_method( Some("org.freedesktop.NetworkManager"), &dev_path, Some("org.freedesktop.DBus.Properties"), "Get", &("org.freedesktop.NetworkManager.Device.Wireless", "ActiveAccessPoint"), ).await { let ap_path: zbus::zvariant::OwnedObjectPath = active_ap_reply.body::().unwrap().downcast().unwrap(); if ap_path.as_str() != "/" { if let Ok(ssid_reply) = c.call_method( Some("org.freedesktop.NetworkManager"), &ap_path, Some("org.freedesktop.DBus.Properties"), "Get", &("org.freedesktop.NetworkManager.AccessPoint", "Ssid"), ).await { let ssid_bytes: Vec = ssid_reply.body::().unwrap().downcast().unwrap(); new_status.wifi_ssid = Some(String::from_utf8_lossy(&ssid_bytes).to_string()); } if let Ok(strength_reply) = c.call_method( Some("org.freedesktop.NetworkManager"), &ap_path, Some("org.freedesktop.DBus.Properties"), "Get", &("org.freedesktop.NetworkManager.AccessPoint", "Strength"), ).await { new_status.wifi_strength = Some(strength_reply.body::().unwrap().downcast().unwrap()); } } } break; } } } } // 4. Bluetooth status (BlueZ) if let Ok(objects_reply) = c.call_method( Some("org.bluez"), "/", Some("org.freedesktop.DBus.ObjectManager"), "GetManagedObjects", &(), ).await { use std::collections::HashMap; type ManagedObjects = HashMap>>; if let Ok(objects) = objects_reply.body::() { for (_path, interfaces) in objects { if let Some(device) = interfaces.get("org.bluez.Device1") { if let Some(connected) = device.get("Connected") { if connected.downcast_ref::().copied().unwrap_or(false) { new_status.bluetooth_connected = true; if let Some(name) = device.get("Name") { let name_str: String = name.downcast_ref::().map(|s| s.to_string()).unwrap_or_default(); new_status.bluetooth_devices.push(name_str); } } } } } } } } { if let Ok(mut s) = s_clone.lock() { *s = new_status; } } } Some(command) = cmd_rx.recv() => { if let Some(ref c) = conn { let method = match command { SystemCommand::PowerOff => "PowerOff", SystemCommand::Reboot => "Reboot", SystemCommand::Suspend => "Suspend", }; debug!("Executing system command: {}", method); // Set a timeout for the DBus call to prevent hanging the background thread let result = tokio::time::timeout( tokio::time::Duration::from_secs(5), c.call_method( Some("org.freedesktop.login1"), "/org/freedesktop/login1", Some("org.freedesktop.login1.Manager"), method, &(true), ) ).await; if let Err(_) = result { error!("System command {} timed out", method); } } } } } }); }); Self { status, cmd_tx } } pub fn get_status(&self) -> SystemStatus { self.status.lock().unwrap().clone() } pub fn send_command(&self, cmd: SystemCommand) { let _ = self.cmd_tx.send(cmd); } pub fn media_play_pause(&self) { if let Ok(finder) = PlayerFinder::new() { if let Ok(player) = finder.find_active() { let _ = player.play_pause(); } } } pub fn media_next(&self) { if let Ok(finder) = PlayerFinder::new() { if let Ok(player) = finder.find_active() { let _ = player.next(); } } } pub fn media_prev(&self) { if let Ok(finder) = PlayerFinder::new() { if let Ok(player) = finder.find_active() { let _ = player.previous(); } } } }