Skip to main content

i3status_rs/blocks/
rofication.rs

1//! The number of pending notifications in rofication-daemon
2//!
3//! A different color is used if there are critical notifications.
4//!
5//! # Configuration
6//!
7//! Key | Values | Default
8//! ----|--------|--------
9//! `interval` | Refresh rate in seconds. | `1`
10//! `format` | A string to customise the output of this block. See below for placeholders. | `" $icon $num.eng(w:1) "`
11//! `socket_path` | Socket path for the rofication daemon. Supports path expansions e.g. `~`. | `"/tmp/rofi_notification_daemon"`
12//!
13//!  Placeholder | Value | Type | Unit
14//! -------------|-------|------|-----
15//! `icon`       | A static icon  | Icon | -
16//! `num`        | Number of pending notifications | Number | -
17//!
18//! # Example
19//!
20//! ```toml
21//! [[block]]
22//! block = "rofication"
23//! interval = 1
24//! socket_path = "/tmp/rofi_notification_daemon"
25//! [[block.click]]
26//! button = "left"
27//! cmd = "rofication-gui"
28//! ```
29//!
30//! # Icons Used
31//! - `bell` (`$icon`)
32
33use super::prelude::*;
34use tokio::net::UnixStream;
35
36#[derive(Deserialize, Debug, SmartDefault)]
37#[serde(deny_unknown_fields, default)]
38pub struct Config {
39    #[default(1.into())]
40    pub interval: Seconds,
41    #[default("/tmp/rofi_notification_daemon".into())]
42    pub socket_path: ShellString,
43    pub format: FormatConfig,
44}
45
46pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
47    BlockPlan::new(vec![
48        OutputPlan::new("main", config.format.with_default(" $icon $num.eng(w:1) ")?)
49            .icon("icon", IconChoices::one(icons::BELL)),
50    ])
51}
52
53pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
54    let output_main = plan.output("main")?;
55
56    let path = config.socket_path.expand()?;
57    let mut timer = config.interval.timer();
58
59    loop {
60        let (num, crit) = rofication_status(&path).await?;
61
62        let mut widget = output_main.new_widget();
63
64        widget.set_values(map!(
65            "icon" => Value::icon(icons::BELL),
66            "num" => Value::number(num)
67        ));
68
69        widget.state = if crit > 0 {
70            State::Warning
71        } else if num > 0 {
72            State::Info
73        } else {
74            State::Idle
75        };
76
77        api.set_widget(widget)?;
78
79        tokio::select! {
80            _ = timer.tick() => (),
81            _ = api.wait_for_update_request() => (),
82        }
83    }
84}
85
86async fn rofication_status(socket_path: &str) -> Result<(usize, usize)> {
87    let mut stream = UnixStream::connect(socket_path)
88        .await
89        .error("Failed to connect to socket")?;
90
91    // Request count
92    stream
93        .write_all(b"num:\n")
94        .await
95        .error("Failed to write to socket")?;
96
97    let mut response = String::new();
98    stream
99        .read_to_string(&mut response)
100        .await
101        .error("Failed to read from socket")?;
102
103    // Response must be two integers: regular and critical, separated either by a comma or a \n
104    let (num, crit) = response
105        .split_once([',', '\n'])
106        .error("Incorrect response")?;
107    Ok((
108        num.parse().error("Incorrect response")?,
109        crit.parse().error("Incorrect response")?,
110    ))
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn plan_declares_main_output_with_bell_icon() {
119        let plan = prepare(&Config::default()).unwrap();
120        let declared: Vec<_> = plan.outputs().map(|o| o.id()).collect();
121        assert_eq!(declared, ["main"]);
122        let output = plan.output("main").unwrap();
123        assert_eq!(output.single_icon("icon").unwrap(), "bell");
124    }
125
126    #[test]
127    fn custom_format_is_respected() {
128        let config = Config {
129            format: " $num ".parse().unwrap(),
130            ..Config::default()
131        };
132        let plan = prepare(&config).unwrap();
133        let output = plan.output("main").unwrap();
134        assert!(output.format().contains_key("num"));
135        assert!(!output.format().contains_key("icon"));
136    }
137}