i3status_rs/blocks/
custom_dbus.rs

1//! A block controlled by the DBus
2//!
3//! This block creates a new DBus object in `rs.i3status` service. This object implements
4//! `rs.i3status.custom` interface which allows you to set block's icon, text and state.
5//!
6//! Output of `busctl --user introspect rs.i3status /<path> rs.i3status.custom`:
7//! ```text
8//! NAME                                TYPE      SIGNATURE RESULT/VALUE FLAGS
9//! rs.i3status.custom                  interface -         -            -
10//! .SetIcon                            method    s         s            -
11//! .SetState                           method    s         s            -
12//! .SetText                            method    ss        s            -
13//! ```
14//!
15//! # Configuration
16//!
17//! Key | Values | Default
18//! ----|--------|--------
19//! `format` | A string to customise the output of this block. | <code>\"{ $icon\|}{ $text.pango-str()\|} \"</code>
20//!
21//! Placeholder  | Value                                  | Type   | Unit
22//! -------------|-------------------------------------------------------------------|--------|---------------
23//! `icon`       | Value of icon set via `SetIcon` if the value is non-empty string. | Icon   | -
24//! `text`       | Value of the first string from SetText                            | Text   | -
25//! `short_text` | Value of the second string from SetText                           | Text   | -
26//!
27//! # Example
28//!
29//! Config:
30//! ```toml
31//! [[block]]
32//! block = "custom_dbus"
33//! path = "/my_path"
34//! ```
35//!
36//! Usage:
37//! ```sh
38//! # set full text to 'hello' and short text to 'hi'
39//! busctl --user call rs.i3status /my_path rs.i3status.custom SetText ss hello hi
40//! # set icon to 'music'
41//! busctl --user call rs.i3status /my_path rs.i3status.custom SetIcon s music
42//! # set state to 'good'
43//! busctl --user call rs.i3status /my_path rs.i3status.custom SetState s good
44//! ```
45//!
46//! Because it's impossible to publish objects to the same name from different
47//! processes, having multiple dbus blocks in different bars won't work. As a workaround,
48//! you can set the env var `I3RS_DBUS_NAME` to set the interface a bar works on to
49//! differentiate between different processes. For example, setting this to 'top', will allow you
50//! to use `rs.i3status.top`.
51//!
52//! # TODO
53//! - Send a signal on click?
54
55use super::prelude::*;
56use std::env;
57use zbus::fdo;
58
59// Share DBus connection between multiple block instances
60static DBUS_CONNECTION: tokio::sync::OnceCell<Result<zbus::Connection>> =
61    tokio::sync::OnceCell::const_new();
62
63const DBUS_NAME: &str = "rs.i3status";
64
65#[derive(Deserialize, Debug)]
66#[serde(deny_unknown_fields)]
67pub struct Config {
68    #[serde(default)]
69    pub format: FormatConfig,
70    pub path: String,
71}
72
73struct Block {
74    widget: Widget,
75    api: CommonApi,
76    icon: Option<String>,
77    text: Option<String>,
78    short_text: Option<String>,
79}
80
81fn block_values(block: &Block) -> HashMap<Cow<'static, str>, Value> {
82    map! {
83        [if let Some(icon) = &block.icon] "icon" => Value::icon(icon.to_string()),
84        [if let Some(text) = &block.text] "text" => Value::text(text.to_string()),
85        [if let Some(short_text) = &block.short_text] "short_text" => Value::text(short_text.to_string()),
86    }
87}
88
89#[zbus::interface(name = "rs.i3status.custom")]
90impl Block {
91    async fn set_icon(&mut self, icon: &str) -> fdo::Result<()> {
92        self.icon = if icon.is_empty() {
93            None
94        } else {
95            Some(icon.to_string())
96        };
97        self.widget.set_values(block_values(self));
98        self.api.set_widget(self.widget.clone())?;
99        Ok(())
100    }
101
102    async fn set_text(&mut self, full: String, short: String) -> fdo::Result<()> {
103        self.text = Some(full);
104        self.short_text = Some(short);
105        self.widget.set_values(block_values(self));
106        self.api.set_widget(self.widget.clone())?;
107        Ok(())
108    }
109
110    async fn set_state(&mut self, state: &str) -> fdo::Result<()> {
111        self.widget.state = match state {
112            "idle" => State::Idle,
113            "info" => State::Info,
114            "good" => State::Good,
115            "warning" => State::Warning,
116            "critical" => State::Critical,
117            _ => return Err(Error::new(format!("'{state}' is not a valid state")).into()),
118        };
119        self.api.set_widget(self.widget.clone())?;
120        Ok(())
121    }
122}
123
124pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
125    let widget = Widget::new().with_format(config.format.with_defaults(
126        "{ $icon|}{ $text.pango-str()|} ",
127        "{ $icon|} $short_text.pango-str() | ",
128    )?);
129
130    let dbus_conn = DBUS_CONNECTION
131        .get_or_init(dbus_conn)
132        .await
133        .as_ref()
134        .map_err(Clone::clone)?;
135    dbus_conn
136        .object_server()
137        .at(
138            config.path.clone(),
139            Block {
140                widget,
141                api: api.clone(),
142                icon: None,
143                text: None,
144                short_text: None,
145            },
146        )
147        .await
148        .error("Failed to setup DBus server")?;
149    Ok(())
150}
151
152async fn dbus_conn() -> Result<zbus::Connection> {
153    let dbus_interface_name = match env::var("I3RS_DBUS_NAME") {
154        Ok(v) => format!("{DBUS_NAME}.{v}"),
155        Err(_) => DBUS_NAME.to_string(),
156    };
157
158    let conn = new_dbus_connection().await?;
159    conn.request_name(dbus_interface_name)
160        .await
161        .error("Failed to request DBus name")?;
162    Ok(conn)
163}