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}