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)
}