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`
53//! - `toggle_on`
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 async fn run(config: &Config, api: &CommonApi) -> Result<()> {
84    let mut actions = api.get_actions()?;
85    api.set_default_actions(&[(MouseButton::Left, None, "toggle")])?;
86
87    let interval = config.interval.map(Duration::from_secs);
88    let mut widget = Widget::new().with_format(config.format.with_default(" $icon ")?);
89
90    let icon_on = config.icon_on.as_deref().unwrap_or("toggle_on");
91    let icon_off = config.icon_off.as_deref().unwrap_or("toggle_off");
92
93    let shell = env::var("SHELL").unwrap_or_else(|_| "sh".to_string());
94
95    loop {
96        // Check state
97        let output = Command::new(&shell)
98            .args(["-c", &config.command_state])
99            .output()
100            .await
101            .error("Failed to run command_state")?;
102        let is_on = !std::str::from_utf8(&output.stdout)
103            .error("The output of command_state is invalid UTF-8")?
104            .trim()
105            .is_empty();
106
107        widget.set_values(map!(
108            "icon" => Value::icon(
109                if is_on { icon_on.to_string() } else { icon_off.to_string() }
110            )
111        ));
112        if widget.state != State::Critical {
113            widget.state = if is_on {
114                config.state_on.unwrap_or(State::Idle)
115            } else {
116                config.state_off.unwrap_or(State::Idle)
117            };
118        }
119        api.set_widget(widget.clone())?;
120
121        loop {
122            select! {
123                _ = sleep_opt(interval) => break,
124                _ = api.wait_for_update_request() => break,
125                Some(action) = actions.recv() => match action.as_ref() {
126                    "toggle" => {
127                        let cmd = if is_on {
128                            &config.command_off
129                        } else {
130                            &config.command_on
131                        };
132                        let output = Command::new(&shell)
133                            .args(["-c", cmd])
134                            .output()
135                            .await
136                            .error("Failed to run command")?;
137                        if output.status.success() {
138                            // Temporary; it will immediately be updated by the outer loop
139                            widget.state = State::Idle;
140                            break;
141                        } else {
142                            widget.state = State::Critical;
143                        }
144                    }
145                    _ => (),
146                }
147            }
148        }
149    }
150}