tools

various tools I have been using throughout the years
Log | Files | Refs | README | LICENSE

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 }