use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use waybar_dynamic_core::protocol::WidgetSpec;
#[derive(Debug, Default, Clone)]
pub struct SharedState(pub Arc<Mutex<Vec<WidgetSpec>>>);
impl SharedState {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(Vec::new())))
}
pub fn set_all(&self, widgets: Vec<WidgetSpec>) {
*self.0.lock().expect("poisoned state mutex") = widgets;
}
pub fn patch(&self, upsert: Vec<WidgetSpec>, remove: Vec<String>) {
let mut list = self.0.lock().expect("poisoned state mutex");
let remove_set: HashSet<&str> = remove.iter().map(|s| s.as_str()).collect();
list.retain(|w| {
w.id.as_deref()
.map(|id| !remove_set.contains(id))
.unwrap_or(true)
});
for spec in upsert {
match &spec.id {
Some(id) => {
if let Some(existing) =
list.iter_mut().find(|w| w.id.as_deref() == Some(id))
{
*existing = spec;
} else {
list.push(spec);
}
}
None => list.push(spec),
}
}
}
pub fn clear(&self) {
self.0.lock().expect("poisoned state mutex").clear();
}
pub fn snapshot(&self) -> Vec<WidgetSpec> {
self.0.lock().expect("poisoned state mutex").clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn widget(id: &str, label: &str) -> WidgetSpec {
WidgetSpec {
id: Some(id.to_string()),
label: label.to_string(),
classes: vec![],
tooltip: None,
on_click: None,
on_right_click: None,
on_middle_click: None,
}
}
fn anon_widget(label: &str) -> WidgetSpec {
WidgetSpec {
id: None,
label: label.to_string(),
classes: vec![],
tooltip: None,
on_click: None,
on_right_click: None,
on_middle_click: None,
}
}
#[test]
fn set_all_replaces_state() {
let state = SharedState::new();
state.set_all(vec![widget("a", "A")]);
assert_eq!(state.snapshot().len(), 1);
state.set_all(vec![widget("b", "B"), widget("c", "C")]);
let snap = state.snapshot();
assert_eq!(snap.len(), 2);
assert_eq!(snap[0].label, "B");
}
#[test]
fn clear_empties_state() {
let state = SharedState::new();
state.set_all(vec![widget("a", "A")]);
state.clear();
assert!(state.snapshot().is_empty());
}
#[test]
fn patch_upsert_new() {
let state = SharedState::new();
state.set_all(vec![widget("a", "A")]);
state.patch(vec![widget("b", "B")], vec![]);
let snap = state.snapshot();
assert_eq!(snap.len(), 2);
}
#[test]
fn patch_upsert_existing() {
let state = SharedState::new();
state.set_all(vec![widget("a", "old")]);
state.patch(vec![widget("a", "new")], vec![]);
let snap = state.snapshot();
assert_eq!(snap.len(), 1);
assert_eq!(snap[0].label, "new");
}
#[test]
fn patch_remove() {
let state = SharedState::new();
state.set_all(vec![widget("a", "A"), widget("b", "B"), widget("c", "C")]);
state.patch(vec![], vec!["b".to_string()]);
let snap = state.snapshot();
assert_eq!(snap.len(), 2);
assert!(snap.iter().all(|w| w.id.as_deref() != Some("b")));
}
#[test]
fn patch_upsert_and_remove() {
let state = SharedState::new();
state.set_all(vec![widget("a", "A"), widget("b", "B")]);
state.patch(vec![widget("c", "C")], vec!["a".to_string()]);
let snap = state.snapshot();
assert_eq!(snap.len(), 2);
let ids: Vec<_> = snap.iter().filter_map(|w| w.id.as_deref()).collect();
assert!(ids.contains(&"b"));
assert!(ids.contains(&"c"));
}
#[test]
fn patch_anonymous_widgets_appended() {
let state = SharedState::new();
state.set_all(vec![widget("a", "A")]);
state.patch(vec![anon_widget("X"), anon_widget("Y")], vec![]);
assert_eq!(state.snapshot().len(), 3);
}
#[test]
fn patch_empty_is_noop() {
let state = SharedState::new();
state.set_all(vec![widget("a", "A")]);
state.patch(vec![], vec![]);
assert_eq!(state.snapshot().len(), 1);
}
#[test]
fn patch_remove_nonexistent_is_noop() {
let state = SharedState::new();
state.set_all(vec![widget("a", "A")]);
state.patch(vec![], vec!["zzz".to_string()]);
assert_eq!(state.snapshot().len(), 1);
}
}