i3status_rs/blocks/
toggle.rs1use 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 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 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}