use serde::Serialize;
const WAYBAR_REFRESH_SIGNAL_OFFSET: i32 = 11;
const ICON_BELL_OFF: &str = "\u{f06d9}";
const ICON_BELL_BADGE: &str = "\u{f009a}";
const ICON_BELL_OUTLINE: &str = "\u{f009c}";
fn waybar_refresh_signal() -> i32 {
libc::SIGRTMIN() + WAYBAR_REFRESH_SIGNAL_OFFSET
}
#[derive(Serialize)]
struct WaybarStatus {
text: String,
tooltip: String,
alt: String,
class: String,
count: usize,
}
fn build_status(unread: usize, dnd: bool) -> WaybarStatus {
if dnd {
WaybarStatus {
text: ICON_BELL_OFF.into(),
tooltip: "Do Not Disturb".into(),
alt: "dnd".into(),
class: "dnd".into(),
count: unread,
}
} else if unread > 0 {
WaybarStatus {
text: format!("{ICON_BELL_BADGE} {unread}"),
tooltip: format!(
"{unread} unread notification{}",
if unread == 1 { "" } else { "s" }
),
alt: "unread".into(),
class: "unread".into(),
count: unread,
}
} else {
WaybarStatus {
text: ICON_BELL_OUTLINE.into(),
tooltip: "No notifications".into(),
alt: "empty".into(),
class: "empty".into(),
count: 0,
}
}
}
pub(crate) fn update_status(unread: usize, dnd: bool) {
let status = build_status(unread, dnd);
let path = crate::paths::status_path();
match serde_json::to_string(&status) {
Ok(json) => {
if let Err(e) = std::fs::write(&path, json) {
log::error!("Failed to write waybar status: {e}");
}
}
Err(e) => log::error!("Failed to serialize waybar status: {e}"),
}
signal_waybar();
}
fn signal_waybar() {
let signal_num = waybar_refresh_signal();
match std::process::Command::new("pkill")
.arg(format!("-{signal_num}"))
.arg("waybar")
.status()
{
Err(e) => log::debug!("Failed to signal waybar: {e}"),
Ok(s) if !s.success() => log::debug!("No waybar process to signal"),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn status_json_includes_count_field() {
let status = WaybarStatus {
text: "x".into(),
tooltip: "t".into(),
alt: "a".into(),
class: "c".into(),
count: 7,
};
let json = serde_json::to_string(&status).expect("serialize");
assert!(
json.contains("\"count\":7"),
"expected count field in JSON, got: {json}"
);
}
#[test]
fn build_status_dnd_branch_uses_bell_off_glyph() {
let s = build_status(5, true);
assert_eq!(s.text, ICON_BELL_OFF);
assert_eq!(s.tooltip, "Do Not Disturb");
assert_eq!(s.alt, "dnd");
assert_eq!(s.class, "dnd");
assert_eq!(s.count, 5);
}
#[test]
fn build_status_singular_unread_uses_singular_tooltip() {
let s = build_status(1, false);
assert_eq!(s.text, format!("{ICON_BELL_BADGE} 1"));
assert_eq!(s.tooltip, "1 unread notification");
assert_eq!(s.alt, "unread");
assert_eq!(s.class, "unread");
assert_eq!(s.count, 1);
}
#[test]
fn build_status_plural_unread_uses_plural_tooltip() {
let s = build_status(5, false);
assert_eq!(s.text, format!("{ICON_BELL_BADGE} 5"));
assert_eq!(s.tooltip, "5 unread notifications");
assert_eq!(s.alt, "unread");
assert_eq!(s.class, "unread");
assert_eq!(s.count, 5);
}
#[test]
fn build_status_empty_branch_uses_bell_outline() {
let s = build_status(0, false);
assert_eq!(s.text, ICON_BELL_OUTLINE);
assert_eq!(s.tooltip, "No notifications");
assert_eq!(s.alt, "empty");
assert_eq!(s.class, "empty");
assert_eq!(s.count, 0);
}
#[test]
fn waybar_refresh_signal_is_sigrtmin_plus_offset() {
let s = waybar_refresh_signal();
let base = libc::SIGRTMIN();
assert_eq!(
s,
base + WAYBAR_REFRESH_SIGNAL_OFFSET,
"expected SIGRTMIN({base}) + {WAYBAR_REFRESH_SIGNAL_OFFSET}, got {s}"
);
assert!(s < libc::SIGRTMAX(), "signal {s} exceeds SIGRTMAX");
}
}