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