main.rs (3490B)
1 use anyhow::{Context, Result}; 2 use serde::Deserialize; 3 use std::collections::HashMap; 4 use std::io::{IsTerminal, Write, stdout}; 5 use std::time::Duration; 6 7 #[derive(Deserialize, Debug, Clone)] 8 struct DeviceConfig { 9 mac: String, 10 text: Option<String>, 11 } 12 13 #[derive(Deserialize, Debug, Clone)] 14 struct Config { 15 devices: HashMap<String, DeviceConfig>, 16 } 17 18 fn load_config_path() -> Result<String> { 19 let home = std::env::var("HOME").context("Failed to get HOME env var")?; 20 Ok(format!("{home}/.config/plants/devices.toml")) 21 } 22 23 async fn load_config() -> Result<Config> { 24 let path = load_config_path()?; 25 let content = tokio::fs::read_to_string(&path) 26 .await 27 .with_context(|| format!("Failed to read config file: {path}"))?; 28 toml::from_str(&content).context("Failed to parse config file") 29 } 30 31 fn format_devices(devices: &[(String, u8, Option<String>)]) -> Option<String> { 32 if devices.is_empty() { 33 return None; 34 } 35 36 let parts: Vec<String> = devices 37 .iter() 38 .map(|(_, battery, text)| { 39 let icon = text.as_deref().unwrap_or(""); 40 format!("{icon} {battery}%") 41 }) 42 .collect(); 43 44 Some(format!("{}", parts.join(" "))) 45 } 46 47 fn print_output(output: &str) { 48 let mut out = stdout(); 49 if out.is_terminal() { 50 let _ = write!(out, "\x1b[2K\r{output}"); 51 } else { 52 let _ = writeln!(out, "{output}"); 53 } 54 let _ = out.flush(); 55 } 56 57 async fn get_battery_percentage( 58 adapter_name: &str, 59 address: bluer::Address, 60 conn: &zbus::Connection, 61 ) -> Option<u8> { 62 let addr_str = address.to_string().replace(':', "_"); 63 let path_str = format!("/org/bluez/{adapter_name}/dev_{addr_str}"); 64 let path = zbus::zvariant::ObjectPath::try_from(path_str).ok()?; 65 66 let proxy = zbus::Proxy::new(conn, "org.bluez", &path, "org.bluez.Battery1") 67 .await 68 .ok()?; 69 70 proxy.get_property::<u8>("Percentage").await.ok() 71 } 72 73 #[tokio::main] 74 async fn main() -> Result<()> { 75 let config = match load_config().await { 76 Ok(c) => c, 77 Err(e) => { 78 eprintln!("Failed to load config: {e}"); 79 return Ok(()); 80 } 81 }; 82 83 if config.devices.is_empty() { 84 eprintln!("No devices configured."); 85 return Ok(()); 86 } 87 88 let session = bluer::Session::new().await?; 89 let adapter = session.default_adapter().await?; 90 adapter.set_powered(true).await?; 91 92 let zbus_conn = zbus::Connection::system().await.ok(); 93 94 let mut devices: Vec<(String, u8, Option<String>)> = Vec::new(); 95 96 for (name, device_cfg) in &config.devices { 97 let Ok(address) = device_cfg.mac.parse::<bluer::Address>() else { 98 eprintln!("Invalid MAC address for {name}"); 99 continue; 100 }; 101 102 let Ok(device) = adapter.device(address) else { 103 continue; 104 }; 105 106 let is_connected = tokio::time::timeout(Duration::from_secs(2), device.is_connected()) 107 .await 108 .is_ok_and(|r| r.is_ok_and(|c| c)); 109 110 if !is_connected { 111 continue; 112 } 113 114 let Some(battery_pct) = (match &zbus_conn { 115 Some(conn) => get_battery_percentage(adapter.name(), address, conn).await, 116 None => None, 117 }) else { 118 continue; 119 }; 120 121 devices.push((name.clone(), battery_pct, device_cfg.text.clone())); 122 } 123 124 if let Some(text) = format_devices(&devices) { 125 print_output(&text); 126 } 127 128 Ok(()) 129 }