i3status_rs/blocks/
notify.rs1use super::prelude::*;
65use tokio::{join, try_join};
66use zbus::proxy::PropertyStream;
67
68#[derive(Deserialize, Debug, Default)]
69#[serde(deny_unknown_fields, default)]
70pub struct Config {
71 pub driver: DriverType,
72 pub format: FormatConfig,
73}
74
75#[derive(Deserialize, Debug, SmartDefault)]
76#[serde(rename_all = "lowercase")]
77pub enum DriverType {
78 #[default]
79 Dunst,
80 SwayNC,
81}
82
83pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
84 let format = config.format.with_default(" $icon ")?;
85 BlockPlan::new(vec![
86 OutputPlan::new("enabled", format.clone()).icon("icon", IconChoices::one(icons::BELL)),
87 OutputPlan::new("paused", format).icon("icon", IconChoices::one(icons::BELL_SLASH)),
90 ])
91}
92
93pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
94 let mut actions = api.get_actions()?;
95 api.set_default_actions(&[(MouseButton::Left, None, "toggle_paused")])?;
96
97 let output_enabled = plan.output("enabled")?;
98 let output_paused = plan.output("paused")?;
99
100 let mut driver: Box<dyn Driver> = match config.driver {
101 DriverType::Dunst => Box::new(DunstDriver::new().await?),
102 DriverType::SwayNC => Box::new(SwayNCDriver::new().await?),
103 };
104
105 loop {
106 let (is_paused, notification_count, history_count) = try_join!(
107 driver.is_paused(),
108 driver.notification_count(),
109 driver.history_count()
110 )?;
111
112 let output = if is_paused {
113 &output_paused
114 } else {
115 &output_enabled
116 };
117 let mut widget = output.new_widget();
118 widget.set_values(map!(
119 "icon" => output.icon_value("icon")?,
120 [if notification_count != 0] "notification_count" => Value::number(notification_count),
121 [if history_count != 0] "history_count" => Value::number(history_count),
122 [if is_paused] "paused" => Value::flag(),
123 ));
124 widget.state = if notification_count == 0 {
125 State::Idle
126 } else {
127 State::Info
128 };
129 api.set_widget(widget)?;
130
131 select! {
132 x = driver.wait_for_change() => x?,
133 Some(action) = actions.recv() => match action.as_ref() {
134 "toggle_paused" => {
135 driver.set_paused(!is_paused).await?;
136 }
137 "show" => {
138 driver.notification_show().await?;
139 }
140 "show_all" => {
141 driver.notification_show_all().await?;
142 }
143 _ => (),
144 }
145 }
146 }
147}
148
149#[async_trait]
150trait Driver {
151 async fn is_paused(&self) -> Result<bool>;
152 async fn set_paused(&self, paused: bool) -> Result<()>;
153 async fn notification_show(&self) -> Result<()>;
154 async fn history_count(&self) -> Result<u32>;
155 async fn notification_show_all(&self) -> Result<()>;
156 async fn notification_count(&self) -> Result<u32>;
157 async fn wait_for_change(&mut self) -> Result<()>;
158}
159
160struct DunstDriver {
161 proxy: DunstDbusProxy<'static>,
162 paused_changes: PropertyStream<'static, bool>,
163 displayed_length_changes: PropertyStream<'static, u32>,
164 waiting_length_changes: PropertyStream<'static, u32>,
165}
166
167impl DunstDriver {
168 async fn new() -> Result<Self> {
169 let dbus_conn = new_dbus_connection().await?;
170 let proxy = DunstDbusProxy::new(&dbus_conn)
171 .await
172 .error("Failed to create DunstDbusProxy")?;
173 Ok(Self {
174 paused_changes: proxy.receive_paused_changed().await,
175 displayed_length_changes: proxy.receive_displayed_length_changed().await,
176 waiting_length_changes: proxy.receive_waiting_length_changed().await,
177 proxy,
178 })
179 }
180}
181
182#[async_trait]
183impl Driver for DunstDriver {
184 async fn is_paused(&self) -> Result<bool> {
185 self.proxy.paused().await.error("Failed to get 'paused'")
186 }
187
188 async fn set_paused(&self, paused: bool) -> Result<()> {
189 self.proxy
190 .set_paused(paused)
191 .await
192 .error("Failed to set 'paused'")
193 }
194
195 async fn notification_show(&self) -> Result<()> {
196 self.proxy
197 .notification_show()
198 .await
199 .error("Could not call 'NotificationShow'")
200 }
201
202 async fn notification_show_all(&self) -> Result<()> {
203 for _ in 0..self.history_count().await? {
204 self.notification_show().await?;
205 }
206 Ok(())
207 }
208
209 async fn history_count(&self) -> Result<u32> {
210 let history_length = self
211 .proxy
212 .history_length()
213 .await
214 .error("Failed to get property")?;
215
216 Ok(history_length)
217 }
218
219 async fn notification_count(&self) -> Result<u32> {
220 let (displayed_length, waiting_length) =
221 try_join!(self.proxy.displayed_length(), self.proxy.waiting_length())
222 .error("Failed to get property")?;
223
224 Ok(displayed_length + waiting_length)
225 }
226
227 async fn wait_for_change(&mut self) -> Result<()> {
228 select! {
229 _ = self.paused_changes.next() => {}
230 _ = self.displayed_length_changes.next() => {}
231 _ = self.waiting_length_changes.next() => {}
232 }
233 Ok(())
234 }
235}
236
237#[zbus::proxy(
238 interface = "org.dunstproject.cmd0",
239 default_service = "org.freedesktop.Notifications",
240 default_path = "/org/freedesktop/Notifications"
241)]
242
243trait DunstDbus {
244 #[zbus(property, name = "paused")]
245 fn paused(&self) -> zbus::Result<bool>;
246 #[zbus(property, name = "paused")]
247 fn set_paused(&self, value: bool) -> zbus::Result<()>;
248 fn notification_show(&self) -> zbus::Result<()>;
249 #[zbus(property, name = "historyLength")]
250 fn history_length(&self) -> zbus::Result<u32>;
251 #[zbus(property, name = "displayedLength")]
252 fn displayed_length(&self) -> zbus::Result<u32>;
253 #[zbus(property, name = "waitingLength")]
254 fn waiting_length(&self) -> zbus::Result<u32>;
255}
256struct SwayNCDriver {
257 proxy: SwayNCDbusProxy<'static>,
258 changes: SubscribeStream,
259 changes_v2: SubscribeV2Stream,
260}
261
262impl SwayNCDriver {
263 async fn new() -> Result<Self> {
264 let dbus_conn = new_dbus_connection().await?;
265 let proxy = SwayNCDbusProxy::new(&dbus_conn)
266 .await
267 .error("Failed to create SwayNCDbusProxy")?;
268 Ok(Self {
269 changes: proxy
270 .receive_subscribe()
271 .await
272 .error("Failed to create SubscribeStream")?,
273 changes_v2: proxy
274 .receive_subscribe_v2()
275 .await
276 .error("Failed to create SubscribeV2Stream")?,
277 proxy,
278 })
279 }
280}
281
282#[async_trait]
283impl Driver for SwayNCDriver {
284 async fn is_paused(&self) -> Result<bool> {
285 let (is_dnd, is_inhibited) = join!(self.proxy.get_dnd(), self.proxy.is_inhibited());
286
287 is_dnd
288 .error("Failed to call 'GetDnd'")
289 .map(|is_dnd| is_dnd || is_inhibited.unwrap_or_default())
290 }
291
292 async fn set_paused(&self, paused: bool) -> Result<()> {
293 if paused {
294 self.proxy.set_dnd(paused).await
295 } else {
296 join!(self.proxy.set_dnd(paused), self.proxy.clear_inhibitors()).0
297 }
298 .error("Failed to call 'SetDnd'")
299 }
300
301 async fn notification_show(&self) -> Result<()> {
302 self.proxy
303 .toggle_visibility()
304 .await
305 .error("Failed to call 'ToggleVisibility'")
306 }
307
308 async fn notification_show_all(&self) -> Result<()> {
309 self.notification_show().await
310 }
311
312 async fn history_count(&self) -> Result<u32> {
313 self.notification_count().await
314 }
315
316 async fn notification_count(&self) -> Result<u32> {
317 self.proxy
318 .notification_count()
319 .await
320 .error("Failed to call 'NotificationCount'")
321 }
322
323 async fn wait_for_change(&mut self) -> Result<()> {
324 select! {
325 _ = self.changes.next() => (),
326 _ = self.changes_v2.next() => (),
327 }
328 Ok(())
329 }
330}
331
332#[zbus::proxy(
333 interface = "org.erikreider.swaync.cc",
334 default_service = "org.freedesktop.Notifications",
335 default_path = "/org/erikreider/swaync/cc"
336)]
337trait SwayNCDbus {
338 fn get_dnd(&self) -> zbus::Result<bool>;
339 fn set_dnd(&self, value: bool) -> zbus::Result<()>;
340 fn toggle_visibility(&self) -> zbus::Result<()>;
341 fn notification_count(&self) -> zbus::Result<u32>;
342 #[zbus(signal)]
343 fn subscribe(&self, count: u32, dnd: bool, cc_open: bool) -> zbus::Result<()>;
344
345 fn is_inhibited(&self) -> zbus::Result<bool>;
347 fn clear_inhibitors(&self) -> zbus::Result<bool>;
348 #[zbus(signal)]
350 fn subscribe_v2(
351 &self,
352 count: u32,
353 dnd: bool,
354 cc_open: bool,
355 inhibited: bool,
356 ) -> zbus::Result<()>;
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn plan_declares_both_states_with_their_icons() {
365 let plan = prepare(&Config::default()).unwrap();
366 let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
367 assert_eq!(ids, ["enabled", "paused"]);
368 assert_eq!(
369 plan.output("enabled").unwrap().single_icon("icon").unwrap(),
370 icons::BELL
371 );
372 assert_eq!(
373 plan.output("paused").unwrap().single_icon("icon").unwrap(),
374 icons::BELL_SLASH
375 );
376 }
377
378 #[test]
379 fn both_states_share_the_same_format() {
380 let config = Config {
381 format: " $icon $notification_count ".parse().unwrap(),
382 ..Config::default()
383 };
384 let plan = prepare(&config).unwrap();
385 for id in ["enabled", "paused"] {
386 assert!(
387 plan.output(id)
388 .unwrap()
389 .format()
390 .contains_key("notification_count")
391 );
392 }
393 }
394}