1use serde::Serialize;
8
9pub const REFRESH_SIGNAL: &str = "-RTMIN+13";
11
12pub const PROCESS_NAME: &str = "waybar";
14
15pub fn request_refresh() {
17 let _ = std::process::Command::new("pkill")
18 .args([REFRESH_SIGNAL, PROCESS_NAME])
19 .status();
20}
21
22#[derive(Debug, Clone, Serialize)]
23pub struct WaybarOutput {
24 pub text: String,
25 pub tooltip: String,
26 pub class: Class,
27}
28
29#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
30#[serde(rename_all = "lowercase")]
31pub enum Class {
32 Low,
33 Mid,
34 High,
35 Critical,
36}
37
38impl From<crate::pacing::PaceSeverity> for Class {
39 fn from(s: crate::pacing::PaceSeverity) -> Self {
40 match s {
41 crate::pacing::PaceSeverity::Low => Class::Low,
42 crate::pacing::PaceSeverity::Mid => Class::Mid,
43 crate::pacing::PaceSeverity::High => Class::High,
44 crate::pacing::PaceSeverity::Critical => Class::Critical,
45 }
46 }
47}
48
49impl WaybarOutput {
50 pub fn to_json_line(&self) -> String {
52 format!("{}\n", serde_json::to_string(self).unwrap_or_default())
55 }
56
57 pub fn error(msg: &str) -> Self {
60 Self {
61 text: "⚠".into(),
62 tooltip: msg.into(),
63 class: Class::Critical,
64 }
65 }
66
67 pub fn loading(prefix_icon: Option<&str>) -> Self {
71 let text = match prefix_icon {
72 Some(ic) if !ic.is_empty() => format!("{ic} Loading…"),
73 _ => "Loading…".to_string(),
74 };
75 Self {
76 text,
77 tooltip: "Usage data is waiting for network.\nWill retry on the next Waybar update."
78 .into(),
79 class: Class::Low,
80 }
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use crate::pacing::PaceSeverity;
88
89 #[test]
90 fn serializes_compact_json() {
91 let out = WaybarOutput {
92 text: "42% · 1h 30m".into(),
93 tooltip: "<b>Claude Max 5x</b>".into(),
94 class: Class::Mid,
95 };
96 let line = out.to_json_line();
97 assert!(line.ends_with('\n'));
98 assert!(line.contains(r#""class":"mid""#));
99 assert!(line.contains(r#""text":"42% · 1h 30m""#));
100 }
101
102 #[test]
103 fn class_serializes_lowercase() {
104 assert_eq!(
105 serde_json::to_string(&Class::Critical).unwrap(),
106 r#""critical""#
107 );
108 }
109
110 #[test]
111 fn severity_maps_to_class() {
112 assert_eq!(Class::from(PaceSeverity::Low), Class::Low);
113 assert_eq!(Class::from(PaceSeverity::Mid), Class::Mid);
114 assert_eq!(Class::from(PaceSeverity::High), Class::High);
115 assert_eq!(Class::from(PaceSeverity::Critical), Class::Critical);
116 }
117
118 #[test]
119 fn error_helper_always_valid() {
120 let line = WaybarOutput::error("Token expired\nRun claude").to_json_line();
121 let v: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
123 assert_eq!(v["text"], "⚠");
124 assert_eq!(v["class"], "critical");
125 assert!(v["tooltip"].as_str().unwrap().contains("Token expired"));
126 }
127
128 #[test]
129 fn loading_with_icon_prepends() {
130 let l = WaybarOutput::loading(Some(""));
131 assert!(l.text.starts_with(" "));
132 assert!(l.text.contains("Loading"));
133 }
134
135 #[test]
136 fn loading_without_icon_is_plain() {
137 let l = WaybarOutput::loading(None);
138 assert_eq!(l.text, "Loading…");
139 }
140}