use serde::Serialize;
pub const REFRESH_SIGNAL: &str = "-RTMIN+13";
pub const PROCESS_NAME: &str = "waybar";
pub fn request_refresh() {
#[cfg(unix)]
{
let _ = std::process::Command::new("pkill")
.args([REFRESH_SIGNAL, PROCESS_NAME])
.status();
}
}
#[derive(Debug, Clone, Serialize)]
pub struct WaybarOutput {
pub text: String,
pub tooltip: String,
pub class: Class,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Class {
Low,
Mid,
High,
Critical,
}
impl From<crate::pacing::PaceSeverity> for Class {
fn from(s: crate::pacing::PaceSeverity) -> Self {
match s {
crate::pacing::PaceSeverity::Low => Class::Low,
crate::pacing::PaceSeverity::Mid => Class::Mid,
crate::pacing::PaceSeverity::High => Class::High,
crate::pacing::PaceSeverity::Critical => Class::Critical,
}
}
}
impl WaybarOutput {
pub fn to_json_line(&self) -> String {
format!("{}\n", serde_json::to_string(self).unwrap_or_default())
}
pub fn error(msg: &str) -> Self {
Self {
text: "⚠".into(),
tooltip: msg.into(),
class: Class::Critical,
}
}
pub fn loading(prefix_icon: Option<&str>) -> Self {
let text = match prefix_icon {
Some(ic) if !ic.is_empty() => format!("{ic} Loading…"),
_ => "Loading…".to_string(),
};
Self {
text,
tooltip: "Usage data is waiting for network.\nWill retry on the next Waybar update."
.into(),
class: Class::Low,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pacing::PaceSeverity;
#[test]
fn serializes_compact_json() {
let out = WaybarOutput {
text: "42% · 1h 30m".into(),
tooltip: "<b>Claude Max 5x</b>".into(),
class: Class::Mid,
};
let line = out.to_json_line();
assert!(line.ends_with('\n'));
assert!(line.contains(r#""class":"mid""#));
assert!(line.contains(r#""text":"42% · 1h 30m""#));
}
#[test]
fn class_serializes_lowercase() {
assert_eq!(
serde_json::to_string(&Class::Critical).unwrap(),
r#""critical""#
);
}
#[test]
fn severity_maps_to_class() {
assert_eq!(Class::from(PaceSeverity::Low), Class::Low);
assert_eq!(Class::from(PaceSeverity::Mid), Class::Mid);
assert_eq!(Class::from(PaceSeverity::High), Class::High);
assert_eq!(Class::from(PaceSeverity::Critical), Class::Critical);
}
#[test]
fn error_helper_always_valid() {
let line = WaybarOutput::error("Token expired\nRun claude").to_json_line();
let v: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
assert_eq!(v["text"], "⚠");
assert_eq!(v["class"], "critical");
assert!(v["tooltip"].as_str().unwrap().contains("Token expired"));
}
#[test]
fn loading_with_icon_prepends() {
let l = WaybarOutput::loading(Some(""));
assert!(l.text.starts_with(" "));
assert!(l.text.contains("Loading"));
}
#[test]
fn loading_without_icon_is_plain() {
let l = WaybarOutput::loading(None);
assert_eq!(l.text, "Loading…");
}
}