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}