Skip to main content

i3status_rs/blocks/
notify.rs

1//! Display and toggle the state of notifications daemon
2//!
3//! Left-clicking on this block will enable/disable notifications.
4//!
5//! # Configuration
6//!
7//! Key | Values | Default
8//! ----|--------|--------
9//! `driver` | Which notifications daemon is running. Available drivers are: `"dunst"` and `"swaync"` | `"dunst"`
10//! `format` | A string to customise the output of this block. See below for available placeholders. | `" $icon "`
11//!
12//! Placeholder                               | Value                                                 | Type   | Unit
13//! ------------------------------------------|-------------------------------------------------------|--------|-----
14//! `icon`                                    | Icon based on notification's state                    | Icon   | -
15//! `notification_count`[^dunst_version_note] | The number of notification (omitted if 0)             | Number | -
16//! `history_count`[^history_count_note]      | The number of notification in history (omitted if 0)  | Number | -
17//! `paused`                                  | Present only if notifications are disabled            | Flag   | -
18//!
19//! Action          | Default button
20//! ----------------|---------------
21//! `toggle_paused` | Left
22//! `show`          | -
23//! `show_all`      | -
24//!
25//! The `show` and `show_all` actions are the same for SwayNC.
26//!
27//! # Examples
28//!
29//! How to use `paused` flag
30//!
31//! ```toml
32//! [[block]]
33//! block = "notify"
34//! format = " $icon {$paused{Off}|On} "
35//! ```
36//! How to use `notification_count`
37//!
38//! ```toml
39//! [[block]]
40//! block = "notify"
41//! format = " $icon {($notification_count.eng(w:1)) |}"
42//! ```
43//! How to remap actions
44//!
45//! ```toml
46//! [[block]]
47//! block = "notify"
48//! driver = "swaync"
49//! [[block.click]]
50//! button = "left"
51//! action = "show"
52//! [[block.click]]
53//! button = "right"
54//! action = "toggle_paused"
55//! ```
56//!
57//! # Icons Used
58//! - `bell` (`$icon`)
59//! - `bell-slash` (`$icon`)
60//!
61//! [^dunst_version_note]: when using `notification_count` with the `dunst` driver use dunst > 1.9.0
62//! [^history_count_note]: `history_count` is the same as `notification_count` in SwayNC
63
64use 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        // The paused output is only ever rendered when `is_paused` is true,
88        // so the `paused` flag is set on every render of this output.
89        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    // inhibitors were introduced in v0.8.0
346    fn is_inhibited(&self) -> zbus::Result<bool>;
347    fn clear_inhibitors(&self) -> zbus::Result<bool>;
348    // subscribe_v2 replaced subscribe in v0.8.0
349    #[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}