use chrono::Local; use chrono_tz::Tz; use glam::Vec3; use smallvec::SmallVec; use std::{ io::Read, process::{self, Stdio}, sync::Arc, time::Instant, }; use crate::{gui::modular::FALLBACK_COLOR, state::AppState}; use serde::Deserialize; use super::{color_parse_or_default, ExecArgs, ModularControl, ModularData}; #[derive(Deserialize)] #[serde(tag = "source")] pub enum LabelContent { Static { text: Arc, }, Exec { command: ExecArgs, interval: f32, }, Clock { format: Arc, timezone: Option>, }, Timer { format: Arc, }, Battery { device: usize, low_threshold: f32, low_color: Arc, charging_color: Arc, }, Ipd, } pub enum LabelData { Battery { device: usize, low_threshold: f32, normal_color: Vec3, low_color: Vec3, charging_color: Vec3, }, Clock { format: Arc, timezone: Option, }, Timer { format: Arc, start: Instant, }, Exec { last_exec: Instant, interval: f32, command: Vec>, child: Option, }, Ipd { last_ipd: f32, }, } pub fn modular_label_init(label: &mut ModularControl, content: &LabelContent) { let state = match content { LabelContent::Battery { device, low_threshold, low_color, charging_color, } => Some(LabelData::Battery { device: *device, low_threshold: *low_threshold, normal_color: label.fg_color, low_color: color_parse_or_default(low_color), charging_color: color_parse_or_default(charging_color), }), LabelContent::Clock { format, timezone } => { let tz: Option = timezone.as_ref().map(|tz| { tz.parse().unwrap_or_else(|_| { log::error!("Failed to parse timezone '{}'", &tz); label.set_fg_color(FALLBACK_COLOR); Tz::UTC }) }); Some(LabelData::Clock { format: format.clone(), timezone: tz, }) } LabelContent::Timer { format } => Some(LabelData::Timer { format: format.clone(), start: Instant::now(), }), LabelContent::Exec { command, interval } => Some(LabelData::Exec { last_exec: Instant::now(), interval: *interval, command: command.clone(), child: None, }), LabelContent::Static { text } => { label.set_text(text); None } LabelContent::Ipd => Some(LabelData::Ipd { last_ipd: 0. }), }; if let Some(state) = state { label.state = Some(ModularData::Label(Box::new(state))); label.on_update = Some(label_update); } } pub(super) fn label_update(control: &mut ModularControl, _: &mut (), app: &mut AppState) { let ModularData::Label(data) = control.state.as_mut().unwrap() else { panic!("Label control has no state"); }; match data.as_mut() { LabelData::Battery { device, low_threshold, normal_color, low_color, charging_color, } => { let device = app.input_state.devices.get(*device); let tags = ["", "H", "L", "R", "T"]; if let Some(device) = device { let (text, color) = device .soc .map(|soc| { let text = format!( "{}{}", tags[device.role as usize], (soc * 100.).min(99.) as u32 ); let color = if device.charging { *charging_color } else if soc < *low_threshold { *low_color } else { *normal_color }; (text, color) }) .unwrap_or_else(|| ("".into(), Vec3::ZERO)); control.set_text(&text); control.set_fg_color(color); } else { control.set_text(""); } } LabelData::Clock { format, timezone } => { let format = format.clone(); if let Some(tz) = timezone { let date = Local::now().with_timezone(tz); control.set_text(&format!("{}", &date.format(&format))); } else { let date = Local::now(); control.set_text(&format!("{}", &date.format(&format))); } } LabelData::Timer { format, start } => { let mut format = format.clone().to_lowercase(); let duration = start.elapsed().as_secs(); format = format.replace("%s", &format!("{:02}", (duration % 60))); format = format.replace("%m", &format!("{:02}", ((duration / 60) % 60))); format = format.replace("%h", &format!("{:02}", ((duration / 60) / 60))); control.set_text(&format); } LabelData::Exec { last_exec, interval, command, child, } => { if let Some(mut proc) = child.take() { match proc.try_wait() { Ok(Some(code)) => { if !code.success() { log::error!("Child process exited with code: {}", code); } else { if let Some(mut stdout) = proc.stdout.take() { let mut buf = String::new(); if stdout.read_to_string(&mut buf).is_ok() { control.set_text(&buf); } else { log::error!("Failed to read stdout for child process"); return; } return; } log::error!("No stdout for child process"); return; } } Ok(None) => { *child = Some(proc); // not exited yet return; } Err(e) => { *child = None; log::error!("Error checking child process: {:?}", e); return; } } } if Instant::now() .saturating_duration_since(*last_exec) .as_secs_f32() > *interval { *last_exec = Instant::now(); let args = command .iter() .map(|s| s.as_ref()) .collect::>(); match process::Command::new(args[0]) .args(&args[1..]) .stdout(Stdio::piped()) .spawn() { Ok(proc) => { *child = Some(proc); } Err(e) => { log::error!("Failed to spawn process {:?}: {:?}", args, e); } }; } } LabelData::Ipd { last_ipd } => { if (app.input_state.ipd - *last_ipd).abs() > 0.05 { *last_ipd = app.input_state.ipd; control.set_text(&format!("{:.1}", app.input_state.ipd)); } } } }