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//! `paused`                                  | Present only if notifications are disabled | Flag   | -
17//!
18//! Action          | Default button
19//! ----------------|---------------
20//! `toggle_paused` | Left
21//! `show`          | -
22//!
23//! # Examples
24//!
25//! How to use `paused` flag
26//!
27//! ```toml
28//! [[block]]
29//! block = "notify"
30//! format = " $icon {$paused{Off}|On} "
31//! ```
32//! How to use `notification_count`
33//!
34//! ```toml
35//! [[block]]
36//! block = "notify"
37//! format = " $icon {($notification_count.eng(w:1)) |}"
38//! ```
39//! How to remap actions
40//!
41//! ```toml
42//! [[block]]
43//! block = "notify"
44//! driver = "swaync"
45//! [[block.click]]
46//! button = "left"
47//! action = "show"
48//! [[block.click]]
49//! button = "right"
50//! action = "toggle_paused"
51//! ```
52//!
53//! # Icons Used
54//! - `bell`
55//! - `bell-slash`
56//!
57//! [^dunst_version_note]: when using `notification_count` with the `dunst` driver use dunst > 1.9.0
58
59use super::prelude::*;
60use tokio::{join, try_join};
61use zbus::proxy::PropertyStream;
62
63const ICON_ON: &str = "bell";
64const ICON_OFF: &str = "bell-slash";
65
66#[derive(Deserialize, Debug, Default)]
67#[serde(deny_unknown_fields, default)]
68pub struct Config {
69    pub driver: DriverType,
70    pub format: FormatConfig,
71}
72
73#[derive(Deserialize, Debug, SmartDefault)]
74#[serde(rename_all = "lowercase")]
75pub enum DriverType {
76    #[default]
77    Dunst,
78    SwayNC,
79}
80
81pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
82    let mut actions = api.get_actions()?;
83    api.set_default_actions(&[(MouseButton::Left, None, "toggle_paused")])?;
84
85    let format = config.format.with_default(" $icon ")?;
86
87    let mut driver: Box<dyn Driver> = match config.driver {
88        DriverType::Dunst => Box::new(DunstDriver::new().await?),
89        DriverType::SwayNC => Box::new(SwayNCDriver::new().await?),
90    };
91
92    loop {
93        let (is_paused, notification_count) =
94            try_join!(driver.is_paused(), driver.notification_count())?;
95
96        let mut widget = Widget::new().with_format(format.clone());
97        widget.set_values(map!(
98            "icon" => Value::icon(if is_paused { ICON_OFF } else { ICON_ON }),
99            [if notification_count != 0] "notification_count" => Value::number(notification_count),
100            [if is_paused] "paused" => Value::flag(),
101        ));
102        widget.state = if notification_count == 0 {
103            State::Idle
104        } else {
105            State::Info
106        };
107        api.set_widget(widget)?;
108
109        select! {
110            x = driver.wait_for_change() => x?,
111            Some(action) = actions.recv() => match action.as_ref() {
112                "toggle_paused" => {
113                    driver.set_paused(!is_paused).await?;
114                }
115                "show" => {
116                    driver.notification_show().await?;
117                }
118                _ => (),
119            }
120        }
121    }
122}
123
124#[async_trait]
125trait Driver {
126    async fn is_paused(&self) -> Result<bool>;
127    async fn set_paused(&self, paused: bool) -> Result<()>;
128    async fn notification_show(&self) -> Result<()>;
129    async fn notification_count(&self) -> Result<u32>;
130    async fn wait_for_change(&mut self) -> Result<()>;
131}
132
133struct DunstDriver {
134    proxy: DunstDbusProxy<'static>,
135    paused_changes: PropertyStream<'static, bool>,
136    displayed_length_changes: PropertyStream<'static, u32>,
137    waiting_length_changes: PropertyStream<'static, u32>,
138}
139
140impl DunstDriver {
141    async fn new() -> Result<Self> {
142        let dbus_conn = new_dbus_connection().await?;
143        let proxy = DunstDbusProxy::new(&dbus_conn)
144            .await
145            .error("Failed to create DunstDbusProxy")?;
146        Ok(Self {
147            paused_changes: proxy.receive_paused_changed().await,
148            displayed_length_changes: proxy.receive_displayed_length_changed().await,
149            waiting_length_changes: proxy.receive_waiting_length_changed().await,
150            proxy,
151        })
152    }
153}
154
155#[async_trait]
156impl Driver for DunstDriver {
157    async fn is_paused(&self) -> Result<bool> {
158        self.proxy.paused().await.error("Failed to get 'paused'")
159    }
160
161    async fn set_paused(&self, paused: bool) -> Result<()> {
162        self.proxy
163            .set_paused(paused)
164            .await
165            .error("Failed to set 'paused'")
166    }
167
168    async fn notification_show(&self) -> Result<()> {
169        self.proxy
170            .notification_show()
171            .await
172            .error("Could not call 'NotificationShow'")
173    }
174
175    async fn notification_count(&self) -> Result<u32> {
176        let (displayed_length, waiting_length) =
177            try_join!(self.proxy.displayed_length(), self.proxy.waiting_length())
178                .error("Failed to get property")?;
179
180        Ok(displayed_length + waiting_length)
181    }
182
183    async fn wait_for_change(&mut self) -> Result<()> {
184        select! {
185            _ = self.paused_changes.next() => {}
186            _ = self.displayed_length_changes.next() => {}
187            _ = self.waiting_length_changes.next() => {}
188        }
189        Ok(())
190    }
191}
192
193#[zbus::proxy(
194    interface = "org.dunstproject.cmd0",
195    default_service = "org.freedesktop.Notifications",
196    default_path = "/org/freedesktop/Notifications"
197)]
198
199trait DunstDbus {
200    #[zbus(property, name = "paused")]
201    fn paused(&self) -> zbus::Result<bool>;
202    #[zbus(property, name = "paused")]
203    fn set_paused(&self, value: bool) -> zbus::Result<()>;
204    fn notification_show(&self) -> zbus::Result<()>;
205    #[zbus(property, name = "displayedLength")]
206    fn displayed_length(&self) -> zbus::Result<u32>;
207    #[zbus(property, name = "waitingLength")]
208    fn waiting_length(&self) -> zbus::Result<u32>;
209}
210struct SwayNCDriver {
211    proxy: SwayNCDbusProxy<'static>,
212    changes: SubscribeStream,
213    changes_v2: SubscribeV2Stream,
214}
215
216impl SwayNCDriver {
217    async fn new() -> Result<Self> {
218        let dbus_conn = new_dbus_connection().await?;
219        let proxy = SwayNCDbusProxy::new(&dbus_conn)
220            .await
221            .error("Failed to create SwayNCDbusProxy")?;
222        Ok(Self {
223            changes: proxy
224                .receive_subscribe()
225                .await
226                .error("Failed to create SubscribeStream")?,
227            changes_v2: proxy
228                .receive_subscribe_v2()
229                .await
230                .error("Failed to create SubscribeV2Stream")?,
231            proxy,
232        })
233    }
234}
235
236#[async_trait]
237impl Driver for SwayNCDriver {
238    async fn is_paused(&self) -> Result<bool> {
239        let (is_dnd, is_inhibited) = join!(self.proxy.get_dnd(), self.proxy.is_inhibited());
240
241        is_dnd
242            .error("Failed to call 'GetDnd'")
243            .map(|is_dnd| is_dnd || is_inhibited.unwrap_or_default())
244    }
245
246    async fn set_paused(&self, paused: bool) -> Result<()> {
247        if paused {
248            self.proxy.set_dnd(paused).await
249        } else {
250            join!(self.proxy.set_dnd(paused), self.proxy.clear_inhibitors()).0
251        }
252        .error("Failed to call 'SetDnd'")
253    }
254
255    async fn notification_show(&self) -> Result<()> {
256        self.proxy
257            .toggle_visibility()
258            .await
259            .error("Failed to call 'ToggleVisibility'")
260    }
261
262    async fn notification_count(&self) -> Result<u32> {
263        self.proxy
264            .notification_count()
265            .await
266            .error("Failed to call 'NotificationCount'")
267    }
268
269    async fn wait_for_change(&mut self) -> Result<()> {
270        select! {
271            _ = self.changes.next() => (),
272            _ = self.changes_v2.next() => (),
273        }
274        Ok(())
275    }
276}
277
278#[zbus::proxy(
279    interface = "org.erikreider.swaync.cc",
280    default_service = "org.freedesktop.Notifications",
281    default_path = "/org/erikreider/swaync/cc"
282)]
283trait SwayNCDbus {
284    fn get_dnd(&self) -> zbus::Result<bool>;
285    fn set_dnd(&self, value: bool) -> zbus::Result<()>;
286    fn toggle_visibility(&self) -> zbus::Result<()>;
287    fn notification_count(&self) -> zbus::Result<u32>;
288    #[zbus(signal)]
289    fn subscribe(&self, count: u32, dnd: bool, cc_open: bool) -> zbus::Result<()>;
290
291    // inhibitors were introduced in v0.8.0
292    fn is_inhibited(&self) -> zbus::Result<bool>;
293    fn clear_inhibitors(&self) -> zbus::Result<bool>;
294    // subscribe_v2 replaced subscribe in v0.8.0
295    #[zbus(signal)]
296    fn subscribe_v2(
297        &self,
298        count: u32,
299        dnd: bool,
300        cc_open: bool,
301        inhibited: bool,
302    ) -> zbus::Result<()>;
303}