i3status-rs 0.36.0

A feature-rich and resource-friendly replacement for i3status, written in Rust.
Documentation
//! Information about the internal power supply
//!
//! This block can display the current battery state (Full, Charging or Discharging), percentage
//! charged and estimate time until (dis)charged for an internal power supply.
//!
//! # Configuration
//!
//! Key | Values | Default
//! ----|--------|--------
//! `device` | sysfs/UPower: The device in `/sys/class/power_supply/` to read from (can also be "DisplayDevice" for UPower, which is a single logical power source representing all physical power sources. This is for example useful if your system has multiple batteries, in which case the DisplayDevice behaves as if you had a single larger battery.). apc_ups: IPv4Address:port or hostname:port | sysfs: the first battery device found in /sys/class/power_supply, with "BATx" or "CMBx" entries taking precedence. apc_ups: "localhost:3551". upower: `DisplayDevice`
//! `driver` | One of `"sysfs"`, `"apc_ups"`, or `"upower"` | `"sysfs"`
//! `model` | If present, the contents of `/sys/class/power_supply/.../model_name` must match this value. Typical use is to select by model name on devices that change their path. | N/A
//! `interval` | Update interval, in seconds. Only relevant for driver = "sysfs" or "apc_ups". | `10`
//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon $percentage "`
//! `full_format` | Same as `format` but for when the battery is full | `" $icon "`
//! `charging_format` | Same as `format` but for when the battery is charging | Links to `format`
//! `empty_format` | Same as `format` but for when the battery is empty | `" $icon "`
//! `not_charging_format` | Same as `format` but for when the battery is not charging. Defaults to the full battery icon as many batteries report this status when they are full. | `" $icon "`
//! `missing_format` | Same as `format` if the battery cannot be found. | `" $icon "`
//! `info` | Minimum battery level, where state is set to info | `60`
//! `good` | Minimum battery level, where state is set to good | `60`
//! `warning` | Minimum battery level, where state is set to warning | `30`
//! `critical` | Minimum battery level, where state is set to critical | `15`
//! `full_threshold` | Percentage above which the battery is considered full (`full_format` shown) | `95`
//! `empty_threshold` | Percentage below which the battery is considered empty | `7.5`
//!
//! Placeholder  | Value                                                                   | Type              | Unit
//! -------------|-------------------------------------------------------------------------|-------------------|-----
//! `icon`       | Icon based on battery's state                                           | Icon   | -
//! `percentage` | Battery level, in percent                                               | Number | Percents
//! `time_remaining`  | Time remaining until (dis)charge is complete. Presented only if battery's status is (dis)charging. | Duration | -
//! `time`       | Time remaining until (dis)charge is complete. Presented only if battery's status is (dis)charging. | String *DEPRECATED* | -
//! `power`      | Power consumption by the battery or from the power supply when charging | String or Float   | Watts
//!
//! `time` has been deprecated in favor of `time_remaining`.
//!
//! # Examples
//!
//! Basic usage:
//!
//! ```toml
//! [[block]]
//! block = "battery"
//! format = " $icon $percentage "
//! ```
//!
//! ```toml
//! [[block]]
//! block = "battery"
//! format = " $percentage {$time_remaining.dur(hms:true, min_unit:m) |}"
//! device = "DisplayDevice"
//! driver = "upower"
//! ```
//!
//! Hide missing battery:
//!
//! ```toml
//! [[block]]
//! block = "battery"
//! missing_format = ""
//! ```
//!
//! # Icons Used
//! - `bat` (as a progression)
//! - `bat_charging` (as a progression)
//! - `bat_not_available`

use regex::Regex;
use std::convert::Infallible;
use std::str::FromStr;

use super::prelude::*;

mod apc_ups;
mod sysfs;
mod upower;

// make_log_macro!(debug, "battery");

#[derive(Deserialize, Debug, SmartDefault)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
    pub device: Option<String>,
    pub driver: BatteryDriver,
    pub model: Option<String>,
    #[default(10.into())]
    pub interval: Seconds,
    pub format: FormatConfig,
    pub full_format: FormatConfig,
    pub charging_format: FormatConfig,
    pub empty_format: FormatConfig,
    pub not_charging_format: FormatConfig,
    pub missing_format: FormatConfig,
    #[default(60.0)]
    pub info: f64,
    #[default(60.0)]
    pub good: f64,
    #[default(30.0)]
    pub warning: f64,
    #[default(15.0)]
    pub critical: f64,
    #[default(95.0)]
    pub full_threshold: f64,
    #[default(7.5)]
    pub empty_threshold: f64,
}

#[derive(Deserialize, Debug, SmartDefault)]
#[serde(rename_all = "snake_case")]
pub enum BatteryDriver {
    #[default]
    Sysfs,
    ApcUps,
    Upower,
}

pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
    let format = config.format.with_default(" $icon $percentage ")?;
    let format_full = config.full_format.with_default(" $icon ")?;
    let charging_format = config.charging_format.with_default_format(&format);
    let format_empty = config.empty_format.with_default(" $icon ")?;
    let format_not_charging = config.not_charging_format.with_default(" $icon ")?;
    let missing_format = config.missing_format.with_default(" $icon ")?;

    let dev_name = DeviceName::new(config.device.clone())?;
    let mut device: Box<dyn BatteryDevice + Send + Sync> = match config.driver {
        BatteryDriver::Sysfs => Box::new(sysfs::Device::new(
            dev_name,
            config.model.clone(),
            config.interval,
        )),
        BatteryDriver::ApcUps => Box::new(apc_ups::Device::new(dev_name, config.interval).await?),
        BatteryDriver::Upower => {
            Box::new(upower::Device::new(dev_name, config.model.clone()).await?)
        }
    };

    loop {
        let mut info = device.get_info().await?;

        if let Some(info) = &mut info {
            if info.capacity >= config.full_threshold {
                info.status = BatteryStatus::Full;
            } else if info.capacity <= config.empty_threshold
                && info.status != BatteryStatus::Charging
            {
                info.status = BatteryStatus::Empty;
            }
        }

        match info {
            Some(info) => {
                let mut widget = Widget::new();

                widget.set_format(match info.status {
                    BatteryStatus::Empty => format_empty.clone(),
                    BatteryStatus::Full => format_full.clone(),
                    BatteryStatus::Charging => charging_format.clone(),
                    BatteryStatus::NotCharging => format_not_charging.clone(),
                    _ => format.clone(),
                });

                let mut values = map!(
                    "percentage" => Value::percents(info.capacity)
                );

                info.power
                    .map(|p| values.insert("power".into(), Value::watts(p)));
                info.time_remaining.inspect(|&t| {
                    map! { @extend values
                        "time" => Value::text(
                            format!(
                                "{}:{:02}",
                                (t / 3600.) as i32,
                                (t % 3600. / 60.) as i32
                            ),
                        ),
                        "time_remaining" =>  Value::duration(
                            Duration::from_secs(t as u64),
                        ),
                    }
                });

                let (icon_name, icon_value, state) = match (info.status, info.capacity) {
                    (BatteryStatus::Empty, _) => ("bat", 0.0, State::Critical),
                    (BatteryStatus::Full | BatteryStatus::NotCharging, _) => {
                        ("bat", 1.0, State::Idle)
                    }
                    (status, capacity) => (
                        if status == BatteryStatus::Charging {
                            "bat_charging"
                        } else {
                            "bat"
                        },
                        capacity / 100.0,
                        if status == BatteryStatus::Charging {
                            State::Good
                        } else if capacity <= config.critical {
                            State::Critical
                        } else if capacity <= config.warning {
                            State::Warning
                        } else if capacity <= config.info {
                            State::Info
                        } else if capacity > config.good {
                            State::Good
                        } else {
                            State::Idle
                        },
                    ),
                };

                values.insert(
                    "icon".into(),
                    Value::icon_progression(icon_name, icon_value),
                );

                widget.set_values(values);
                widget.state = state;
                api.set_widget(widget)?;
            }
            None => {
                let mut widget = Widget::new()
                    .with_format(missing_format.clone())
                    .with_state(State::Critical);
                widget.set_values(map!("icon" => Value::icon("bat_not_available")));
                api.set_widget(widget)?;
            }
        }

        select! {
            update = device.wait_for_change() => update?,
            _ = api.wait_for_update_request() => (),
        }
    }
}

#[async_trait]
trait BatteryDevice {
    async fn get_info(&mut self) -> Result<Option<BatteryInfo>>;
    async fn wait_for_change(&mut self) -> Result<()>;
}

/// `Option<Regex>`, but more intuitive
#[derive(Debug)]
enum DeviceName {
    Any,
    Regex(Regex),
}

impl DeviceName {
    fn new(pat: Option<String>) -> Result<Self> {
        Ok(match pat {
            None => Self::Any,
            Some(pat) => Self::Regex(pat.parse().error("failed to parse regex")?),
        })
    }

    fn matches(&self, name: &str) -> bool {
        match self {
            Self::Any => true,
            Self::Regex(pat) => pat.is_match(name),
        }
    }

    fn exact(&self) -> Option<&str> {
        match self {
            Self::Any => None,
            Self::Regex(pat) => Some(pat.as_str()),
        }
    }
}

#[derive(Debug, Clone, Copy)]
struct BatteryInfo {
    /// Current status, e.g. "charging", "discharging", etc.
    status: BatteryStatus,
    /// The capacity in percents
    capacity: f64,
    /// Power consumption in watts
    power: Option<f64>,
    /// Time in seconds
    time_remaining: Option<f64>,
}

#[derive(Copy, Clone, Debug, Eq, PartialEq, SmartDefault)]
enum BatteryStatus {
    Charging,
    Discharging,
    Empty,
    Full,
    NotCharging,
    #[default]
    Unknown,
}

impl FromStr for BatteryStatus {
    type Err = Infallible;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(match s {
            "Charging" => Self::Charging,
            "Discharging" => Self::Discharging,
            "Empty" => Self::Empty,
            "Full" => Self::Full,
            "Not charging" => Self::NotCharging,
            _ => Self::Unknown,
        })
    }
}