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