i3status_rs/blocks/
service_status.rs

1//! Display the status of a service
2//!
3//! Right now only `systemd` is supported.
4//!
5//! # Configuration
6//!
7//! Key | Values | Default
8//! ----|--------|--------
9//! `driver` | Which init system is running the service. Available drivers are: `"systemd"` | `"systemd"`
10//! `service` | The name of the service | **Required**
11//! `active_format` | A string to customise the output of this block. See below for available placeholders. | `" $service active "`
12//! `inactive_format` | A string to customise the output of this block. See below for available placeholders. | `" $service inactive "`
13//! `active_state` | A valid [`State`] | [`State::Idle`]
14//! `inactive_state` | A valid [`State`]  | [`State::Critical`]
15//!
16//! Placeholder    | Value                     | Type   | Unit
17//! ---------------|---------------------------|--------|-----
18//! `service`      | The name of the service   | Text   | -
19//!
20//! # Example
21//!
22//! Example using an icon:
23//!
24//! ```toml
25//! [[block]]
26//! block = "service_status"
27//! service = "cups"
28//! active_format = " ^icon_tea "
29//! inactive_format = " no ^icon_tea "
30//! ```
31//!
32//! Example overriding the default `inactive_state`:
33//!
34//! ```toml
35//! [[block]]
36//! block = "service_status"
37//! service = "shadow"
38//! active_format = ""
39//! inactive_format = " Integrity of password and group files failed "
40//! inactive_state = "Warning"
41//! ```
42//!
43
44use super::prelude::*;
45use zbus::proxy::PropertyStream;
46
47#[derive(Deserialize, Debug, Default)]
48#[serde(deny_unknown_fields, default)]
49pub struct Config {
50    pub driver: DriverType,
51    pub service: String,
52    pub active_format: FormatConfig,
53    pub inactive_format: FormatConfig,
54    pub active_state: Option<State>,
55    pub inactive_state: Option<State>,
56}
57
58#[derive(Deserialize, Debug, SmartDefault)]
59#[serde(rename_all = "snake_case")]
60pub enum DriverType {
61    #[default]
62    Systemd,
63}
64
65pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
66    let active_format = config.active_format.with_default(" $service active ")?;
67    let inactive_format = config.inactive_format.with_default(" $service inactive ")?;
68
69    let active_state = config.active_state.unwrap_or(State::Idle);
70    let inactive_state = config.inactive_state.unwrap_or(State::Critical);
71
72    let mut driver: Box<dyn Driver> = match config.driver {
73        DriverType::Systemd => Box::new(SystemdDriver::new(config.service.clone()).await?),
74    };
75
76    loop {
77        let service_active_state = driver.is_active().await?;
78
79        let mut widget = Widget::new();
80
81        if service_active_state {
82            widget.state = active_state;
83            widget.set_format(active_format.clone());
84        } else {
85            widget.state = inactive_state;
86            widget.set_format(inactive_format.clone());
87        };
88
89        widget.set_values(map! {
90            "service" =>Value::text(config.service.clone()),
91        });
92
93        api.set_widget(widget)?;
94
95        driver.wait_for_change().await?;
96    }
97}
98
99#[async_trait]
100trait Driver {
101    async fn is_active(&self) -> Result<bool>;
102    async fn wait_for_change(&mut self) -> Result<()>;
103}
104
105struct SystemdDriver {
106    proxy: UnitProxy<'static>,
107    active_state_changed: PropertyStream<'static, String>,
108}
109
110impl SystemdDriver {
111    async fn new(service: String) -> Result<Self> {
112        let dbus_conn = new_system_dbus_connection().await?;
113
114        if !service.is_ascii() {
115            return Err(Error::new(format!(
116                "service name \"{service}\" must only contain ASCII characters"
117            )));
118        }
119        let encoded_service = format!("{service}.service")
120            // For each byte...
121            .bytes()
122            .map(|b| {
123                if b.is_ascii_alphanumeric() {
124                    // Just use the character as a string
125                    char::from(b).to_string()
126                } else {
127                    // Otherwise use the hex representation of the byte preceded by an underscore
128                    format!("_{b:02x}")
129                }
130            })
131            .collect::<String>();
132
133        let path = format!("/org/freedesktop/systemd1/unit/{encoded_service}");
134
135        let proxy = UnitProxy::builder(&dbus_conn)
136            .path(path)
137            .error("Could not set path")?
138            .build()
139            .await
140            .error("Failed to create UnitProxy")?;
141
142        Ok(Self {
143            active_state_changed: proxy.receive_active_state_changed().await,
144            proxy,
145        })
146    }
147}
148
149#[async_trait]
150impl Driver for SystemdDriver {
151    async fn is_active(&self) -> Result<bool> {
152        self.proxy
153            .active_state()
154            .await
155            .error("Could not get active_state")
156            .map(|state| state == "active")
157    }
158
159    async fn wait_for_change(&mut self) -> Result<()> {
160        self.active_state_changed.next().await;
161        Ok(())
162    }
163}
164
165#[zbus::proxy(
166    interface = "org.freedesktop.systemd1.Unit",
167    default_service = "org.freedesktop.systemd1"
168)]
169trait Unit {
170    #[zbus(property)]
171    fn active_state(&self) -> zbus::Result<String>;
172}