Skip to main content

i3status_rs/blocks/
toggle.rs

1//! A Toggle block
2//!
3//! You can add commands to be executed to disable the toggle (`command_off`), and to enable it
4//! (`command_on`). If these command exit with a non-zero status, the block will not be toggled and
5//! the block state will be changed to `critical` to give a visual warning of the failure. You also need to
6//! specify a command to determine the state of the toggle (`command_state`). When the command outputs
7//! nothing, the toggle is disabled, otherwise enabled. By specifying the interval property you can
8//! let the command_state be executed continuously.
9//!
10//! To run those commands, the shell form `$SHELL` environment variable is used. If such variable
11//! is not presented, `sh` is used.
12//!
13//! # Configuration
14//!
15//! Key | Values | Default
16//! ----|--------|--------
17//! `format` | A string to customise the output of this block. See below for available placeholders | `" $icon "`
18//! `command_on` | Shell command to enable the toggle | **Required**
19//! `command_off` | Shell command to disable the toggle | **Required**
20//! `command_state` | Shell command to determine the state. Empty output => No, otherwise => Yes. | **Required**
21//! `icon_on` | Icon override for the toggle button while on | `"toggle_on"`
22//! `icon_off` | Icon override for the toggle button while off | `"toggle_off"`
23//! `interval` | Update interval in seconds. If not set, `command_state` will run only on click. | None
24//! `state_on` | [`State`] (color) of this block while on | [idle][State::Idle]
25//! `state_off` | [`State`] (color) of this block while off | [idle][State::Idle]
26//!
27//! Placeholder   | Value                                       | Type   | Unit
28//! --------------|---------------------------------------------|--------|-----
29//! `icon`        | Icon based on toggle's state                | Icon   | -
30//!
31//! Action   | Default button
32//! ---------|---------------
33//! `toggle` | Left
34//!
35//! # Examples
36//!
37//! This is what can be used to toggle an external monitor configuration:
38//!
39//! ```toml
40//! [[block]]
41//! block = "toggle"
42//! format = " $icon 4k "
43//! command_state = "xrandr | grep 'DP1 connected 38' | grep -v eDP1"
44//! command_on = "~/.screenlayout/4kmon_default.sh"
45//! command_off = "~/.screenlayout/builtin.sh"
46//! interval = 5
47//! state_on = "good"
48//! state_off = "warning"
49//! ```
50//!
51//! # Icons Used
52//! - `toggle_off` (`$icon`)
53//! - `toggle_on` (`$icon`)
54
55use super::prelude::*;
56use std::env;
57use tokio::process::Command;
58
59#[derive(Deserialize, Debug)]
60#[serde(deny_unknown_fields)]
61pub struct Config {
62    pub format: FormatConfig,
63    pub command_on: String,
64    pub command_off: String,
65    pub command_state: String,
66    #[serde(default)]
67    pub icon_on: Option<String>,
68    #[serde(default)]
69    pub icon_off: Option<String>,
70    #[serde(default)]
71    pub interval: Option<u64>,
72    pub state_on: Option<State>,
73    pub state_off: Option<State>,
74}
75
76async fn sleep_opt(dur: Option<Duration>) {
77    match dur {
78        Some(dur) => tokio::time::sleep(dur).await,
79        None => std::future::pending().await,
80    }
81}
82
83pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
84    let format = config.format.with_default(" $icon ")?;
85    let icon_on = config
86        .icon_on
87        .clone()
88        .unwrap_or_else(|| icons::TOGGLE_ON.into());
89    let icon_off = config
90        .icon_off
91        .clone()
92        .unwrap_or_else(|| icons::TOGGLE_OFF.into());
93    BlockPlan::new(vec![
94        OutputPlan::new("on", format.clone()).icon("icon", IconChoices::one(icon_on)),
95        OutputPlan::new("off", format).icon("icon", IconChoices::one(icon_off)),
96    ])
97}
98
99pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
100    let mut actions = api.get_actions()?;
101    api.set_default_actions(&[(MouseButton::Left, None, "toggle")])?;
102
103    let interval = config.interval.map(Duration::from_secs);
104
105    let output_on = plan.output("on")?;
106    let output_off = plan.output("off")?;
107
108    let shell = env::var("SHELL").unwrap_or_else(|_| "sh".to_string());
109    let mut state = State::Idle;
110
111    loop {
112        // Check state
113        let output = Command::new(&shell)
114            .args(["-c", &config.command_state])
115            .output()
116            .await
117            .error("Failed to run command_state")?;
118        let is_on = !std::str::from_utf8(&output.stdout)
119            .error("The output of command_state is invalid UTF-8")?
120            .trim()
121            .is_empty();
122
123        let output = if is_on { &output_on } else { &output_off };
124        let mut widget = output.new_widget();
125        widget.set_values(map!(
126            "icon" => output.icon_value("icon")?
127        ));
128        if state != State::Critical {
129            state = if is_on {
130                config.state_on.unwrap_or(State::Idle)
131            } else {
132                config.state_off.unwrap_or(State::Idle)
133            };
134        }
135        widget.state = state;
136        api.set_widget(widget)?;
137
138        loop {
139            select! {
140                _ = sleep_opt(interval) => break,
141                _ = api.wait_for_update_request() => break,
142                Some(action) = actions.recv() => match action.as_ref() {
143                    "toggle" => {
144                        let cmd = if is_on {
145                            &config.command_off
146                        } else {
147                            &config.command_on
148                        };
149                        let output = Command::new(&shell)
150                            .args(["-c", cmd])
151                            .output()
152                            .await
153                            .error("Failed to run command")?;
154                        if output.status.success() {
155                            // Temporary; it will immediately be updated by the outer loop
156                            state = State::Idle;
157                            break;
158                        } else {
159                            state = State::Critical;
160                        }
161                    }
162                    _ => (),
163                }
164            }
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    fn config(icon_on: Option<&str>, icon_off: Option<&str>) -> Config {
174        Config {
175            format: Default::default(),
176            command_on: String::new(),
177            command_off: String::new(),
178            command_state: String::new(),
179            icon_on: icon_on.map(Into::into),
180            icon_off: icon_off.map(Into::into),
181            interval: None,
182            state_on: None,
183            state_off: None,
184        }
185    }
186
187    #[test]
188    fn plan_uses_default_icon_names() {
189        let plan = prepare(&config(None, None)).unwrap();
190        assert_eq!(
191            plan.output("on").unwrap().single_icon("icon").unwrap(),
192            "toggle_on"
193        );
194        assert_eq!(
195            plan.output("off").unwrap().single_icon("icon").unwrap(),
196            "toggle_off"
197        );
198    }
199
200    #[test]
201    fn plan_uses_configuration_derived_icon_names() {
202        let plan = prepare(&config(Some("my_enabled_icon"), Some("my_disabled_icon"))).unwrap();
203        assert_eq!(
204            plan.output("on").unwrap().single_icon("icon").unwrap(),
205            "my_enabled_icon"
206        );
207        assert_eq!(
208            plan.output("off").unwrap().single_icon("icon").unwrap(),
209            "my_disabled_icon"
210        );
211    }
212}