diff --git a/Cargo.lock b/Cargo.lock index 1e8e46b..cfb8dba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,8 +1,16 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + [[package]] name = "bato" -version = "0.1.4" +version = "0.1.5" dependencies = [ "cmake", "serde", @@ -11,9 +19,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.67" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" [[package]] name = "cmake" @@ -30,6 +38,22 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "indexmap" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "linked-hash-map" version = "0.5.4" @@ -38,9 +62,9 @@ checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "proc-macro2" -version = "1.0.26" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" +checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" dependencies = [ "unicode-xid", ] @@ -56,18 +80,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.126" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.126" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", @@ -76,21 +100,21 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.8.17" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15654ed4ab61726bf918a39cb8d98a2e2995b002387807fa6ba58fdf7f59bb23" +checksum = "d8c608a35705a5d3cdc9fbe403147647ff34b921f8e833e49306df898f9b20af" dependencies = [ "dtoa", - "linked-hash-map", + "indexmap", "serde", "yaml-rust", ] [[package]] name = "syn" -version = "1.0.72" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 542af54..e0a5490 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bato" -version = "0.1.4" +version = "0.1.5" authors = ["pierre "] edition = "2018" links = "notilus" diff --git a/src/battery.rs b/src/battery.rs new file mode 100644 index 0000000..6832249 --- /dev/null +++ b/src/battery.rs @@ -0,0 +1,141 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use crate::{ + fsm::{Fsm, FsmState, StateMap}, + notify::{send, NotifyNotification}, + Notification, +}; +use std::{collections::HashMap, hash::Hash}; + +struct ChargingState; +struct DischargingState; +struct FullState; +struct LowState; +struct CriticalState; + +#[derive(Hash, Eq, PartialEq)] +pub enum State { + Charging, + Discharging, + Full, + Low, + Critical, +} + +pub struct Data<'data> { + pub current_level: u32, + pub status: String, + pub low_level: u32, + pub critical_level: u32, + pub critical: Option<&'data Notification>, + pub low: Option<&'data Notification>, + pub full: Option<&'data Notification>, + pub charging: Option<&'data Notification>, + pub discharging: Option<&'data Notification>, + pub notification: *mut NotifyNotification, +} + +impl<'data> FsmState> for ChargingState { + fn enter(&mut self, data: &mut Data) { + if let Some(n) = data.charging { + send(data.notification, n); + } + } + + fn next_state(&self, data: &mut Data) -> Option { + match (data.status.as_str(), data.current_level) { + ("Full", _) => Some(State::Full), + ("Discharging", l) if l <= data.critical_level => Some(State::Critical), + ("Discharging", l) if l <= data.low_level => Some(State::Low), + ("Discharging", _) => Some(State::Discharging), + _ => None, + } + } + + fn exit(&mut self, _data: &mut Data) {} +} + +impl<'data> FsmState> for DischargingState { + fn enter(&mut self, data: &mut Data) { + if let Some(n) = data.discharging { + send(data.notification, n); + } + } + + fn next_state(&self, data: &mut Data) -> Option { + match (data.status.as_str(), data.current_level) { + ("Charging", _) => Some(State::Charging), + ("Full", _) => Some(State::Full), // add this just in case + ("Discharging", l) if l <= data.critical_level => Some(State::Critical), + ("Discharging", l) if l <= data.low_level => Some(State::Low), + _ => None, + } + } + + fn exit(&mut self, _data: &mut Data) {} +} + +impl<'data> FsmState> for FullState { + fn enter(&mut self, data: &mut Data) { + if let Some(n) = data.full { + send(data.notification, n); + } + } + + fn next_state(&self, data: &mut Data) -> Option { + match data.status.as_str() { + "Charging" => Some(State::Charging), + "Discharging" => Some(State::Discharging), + _ => None, + } + } + + fn exit(&mut self, _data: &mut Data) {} +} + +impl<'data> FsmState> for LowState { + fn enter(&mut self, data: &mut Data) { + if let Some(n) = data.low { + send(data.notification, n); + } + } + + fn next_state(&self, data: &mut Data) -> Option { + match (data.status.as_str(), data.current_level) { + ("Charging", _) => Some(State::Charging), + ("Discharging", l) if l <= data.critical_level => Some(State::Critical), + _ => None, + } + } + + fn exit(&mut self, _data: &mut Data) {} +} + +impl<'data> FsmState> for CriticalState { + fn enter(&mut self, data: &mut Data) { + if let Some(n) = data.critical { + send(data.notification, n); + } + } + + fn next_state(&self, data: &mut Data) -> Option { + if data.status == "Charging" { + return Some(State::Charging); + } + None + } + + fn exit(&mut self, _data: &mut Data) {} +} + +pub fn create_fsm<'data>() -> Fsm> { + let mut states: StateMap = HashMap::new(); + states.insert(State::Full, Box::new(FullState)); + states.insert(State::Charging, Box::new(ChargingState)); + states.insert(State::Discharging, Box::new(DischargingState)); + states.insert(State::Low, Box::new(LowState)); + states.insert(State::Critical, Box::new(CriticalState)); + Fsm::new(State::Discharging, states) +} diff --git a/src/fsm.rs b/src/fsm.rs new file mode 100644 index 0000000..e7ad51a --- /dev/null +++ b/src/fsm.rs @@ -0,0 +1,53 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::{collections::HashMap, hash::Hash}; + +pub type StateMap = HashMap>>; + +pub struct Fsm +where + K: Eq + Hash, +{ + current_state: K, + states: StateMap, +} + +impl Fsm +where + K: Eq + Hash, +{ + pub fn new(init_state: K, states: StateMap) -> Self { + Fsm { + current_state: init_state, + states, + } + } + + fn set_state(&mut self, new_state: K, data: &mut D) { + self.states.get_mut(&self.current_state).unwrap().exit(data); + self.states.get_mut(&new_state).unwrap().enter(data); + self.current_state = new_state; + } + + pub fn shift(&mut self, data: &mut D) { + if let Some(next_state) = self + .states + .get_mut(&self.current_state) + .unwrap() + .next_state(data) + { + self.set_state(next_state, data); + } + } +} + +pub trait FsmState +where + K: Eq + Hash, +{ + fn enter(&mut self, data: &mut D); + fn next_state(&self, data: &mut D) -> Option; + fn exit(&mut self, data: &mut D); +} diff --git a/src/lib.rs b/src/lib.rs index 09024f8..c341080 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,10 +2,14 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +mod battery; mod error; +mod fsm; mod notify; +use battery::{Data, State}; use error::Error; -use notify::{close_libnotilus, init_libnotilus, send, NotifyNotification}; +use fsm::Fsm; +use notify::{close_libnotilus, init_libnotilus, NotifyNotification}; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use std::env; @@ -56,41 +60,53 @@ pub struct Config { discharging: Option, } -pub struct Bato { - uevent: String, - now_attribute: String, - full_attribute: String, - notification: *mut NotifyNotification, - config: Config, - critical_notified: bool, - low_notified: bool, - full_notified: bool, - status_notified: bool, - previous_status: String, -} +impl Config { + pub fn new() -> Result { + let home = env::var("HOME").map_err(|err| format!("environment variable HOME, {}", err))?; + let config_path = + env::var("XDG_CONFIG_HOME").unwrap_or_else(|_| format!("{}/.config", home)); + let content = fs::read_to_string(format!("{}/bato/bato.yaml", config_path)) + .map_err(|err| format!("while reading the config file, {}", err))?; + let config: Config = serde_yaml::from_str(&content) + .map_err(|err| format!("while deserializing the config file, {}", err))?; + Ok(config) + } -impl Bato { - pub fn with_config(mut config: Config) -> Result { - let bat_name = if let Some(v) = &config.bat_name { - String::from(v) - } else { - String::from(BAT_NAME) - }; - if let Some(v) = &mut config.critical { + pub fn normalize(&mut self) -> &Self { + if let Some(v) = &mut self.critical { if v.urgency.is_none() { v.urgency = Some(Urgency::Critical) } } - if let Some(v) = &mut config.low { + if let Some(v) = &mut self.low { if v.urgency.is_none() { v.urgency = Some(Urgency::Normal) } } - if let Some(v) = &mut config.full { + if let Some(v) = &mut self.full { if v.urgency.is_none() { v.urgency = Some(Urgency::Normal) } } + self + } +} + +pub struct Bato<'data> { + uevent: String, + now_attribute: String, + full_attribute: String, + notification: *mut NotifyNotification, + fsm: Fsm>, +} + +impl<'data> Bato<'data> { + pub fn with_config(config: &Config) -> Result { + let bat_name = if let Some(v) = &config.bat_name { + String::from(v) + } else { + String::from(BAT_NAME) + }; let mut full_design = true; if let Some(v) = config.full_design { full_design = v; @@ -107,13 +123,8 @@ impl Bato { uevent, now_attribute, full_attribute, - config, - critical_notified: false, notification: ptr::null_mut(), - low_notified: false, - full_notified: false, - status_notified: false, - previous_status: "Unknown".to_string(), + fsm: battery::create_fsm(), }) } @@ -152,66 +163,24 @@ impl Bato { Ok((now.unwrap(), full.unwrap(), status.unwrap())) } - pub fn check(&mut self) -> Result<(), Error> { - let mut current_notification: Option<&Notification> = None; + pub fn update(&mut self, config: &'data Config) -> Result<(), Error> { let (energy, capacity, status) = self.parse_attributes()?; let capacity = capacity as u64; let energy = energy as u64; let battery_level = u32::try_from(100_u64 * energy / capacity)?; - if status == "Charging" && self.previous_status != "Charging" && !self.status_notified { - self.status_notified = true; - if let Some(n) = &self.config.charging { - current_notification = Some(n); - } - } - if status == "Discharging" && self.previous_status != "Discharging" && !self.status_notified - { - self.status_notified = true; - if let Some(n) = &self.config.discharging { - current_notification = Some(n); - } - } - if status == self.previous_status && self.status_notified { - self.status_notified = false; - } - if status == "Discharging" - && !self.low_notified - && battery_level <= self.config.low_level - && battery_level > self.config.critical_level - { - self.low_notified = true; - if let Some(n) = &self.config.low { - current_notification = Some(n); - } - } - if status == "Discharging" - && !self.critical_notified - && battery_level <= self.config.critical_level - { - self.critical_notified = true; - if let Some(n) = &self.config.critical { - current_notification = Some(n); - } - } - if status == "Full" && !self.full_notified { - self.full_notified = true; - if let Some(n) = &self.config.full { - current_notification = Some(n); - } - } - if status == "Charging" && self.critical_notified { - self.critical_notified = false; - } - if status == "Charging" && self.low_notified { - self.low_notified = false; - } - if status == "Discharging" && self.full_notified { - self.full_notified = false; - } - self.previous_status = status; - if let Some(notification) = current_notification { - send(self.notification, notification); - } + let mut data = Data { + current_level: battery_level, + status, + low_level: config.low_level, + critical_level: config.critical_level, + critical: config.critical.as_ref(), + low: config.low.as_ref(), + full: config.full.as_ref(), + charging: config.charging.as_ref(), + discharging: config.discharging.as_ref(), + notification: self.notification, + }; + self.fsm.shift(&mut data); Ok(()) } @@ -274,13 +243,3 @@ fn find_attribute_prefix<'a, 'b>(path: &'a str) -> Result<&'b str, Error> { )) }) } - -pub fn deserialize_config() -> Result { - let home = env::var("HOME").map_err(|err| format!("environment variable HOME, {}", err))?; - let config_path = env::var("XDG_CONFIG_HOME").unwrap_or_else(|_| format!("{}/.config", home)); - let content = fs::read_to_string(format!("{}/bato/bato.yaml", config_path)) - .map_err(|err| format!("while reading the config file, {}", err))?; - let config: Config = serde_yaml::from_str(&content) - .map_err(|err| format!("while deserializing the config file, {}", err))?; - Ok(config) -} diff --git a/src/main.rs b/src/main.rs index bcb997a..b41713a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. mod error; -use bato::{deserialize_config, Bato}; +use bato::{Bato, Config}; use std::io::Error; use std::process; use std::thread; @@ -12,15 +12,16 @@ use std::time::Duration; const TICK_RATE: Duration = Duration::from_secs(5); fn main() -> Result<(), Error> { - let config = deserialize_config().unwrap_or_else(|err| { + let mut config = Config::new().unwrap_or_else(|err| { eprintln!("bato error: {}", err); process::exit(1); }); + config.normalize(); let mut tick = TICK_RATE; if let Some(rate) = config.tick_rate { tick = Duration::from_secs(rate as u64); } - let mut bato = Bato::with_config(config).unwrap_or_else(|err| { + let mut bato = Bato::with_config(&config).unwrap_or_else(|err| { eprintln!("bato error: {}", err); process::exit(1); }); @@ -29,7 +30,7 @@ fn main() -> Result<(), Error> { process::exit(1); }); loop { - bato.check().unwrap_or_else(|err| { + bato.update(&config).unwrap_or_else(|err| { eprintln!("bato error: {}", err); process::exit(1); });