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`
59//! - `bell-slash`
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
68const ICON_ON: &str = "bell";
69const ICON_OFF: &str = "bell-slash";
70
71#[derive(Deserialize, Debug, Default)]
72#[serde(deny_unknown_fields, default)]
73pub struct Config {
74    pub driver: DriverType,
75    pub format: FormatConfig,
76}
77
78#[derive(Deserialize, Debug, SmartDefault)]
79#[serde(rename_all = "lowercase")]
80pub enum DriverType {
81    #[default]
82    Dunst,
83    SwayNC,
84}
85
86pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
87    let mut actions = api.get_actions()?;
88    api.set_default_actions(&[(MouseButton::Left, None, "toggle_paused")])?;
89
90    let format = config.format.with_default(" $icon ")?;
91
92    let mut driver: Box<dyn Driver> = match config.driver {
93        DriverType::Dunst => Box::new(DunstDriver::new().await?),
94        DriverType::SwayNC => Box::new(SwayNCDriver::new().await?),
95    };
96
97    loop {
98        let (is_paused, notification_count, history_count) = try_join!(
99            driver.is_paused(),
100            driver.notification_count(),
101            driver.history_count()
102        )?;
103
104        let mut widget = Widget::new().with_format(format.clone());
105        widget.set_values(map!(
106            "icon" => Value::icon(if is_paused { ICON_OFF } else { ICON_ON }),
107            [if notification_count != 0] "notification_count" => Value::number(notification_count),
108            [if history_count != 0] "history_count" => Value::number(history_count),
109            [if is_paused] "paused" => Value::flag(),
110        ));
111        widget.state = if notification_count == 0 {
112            State::Idle
113        } else {
114            State::Info
115        };
116        api.set_widget(widget)?;
117
118        select! {
119            x = driver.wait_for_change() => x?,
120            Some(action) = actions.recv() => match action.as_ref() {
121                "toggle_paused" => {
122                    driver.set_paused(!is_paused).await?;
123                }
124                "show" => {
125                    driver.notification_show().await?;
126                }
127                "show_all" => {
128                    driver.notification_show_all().await?;
129                }
130                _ => (),
131            }
132        }
133    }
134}
135
136#[async_trait]
137trait Driver {
138    async fn is_paused(&self) -> Result<bool>;
139    async fn set_paused(&self, paused: bool) -> Result<()>;
140    async fn notification_show(&self) -> Result<()>;
141    async fn history_count(&self) -> Result<u32>;
142    async fn notification_show_all(&self) -> Result<()>;
143    async fn notification_count(&self) -> Result<u32>;
144    async fn wait_for_change(&mut self) -> Result<()>;
145}
146
147struct DunstDriver {
148    proxy: DunstDbusProxy<'static>,
149    paused_changes: PropertyStream<'static, bool>,
150    displayed_length_changes: PropertyStream<'static, u32>,
151    waiting_length_changes: PropertyStream<'static, u32>,
152}
153
154impl DunstDriver {
155    async fn new() -> Result<Self> {
156        let dbus_conn = new_dbus_connection().await?;
157        let proxy = DunstDbusProxy::new(&dbus_conn)
158            .await
159            .error("Failed to create DunstDbusProxy")?;
160        Ok(Self {
161            paused_changes: proxy.receive_paused_changed().await,
162            displayed_length_changes: proxy.receive_displayed_length_changed().await,
163            waiting_length_changes: proxy.receive_waiting_length_changed().await,
164            proxy,
165        })
166    }
167}
168
169#[async_trait]
170impl Driver for DunstDriver {
171    async fn is_paused(&self) -> Result<bool> {
172        self.proxy.paused().await.error("Failed to get 'paused'")
173    }
174
175    async fn set_paused(&self, paused: bool) -> Result<()> {
176        self.proxy
177            .set_paused(paused)
178            .await
179            .error("Failed to set 'paused'")
180    }
181
182    async fn notification_show(&self) -> Result<()> {
183        self.proxy
184            .notification_show()
185            .await
186            .error("Could not call 'NotificationShow'")
187    }
188
189    async fn notification_show_all(&self) -> Result<()> {
190        for _ in 0..self.history_count().await? {
191            self.notification_show().await?;
192        }
193        Ok(())
194    }
195
196    async fn history_count(&self) -> Result<u32> {
197        let history_length = self
198            .proxy
199            .history_length()
200            .await
201            .error("Failed to get property")?;
202
203        Ok(history_length)
204    }
205
206    async fn notification_count(&self) -> Result<u32> {
207        let (displayed_length, waiting_length) =
208            try_join!(self.proxy.displayed_length(), self.proxy.waiting_length())
209                .error("Failed to get property")?;
210
211        Ok(displayed_length + waiting_length)
212    }
213
214    async fn wait_for_change(&mut self) -> Result<()> {
215        select! {
216            _ = self.paused_changes.next() => {}
217            _ = self.displayed_length_changes.next() => {}
218            _ = self.waiting_length_changes.next() => {}
219        }
220        Ok(())
221    }
222}
223
224#[zbus::proxy(
225    interface = "org.dunstproject.cmd0",
226    default_service = "org.freedesktop.Notifications",
227    default_path = "/org/freedesktop/Notifications"
228)]
229
230trait DunstDbus {
231    #[zbus(property, name = "paused")]
232    fn paused(&self) -> zbus::Result<bool>;
233    #[zbus(property, name = "paused")]
234    fn set_paused(&self, value: bool) -> zbus::Result<()>;
235    fn notification_show(&self) -> zbus::Result<()>;
236    #[zbus(property, name = "historyLength")]
237    fn history_length(&self) -> zbus::Result<u32>;
238    #[zbus(property, name = "displayedLength")]
239    fn displayed_length(&self) -> zbus::Result<u32>;
240    #[zbus(property, name = "waitingLength")]
241    fn waiting_length(&self) -> zbus::Result<u32>;
242}
243struct SwayNCDriver {
244    proxy: SwayNCDbusProxy<'static>,
245    changes: SubscribeStream,
246    changes_v2: SubscribeV2Stream,
247}
248
249impl SwayNCDriver {
250    async fn new() -> Result<Self> {
251        let dbus_conn = new_dbus_connection().await?;
252        let proxy = SwayNCDbusProxy::new(&dbus_conn)
253            .await
254            .error("Failed to create SwayNCDbusProxy")?;
255        Ok(Self {
256            changes: proxy
257                .receive_subscribe()
258                .await
259                .error("Failed to create SubscribeStream")?,
260            changes_v2: proxy
261                .receive_subscribe_v2()
262                .await
263                .error("Failed to create SubscribeV2Stream")?,
264            proxy,
265        })
266    }
267}
268
269#[async_trait]
270impl Driver for SwayNCDriver {
271    async fn is_paused(&self) -> Result<bool> {
272        let (is_dnd, is_inhibited) = join!(self.proxy.get_dnd(), self.proxy.is_inhibited());
273
274        is_dnd
275            .error("Failed to call 'GetDnd'")
276            .map(|is_dnd| is_dnd || is_inhibited.unwrap_or_default())
277    }
278
279    async fn set_paused(&self, paused: bool) -> Result<()> {
280        if paused {
281            self.proxy.set_dnd(paused).await
282        } else {
283            join!(self.proxy.set_dnd(paused), self.proxy.clear_inhibitors()).0
284        }
285        .error("Failed to call 'SetDnd'")
286    }
287
288    async fn notification_show(&self) -> Result<()> {
289        self.proxy
290            .toggle_visibility()
291            .await
292            .error("Failed to call 'ToggleVisibility'")
293    }
294
295    async fn notification_show_all(&self) -> Result<()> {
296        self.notification_show().await
297    }
298
299    async fn history_count(&self) -> Result<u32> {
300        self.notification_count().await
301    }
302
303    async fn notification_count(&self) -> Result<u32> {
304        self.proxy
305            .notification_count()
306            .await
307            .error("Failed to call 'NotificationCount'")
308    }
309
310    async fn wait_for_change(&mut self) -> Result<()> {
311        select! {
312            _ = self.changes.next() => (),
313            _ = self.changes_v2.next() => (),
314        }
315        Ok(())
316    }
317}
318
319#[zbus::proxy(
320    interface = "org.erikreider.swaync.cc",
321    default_service = "org.freedesktop.Notifications",
322    default_path = "/org/erikreider/swaync/cc"
323)]
324trait SwayNCDbus {
325    fn get_dnd(&self) -> zbus::Result<bool>;
326    fn set_dnd(&self, value: bool) -> zbus::Result<()>;
327    fn toggle_visibility(&self) -> zbus::Result<()>;
328    fn notification_count(&self) -> zbus::Result<u32>;
329    #[zbus(signal)]
330    fn subscribe(&self, count: u32, dnd: bool, cc_open: bool) -> zbus::Result<()>;
331
332    // inhibitors were introduced in v0.8.0
333    fn is_inhibited(&self) -> zbus::Result<bool>;
334    fn clear_inhibitors(&self) -> zbus::Result<bool>;
335    // subscribe_v2 replaced subscribe in v0.8.0
336    #[zbus(signal)]
337    fn subscribe_v2(
338        &self,
339        count: u32,
340        dnd: bool,
341        cc_open: bool,
342        inhibited: bool,
343    ) -> zbus::Result<()>;
344}