use crate::config::LabelPrefix; use crate::render::RenderConfig; use crate::selected_frame::SelectableFrame; use crate::widgets::widget::BarWidget; use eframe::egui::Align; use eframe::egui::Context; use eframe::egui::Label; use eframe::egui::TextFormat; use eframe::egui::Ui; use eframe::egui::text::LayoutJob; use serde::Deserialize; use serde::Serialize; use starship_battery::Manager; use starship_battery::State; use starship_battery::units::ratio::percent; use std::process::Command; use std::time::Duration; use std::time::Instant; #[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] /// Battery widget configuration pub struct BatteryConfig { /// Enable the Battery widget pub enable: bool, /// Hide the widget if the battery is at full charge pub hide_on_full_charge: Option, /// Data refresh interval in seconds #[cfg_attr(feature = "schemars", schemars(extend("default" = 10)))] pub data_refresh_interval: Option, /// Display label prefix pub label_prefix: Option, /// Select when the current percentage is under this value [[1-100]] pub auto_select_under: Option, } impl From for Battery { fn from(value: BatteryConfig) -> Self { let data_refresh_interval = value.data_refresh_interval.unwrap_or(10); Self { enable: value.enable, hide_on_full_charge: value.hide_on_full_charge.unwrap_or(false), manager: Manager::new().unwrap(), last_state: None, data_refresh_interval, label_prefix: value.label_prefix.unwrap_or(LabelPrefix::Icon), auto_select_under: value.auto_select_under.map(|u| u.clamp(1, 100)), state: BatteryState::Discharging, last_updated: Instant::now() .checked_sub(Duration::from_secs(data_refresh_interval)) .unwrap(), } } } pub enum BatteryState { Charging, Discharging, High, Medium, Low, Warning, } #[derive(Clone, Debug)] struct BatteryOutput { label: String, selected: bool, } pub struct Battery { pub enable: bool, hide_on_full_charge: bool, manager: Manager, pub state: BatteryState, data_refresh_interval: u64, label_prefix: LabelPrefix, auto_select_under: Option, last_state: Option, last_updated: Instant, } impl Battery { fn output(&mut self) -> Option { let mut output = self.last_state.clone(); let now = Instant::now(); if now.duration_since(self.last_updated) > Duration::from_secs(self.data_refresh_interval) { output = None; if let Ok(mut batteries) = self.manager.batteries() && let Some(Ok(first)) = batteries.nth(0) { let percentage = first.state_of_charge().get::().round() as u8; if percentage == 100 && self.hide_on_full_charge { output = None } else { match first.state() { State::Charging => self.state = BatteryState::Charging, State::Discharging => { self.state = match percentage { p if p > 75 => BatteryState::Discharging, p if p > 50 => BatteryState::High, p if p > 25 => BatteryState::Medium, p if p > 10 => BatteryState::Low, _ => BatteryState::Warning, } } _ => {} } let selected = self.auto_select_under.is_some_and(|u| percentage <= u); output = Some(BatteryOutput { label: match self.label_prefix { LabelPrefix::Text | LabelPrefix::IconAndText => { format!("BAT: {percentage}%") } LabelPrefix::None | LabelPrefix::Icon => { format!("{percentage}%") } }, selected, }) } } self.last_state.clone_from(&output); self.last_updated = now; } output } } impl BarWidget for Battery { fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) { if self.enable { let output = self.output(); if let Some(output) = output { let emoji = match self.state { BatteryState::Charging => egui_phosphor::regular::BATTERY_CHARGING, BatteryState::Discharging => egui_phosphor::regular::BATTERY_FULL, BatteryState::High => egui_phosphor::regular::BATTERY_HIGH, BatteryState::Medium => egui_phosphor::regular::BATTERY_MEDIUM, BatteryState::Low => egui_phosphor::regular::BATTERY_LOW, BatteryState::Warning => egui_phosphor::regular::BATTERY_WARNING, }; let auto_text_color = config.auto_select_text.filter(|_| output.selected); let mut layout_job = LayoutJob::simple( match self.label_prefix { LabelPrefix::Icon | LabelPrefix::IconAndText => emoji.to_string(), LabelPrefix::None | LabelPrefix::Text => String::new(), }, config.icon_font_id.clone(), auto_text_color.unwrap_or(ctx.style().visuals.selection.stroke.color), 100.0, ); layout_job.append( &output.label, 10.0, TextFormat { font_id: config.text_font_id.clone(), color: auto_text_color.unwrap_or(ctx.style().visuals.text_color()), valign: Align::Center, ..Default::default() }, ); let auto_focus_fill = config.auto_select_fill; config.apply_on_widget(false, ui, |ui| { if SelectableFrame::new_auto(output.selected, auto_focus_fill) .show(ui, |ui| ui.add(Label::new(layout_job).selectable(false))) .clicked() && let Err(error) = Command::new("cmd.exe") .args(["/C", "start", "ms-settings:batterysaver"]) .spawn() { eprintln!("{error}") } }); } } } }