2020-01-26 22:37:18 +00:00
|
|
|
use super::{Context, Module, RootModuleConfig, Shell};
|
2019-09-30 12:10:35 +00:00
|
|
|
use crate::configs::battery::BatteryConfig;
|
2021-06-29 23:46:41 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
use mockall::automock;
|
2019-05-22 16:29:39 +00:00
|
|
|
|
2020-07-07 22:45:32 +00:00
|
|
|
use crate::formatter::StringFormatter;
|
|
|
|
|
2019-07-19 20:18:52 +00:00
|
|
|
/// Creates a module for the battery percentage and charging state
|
2019-07-02 20:12:53 +00:00
|
|
|
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
|
2019-08-26 01:52:44 +00:00
|
|
|
// TODO: Update when v1.0 printing refactor is implemented to only
|
|
|
|
// print escapes in a prompt context.
|
2020-01-26 22:37:18 +00:00
|
|
|
let percentage_char = match context.shell {
|
|
|
|
Shell::Zsh => "%%", // % is an escape in zsh, see PROMPT in `man zshmisc`
|
2019-08-26 01:52:44 +00:00
|
|
|
_ => "%",
|
|
|
|
};
|
2019-05-22 16:29:39 +00:00
|
|
|
|
2021-06-29 23:46:41 +00:00
|
|
|
let battery_status = get_battery_status(context)?;
|
2019-05-22 16:29:39 +00:00
|
|
|
let BatteryStatus { state, percentage } = battery_status;
|
|
|
|
|
2019-09-09 23:14:38 +00:00
|
|
|
let mut module = context.new_module("battery");
|
2020-07-07 22:45:32 +00:00
|
|
|
let config: BatteryConfig = BatteryConfig::try_load(module.config);
|
2019-09-12 18:06:59 +00:00
|
|
|
|
2020-07-07 22:45:32 +00:00
|
|
|
// Parse config under `display`.
|
|
|
|
// Select the first style that match the threshold,
|
|
|
|
// if all thresholds are lower do not display battery module.
|
|
|
|
let display_style = config
|
|
|
|
.display
|
2019-09-30 12:10:35 +00:00
|
|
|
.iter()
|
2020-07-07 22:45:32 +00:00
|
|
|
.find(|display_style| percentage <= display_style.threshold as f32)?;
|
2019-09-12 18:06:59 +00:00
|
|
|
|
2020-07-07 22:45:32 +00:00
|
|
|
// Parse the format string and build the module
|
|
|
|
match StringFormatter::new(config.format) {
|
|
|
|
Ok(formatter) => {
|
|
|
|
let formatter = formatter
|
|
|
|
.map_meta(|variable, _| match variable {
|
|
|
|
"symbol" => match state {
|
|
|
|
battery::State::Full => Some(config.full_symbol),
|
2021-04-17 11:52:46 +00:00
|
|
|
battery::State::Charging => display_style
|
|
|
|
.charging_symbol
|
|
|
|
.or(Some(config.charging_symbol)),
|
|
|
|
battery::State::Discharging => display_style
|
|
|
|
.discharging_symbol
|
|
|
|
.or(Some(config.discharging_symbol)),
|
2021-01-01 11:16:55 +00:00
|
|
|
battery::State::Unknown => Some(config.unknown_symbol),
|
|
|
|
battery::State::Empty => Some(config.empty_symbol),
|
2020-07-07 22:45:32 +00:00
|
|
|
_ => {
|
|
|
|
log::debug!("Unhandled battery state `{}`", state);
|
|
|
|
None
|
|
|
|
}
|
|
|
|
},
|
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.map_style(|style| match style {
|
|
|
|
"style" => Some(Ok(display_style.style)),
|
|
|
|
_ => None,
|
|
|
|
})
|
|
|
|
.map(|variable| match variable {
|
|
|
|
"percentage" => Some(Ok(format!("{}{}", percentage.round(), percentage_char))),
|
|
|
|
_ => None,
|
|
|
|
});
|
2019-09-12 18:06:59 +00:00
|
|
|
|
2020-07-07 22:45:32 +00:00
|
|
|
match formatter.parse(None) {
|
|
|
|
Ok(format_string) => {
|
|
|
|
module.set_segments(format_string);
|
|
|
|
Some(module)
|
2019-09-30 12:10:35 +00:00
|
|
|
}
|
2020-07-07 22:45:32 +00:00
|
|
|
Err(e) => {
|
|
|
|
log::warn!("Cannot parse `battery.format`: {}", e);
|
|
|
|
None
|
2019-09-30 12:10:35 +00:00
|
|
|
}
|
2019-09-20 16:52:54 +00:00
|
|
|
}
|
2019-05-22 16:29:39 +00:00
|
|
|
}
|
2020-07-07 22:45:32 +00:00
|
|
|
Err(e) => {
|
|
|
|
log::warn!("Cannot load `battery.format`: {}", e);
|
|
|
|
None
|
|
|
|
}
|
2019-05-22 16:29:39 +00:00
|
|
|
}
|
2019-09-12 18:06:59 +00:00
|
|
|
}
|
2019-05-22 16:29:39 +00:00
|
|
|
|
2021-06-29 23:46:41 +00:00
|
|
|
fn get_battery_status(context: &Context) -> Option<BatteryStatus> {
|
|
|
|
let battery_info = context.battery_info_provider.get_battery_info()?;
|
|
|
|
if battery_info.energy_full != 0.0 {
|
2019-12-03 16:48:50 +00:00
|
|
|
let battery = BatteryStatus {
|
2021-06-29 23:46:41 +00:00
|
|
|
percentage: battery_info.energy / battery_info.energy_full * 100.0,
|
|
|
|
state: battery_info.state,
|
2019-12-03 16:48:50 +00:00
|
|
|
};
|
|
|
|
log::debug!("Battery status: {:?}", battery);
|
|
|
|
Some(battery)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
2019-05-22 16:29:39 +00:00
|
|
|
|
2019-12-03 16:48:50 +00:00
|
|
|
/// the merge returns Charging if at least one is charging
|
|
|
|
/// Discharging if at least one is Discharging
|
|
|
|
/// Full if both are Full or one is Full and the other Unknow
|
|
|
|
/// Empty if both are Empty or one is Empty and the other Unknow
|
|
|
|
/// Unknown otherwise
|
|
|
|
fn merge_battery_states(state1: battery::State, state2: battery::State) -> battery::State {
|
|
|
|
use battery::State::{Charging, Discharging, Unknown};
|
|
|
|
if state1 == Charging || state2 == Charging {
|
|
|
|
Charging
|
|
|
|
} else if state1 == Discharging || state2 == Discharging {
|
|
|
|
Discharging
|
|
|
|
} else if state1 == state2 {
|
|
|
|
state1
|
|
|
|
} else if state1 == Unknown {
|
|
|
|
state2
|
|
|
|
} else if state2 == Unknown {
|
|
|
|
state1
|
|
|
|
} else {
|
|
|
|
Unknown
|
2019-05-22 16:29:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-29 23:46:41 +00:00
|
|
|
pub struct BatteryInfo {
|
2019-12-03 16:48:50 +00:00
|
|
|
energy: f32,
|
|
|
|
energy_full: f32,
|
|
|
|
state: battery::State,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2019-05-22 16:29:39 +00:00
|
|
|
struct BatteryStatus {
|
|
|
|
percentage: f32,
|
|
|
|
state: battery::State,
|
|
|
|
}
|
2021-06-29 23:46:41 +00:00
|
|
|
|
|
|
|
#[cfg_attr(test, automock)]
|
|
|
|
pub trait BatteryInfoProvider {
|
|
|
|
fn get_battery_info(&self) -> Option<BatteryInfo>;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct BatteryStatusProviderImpl;
|
|
|
|
|
|
|
|
impl BatteryInfoProvider for BatteryStatusProviderImpl {
|
|
|
|
fn get_battery_info(&self) -> Option<BatteryInfo> {
|
|
|
|
let battery_manager = battery::Manager::new().ok()?;
|
|
|
|
let batteries = battery_manager.batteries().ok()?;
|
|
|
|
Some(
|
|
|
|
batteries
|
|
|
|
.filter_map(|battery| match battery {
|
|
|
|
Ok(battery) => {
|
|
|
|
log::debug!("Battery found: {:?}", battery);
|
|
|
|
Some(BatteryInfo {
|
|
|
|
energy: battery.energy().value,
|
|
|
|
energy_full: battery.energy_full().value,
|
|
|
|
state: battery.state(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
Err(e) => {
|
|
|
|
let level = if cfg!(target_os = "linux") {
|
|
|
|
log::Level::Info
|
|
|
|
} else {
|
|
|
|
log::Level::Warn
|
|
|
|
};
|
|
|
|
log::log!(level, "Unable to access battery information:\n{}", &e);
|
|
|
|
None
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.fold(
|
|
|
|
BatteryInfo {
|
|
|
|
energy: 0.0,
|
|
|
|
energy_full: 0.0,
|
|
|
|
state: battery::State::Unknown,
|
|
|
|
},
|
|
|
|
|mut acc, x| {
|
|
|
|
acc.energy += x.energy;
|
|
|
|
acc.energy_full += x.energy_full;
|
|
|
|
acc.state = merge_battery_states(acc.state, x.state);
|
|
|
|
acc
|
|
|
|
},
|
|
|
|
),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use crate::test::ModuleRenderer;
|
|
|
|
use ansi_term::Color;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn no_battery_status() {
|
|
|
|
let mut mock = MockBatteryInfoProvider::new();
|
|
|
|
|
|
|
|
mock.expect_get_battery_info().times(1).returning(|| None);
|
|
|
|
|
|
|
|
let actual = ModuleRenderer::new("battery")
|
|
|
|
.config(toml::toml! {
|
|
|
|
[[battery.display]]
|
|
|
|
threshold = 100
|
|
|
|
style = ""
|
|
|
|
})
|
|
|
|
.battery_info_provider(&mock)
|
|
|
|
.collect();
|
|
|
|
let expected = None;
|
|
|
|
|
|
|
|
assert_eq!(expected, actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn ignores_zero_capacity_battery() {
|
|
|
|
let mut mock = MockBatteryInfoProvider::new();
|
|
|
|
|
|
|
|
mock.expect_get_battery_info().times(1).returning(|| {
|
|
|
|
Some(BatteryInfo {
|
|
|
|
energy: 0.0,
|
|
|
|
energy_full: 0.0,
|
|
|
|
state: battery::State::Full,
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
let actual = ModuleRenderer::new("battery")
|
|
|
|
.config(toml::toml! {
|
|
|
|
[[battery.display]]
|
|
|
|
threshold = 100
|
|
|
|
style = ""
|
|
|
|
})
|
|
|
|
.battery_info_provider(&mock)
|
|
|
|
.collect();
|
|
|
|
let expected = None;
|
|
|
|
|
|
|
|
assert_eq!(expected, actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn battery_full() {
|
|
|
|
let mut mock = MockBatteryInfoProvider::new();
|
|
|
|
|
|
|
|
mock.expect_get_battery_info().times(1).returning(|| {
|
|
|
|
Some(BatteryInfo {
|
|
|
|
energy: 1000.0,
|
|
|
|
energy_full: 1000.0,
|
|
|
|
state: battery::State::Full,
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
let actual = ModuleRenderer::new("battery")
|
|
|
|
.config(toml::toml! {
|
|
|
|
[[battery.display]]
|
|
|
|
threshold = 100
|
|
|
|
style = ""
|
|
|
|
})
|
|
|
|
.battery_info_provider(&mock)
|
|
|
|
.collect();
|
|
|
|
let expected = Some(String::from(" 100% "));
|
|
|
|
|
|
|
|
assert_eq!(expected, actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn battery_charging() {
|
|
|
|
let mut mock = MockBatteryInfoProvider::new();
|
|
|
|
|
|
|
|
mock.expect_get_battery_info().times(1).returning(|| {
|
|
|
|
Some(BatteryInfo {
|
|
|
|
energy: 800.0,
|
|
|
|
energy_full: 1000.0,
|
|
|
|
state: battery::State::Charging,
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
let actual = ModuleRenderer::new("battery")
|
|
|
|
.config(toml::toml! {
|
|
|
|
[[battery.display]]
|
|
|
|
threshold = 90
|
|
|
|
style = ""
|
|
|
|
})
|
|
|
|
.battery_info_provider(&mock)
|
|
|
|
.collect();
|
|
|
|
let expected = Some(String::from(" 80% "));
|
|
|
|
|
|
|
|
assert_eq!(expected, actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn battery_discharging() {
|
|
|
|
let mut mock = MockBatteryInfoProvider::new();
|
|
|
|
|
|
|
|
mock.expect_get_battery_info().times(1).returning(|| {
|
|
|
|
Some(BatteryInfo {
|
|
|
|
energy: 800.0,
|
|
|
|
energy_full: 1000.0,
|
|
|
|
state: battery::State::Discharging,
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
let actual = ModuleRenderer::new("battery")
|
|
|
|
.config(toml::toml! {
|
|
|
|
[[battery.display]]
|
|
|
|
threshold = 100
|
|
|
|
style = ""
|
|
|
|
})
|
|
|
|
.battery_info_provider(&mock)
|
|
|
|
.collect();
|
|
|
|
let expected = Some(String::from(" 80% "));
|
|
|
|
|
|
|
|
assert_eq!(expected, actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn battery_unknown() {
|
|
|
|
let mut mock = MockBatteryInfoProvider::new();
|
|
|
|
|
|
|
|
mock.expect_get_battery_info().times(1).returning(|| {
|
|
|
|
Some(BatteryInfo {
|
|
|
|
energy: 0.0,
|
|
|
|
energy_full: 1.0,
|
|
|
|
state: battery::State::Unknown,
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
let actual = ModuleRenderer::new("battery")
|
|
|
|
.config(toml::toml! {
|
|
|
|
[[battery.display]]
|
|
|
|
threshold = 100
|
|
|
|
style = ""
|
|
|
|
})
|
|
|
|
.battery_info_provider(&mock)
|
|
|
|
.collect();
|
|
|
|
let expected = Some(String::from(" 0% "));
|
|
|
|
|
|
|
|
assert_eq!(expected, actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn battery_empty() {
|
|
|
|
let mut mock = MockBatteryInfoProvider::new();
|
|
|
|
|
|
|
|
mock.expect_get_battery_info().times(1).returning(|| {
|
|
|
|
Some(BatteryInfo {
|
|
|
|
energy: 0.0,
|
|
|
|
energy_full: 1000.0,
|
|
|
|
state: battery::State::Empty,
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
let actual = ModuleRenderer::new("battery")
|
|
|
|
.config(toml::toml! {
|
|
|
|
[[battery.display]]
|
|
|
|
threshold = 100
|
|
|
|
style = ""
|
|
|
|
})
|
|
|
|
.battery_info_provider(&mock)
|
|
|
|
.collect();
|
|
|
|
let expected = Some(String::from(" 0% "));
|
|
|
|
|
|
|
|
assert_eq!(expected, actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn battery_hidden_when_percentage_above_threshold() {
|
|
|
|
let mut mock = MockBatteryInfoProvider::new();
|
|
|
|
|
|
|
|
mock.expect_get_battery_info().times(1).returning(|| {
|
|
|
|
Some(BatteryInfo {
|
|
|
|
energy: 600.0,
|
|
|
|
energy_full: 1000.0,
|
|
|
|
state: battery::State::Full,
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
let actual = ModuleRenderer::new("battery")
|
|
|
|
.config(toml::toml! {
|
|
|
|
[[battery.display]]
|
|
|
|
threshold = 50
|
|
|
|
style = ""
|
|
|
|
})
|
|
|
|
.battery_info_provider(&mock)
|
|
|
|
.collect();
|
|
|
|
let expected = None;
|
|
|
|
|
|
|
|
assert_eq!(expected, actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn battery_uses_style() {
|
|
|
|
let mut mock = MockBatteryInfoProvider::new();
|
|
|
|
|
|
|
|
mock.expect_get_battery_info().times(1).returning(|| {
|
|
|
|
Some(BatteryInfo {
|
|
|
|
energy: 400.0,
|
|
|
|
energy_full: 1000.0,
|
|
|
|
state: battery::State::Discharging,
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
let actual = ModuleRenderer::new("battery")
|
|
|
|
.config(toml::toml! {
|
|
|
|
[[battery.display]]
|
|
|
|
threshold = 50
|
|
|
|
style = "bold red"
|
|
|
|
})
|
|
|
|
.battery_info_provider(&mock)
|
|
|
|
.collect();
|
|
|
|
let expected = Some(format!("{} ", Color::Red.bold().paint(" 40%")));
|
|
|
|
|
|
|
|
assert_eq!(expected, actual);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn battery_displayed_precision() {
|
|
|
|
let mut mock = MockBatteryInfoProvider::new();
|
|
|
|
|
|
|
|
mock.expect_get_battery_info().times(1).returning(|| {
|
|
|
|
Some(BatteryInfo {
|
|
|
|
energy: 129.87654,
|
|
|
|
energy_full: 1000.0,
|
|
|
|
state: battery::State::Discharging,
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
let actual = ModuleRenderer::new("battery")
|
|
|
|
.config(toml::toml! {
|
|
|
|
[[battery.display]]
|
|
|
|
threshold = 100
|
|
|
|
style = ""
|
|
|
|
})
|
|
|
|
.battery_info_provider(&mock)
|
|
|
|
.collect();
|
|
|
|
let expected = Some(String::from(" 13% "));
|
|
|
|
|
|
|
|
assert_eq!(expected, actual);
|
|
|
|
}
|
|
|
|
}
|