i3status_rs/blocks/
custom_dbus.rs1use super::prelude::*;
56use std::env;
57use zbus::fdo;
58
59static 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(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
125 BlockPlan::new(vec![
128 OutputPlan::new(
129 "main",
130 config.format.with_defaults(
131 "{ $icon|}{ $text.pango-str()|} ",
132 "{ $icon|} $short_text.pango-str() | ",
133 )?,
134 )
135 .icon("icon", IconChoices::OpenResolvable),
136 ])
137}
138
139pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
140 let output = plan.output("main")?;
141 let widget = output.new_widget();
142
143 let dbus_conn = DBUS_CONNECTION
144 .get_or_init(dbus_conn)
145 .await
146 .as_ref()
147 .map_err(Clone::clone)?;
148 dbus_conn
149 .object_server()
150 .at(
151 config.path.clone(),
152 Block {
153 widget,
154 api: api.clone(),
155 icon: None,
156 text: None,
157 short_text: None,
158 },
159 )
160 .await
161 .error("Failed to setup DBus server")?;
162 Ok(())
163}
164
165async fn dbus_conn() -> Result<zbus::Connection> {
166 let dbus_interface_name = match env::var("I3RS_DBUS_NAME") {
167 Ok(v) => format!("{DBUS_NAME}.{v}"),
168 Err(_) => DBUS_NAME.to_string(),
169 };
170
171 let conn = new_dbus_connection().await?;
172 conn.request_name(dbus_interface_name)
173 .await
174 .error("Failed to request DBus name")?;
175 Ok(conn)
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 fn config(toml: &str) -> Config {
183 toml::from_str(toml).unwrap()
184 }
185
186 #[test]
187 fn plan_declares_an_open_icon_because_the_name_arrives_over_dbus() {
188 let plan = prepare(&config(r#"path = "/my_path""#)).unwrap();
189 let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
190 assert_eq!(ids, ["main"]);
191
192 let output = plan.output("main").unwrap();
193 let choices = output.output().choices_for("icon").unwrap();
194 assert!(matches!(choices, IconChoices::OpenResolvable));
195 assert!(choices.permits("any_name_set_over_dbus"));
196 let mut widget = output.new_widget();
199 widget.set_values(map!("icon" => Value::icon("whatever_was_sent")));
200 widget.check_contract().unwrap();
201 }
202
203 #[test]
204 fn custom_format_is_respected() {
205 let plan = prepare(&config(
208 r#"
209 path = "/my_path"
210 format = { full = " $text.pango-str() ", short = " $short_text.pango-str() " }
211 "#,
212 ))
213 .unwrap();
214 let output = plan.output("main").unwrap();
215 assert!(output.format().contains_key("text"));
216 assert!(output.format().contains_key("short_text"));
217 assert!(!output.format().contains_key("icon"));
218 }
219}