1use std::{future::Future, io::stdout, iter::zip};
2
3use itertools::Itertools;
4use switchbot_api::{CommandRequest, Device, DeviceList, SwitchBot};
5
6use crate::{Args, UserInput};
7
8#[derive(Debug, Default)]
9pub struct Cli {
10 args: Args,
11 switch_bot: SwitchBot,
12 current_device_indexes: Vec<usize>,
13}
14
15impl Cli {
16 pub fn new_from_args() -> Self {
17 Self {
18 args: Args::new_from_args(),
19 ..Default::default()
20 }
21 }
22
23 #[cfg(test)]
24 fn new_for_test(n_devices: usize) -> Self {
25 Self {
26 switch_bot: SwitchBot::new_for_test(n_devices),
27 ..Default::default()
28 }
29 }
30
31 fn devices(&self) -> &DeviceList {
32 self.switch_bot.devices()
33 }
34
35 fn has_current_device(&self) -> bool {
36 !self.current_device_indexes.is_empty()
37 }
38
39 fn num_current_devices(&self) -> usize {
40 self.current_device_indexes.len()
41 }
42
43 fn current_devices_as<'a, T, F>(&'a self, f: F) -> impl Iterator<Item = T> + 'a
44 where
45 F: Fn(usize) -> T + 'a,
46 {
47 self.current_device_indexes
48 .iter()
49 .map(move |&index| f(index))
50 }
51
52 fn current_devices(&self) -> impl Iterator<Item = &Device> {
53 self.current_devices_as(|index| &self.devices()[index])
54 }
55
56 fn current_devices_with_index(&self) -> impl Iterator<Item = (usize, &Device)> {
57 self.current_devices_as(|index| (index, &self.devices()[index]))
58 }
59
60 fn first_current_device(&self) -> &Device {
61 &self.devices()[self.current_device_indexes[0]]
62 }
63
64 async fn ensure_devices(&mut self) -> anyhow::Result<()> {
65 if self.devices().is_empty() {
66 self.switch_bot = self.args.create_switch_bot()?;
67 self.switch_bot.load_devices().await?;
68 log::debug!("ensure_devices: {} devices", self.devices().len());
69 }
70 Ok(())
71 }
72
73 pub async fn run(&mut self) -> anyhow::Result<()> {
74 self.run_core().await?;
75 self.args.save()?;
76 Ok(())
77 }
78
79 async fn run_core(&mut self) -> anyhow::Result<()> {
80 let mut is_interactive = true;
81 if !self.args.alias_updates.is_empty() {
82 self.args.print_aliases();
83 is_interactive = false;
84 }
85
86 if !self.args.commands.is_empty() {
87 self.ensure_devices().await?;
88 self.execute_args(&self.args.commands.clone()).await?;
89 } else if is_interactive {
90 self.ensure_devices().await?;
91 self.run_interactive().await?;
92 }
93 Ok(())
94 }
95
96 async fn run_interactive(&mut self) -> anyhow::Result<()> {
97 let mut input = UserInput::new();
98 self.print_devices();
99 loop {
100 input.set_prompt(if self.has_current_device() {
101 "Command> "
102 } else {
103 "Device> "
104 });
105
106 let input_text = input.read_line()?;
107 match input_text {
108 "q" => break,
109 "" => {
110 if self.has_current_device() {
111 self.current_device_indexes.clear();
112 self.print_devices();
113 continue;
114 }
115 break;
116 }
117 _ => match self.execute(input_text).await {
118 Ok(true) => self.print_devices(),
119 Ok(false) => {}
120 Err(error) => log::error!("{error}"),
121 },
122 }
123 }
124 Ok(())
125 }
126
127 fn print_devices(&self) {
128 if !self.has_current_device() {
129 self.print_all_devices();
130 return;
131 }
132
133 if self.current_device_indexes.len() >= 2 {
134 for (i, device) in self.current_devices_with_index() {
135 println!("{}: {device}", i + 1);
136 }
137 return;
138 }
139
140 let device = self.first_current_device();
141 print!("{device:#}");
142 }
143
144 fn print_all_devices(&self) {
145 for (i, device) in self.devices().iter().enumerate() {
146 println!("{}: {device}", i + 1);
147 }
148 }
149
150 async fn execute_args(&mut self, list: &[String]) -> anyhow::Result<()> {
151 for command in list {
152 self.execute(command).await?;
153 }
154 Ok(())
155 }
156
157 async fn execute(&mut self, text: &str) -> anyhow::Result<bool> {
158 if let Some(alias) = self.args.aliases.get(text) {
159 log::debug!(r#"alias: "{text}" -> "{alias}""#);
160 return self.execute_no_alias(&alias.clone()).await;
161 }
162 self.execute_no_alias(text).await
163 }
164
165 async fn execute_no_alias(&mut self, text: &str) -> anyhow::Result<bool> {
167 let set_device_result = self.set_current_devices(text);
168 if set_device_result.is_ok() {
169 return Ok(true);
170 }
171 if self.execute_global_builtin_command(text)? {
172 return Ok(false);
173 }
174 if self.has_current_device() {
175 if self.execute_if_expr(text).await? {
176 return Ok(false);
177 }
178 self.execute_command(text).await?;
179 return Ok(false);
180 }
181 Err(set_device_result.unwrap_err())
182 }
183
184 fn set_current_devices(&mut self, text: &str) -> anyhow::Result<()> {
185 self.current_device_indexes = self.parse_device_indexes(text)?;
186 log::debug!("current_device_indexes={:?}", self.current_device_indexes);
187 Ok(())
188 }
189
190 fn parse_device_indexes(&self, value: &str) -> anyhow::Result<Vec<usize>> {
191 let values = value.split(',');
192 let mut indexes: Vec<usize> = Vec::new();
193 for s in values {
194 if let Some(alias) = self.args.aliases.get(s) {
195 indexes.extend(self.parse_device_indexes(alias)?);
196 continue;
197 }
198 indexes.push(self.parse_device_index(s)?);
199 }
200 indexes = indexes.into_iter().unique().collect::<Vec<_>>();
201 Ok(indexes)
202 }
203
204 fn parse_device_index(&self, value: &str) -> anyhow::Result<usize> {
205 if let Ok(number) = value.parse::<usize>() {
206 if number > 0 && number <= self.devices().len() {
207 return Ok(number - 1);
208 }
209 }
210 self.devices()
211 .index_by_device_id(value)
212 .ok_or_else(|| anyhow::anyhow!("Not a valid device: \"{value}\""))
213 }
214
215 async fn execute_if_expr(&mut self, expr: &str) -> anyhow::Result<bool> {
216 assert!(self.has_current_device());
217 if let Some((condition, then_command, else_command)) = Self::parse_if_expr(expr) {
218 let (device, expr) = self.device_expr(condition);
219 device.update_status().await?;
220 let eval_result = device.eval_condition(expr)?;
221 let command = if eval_result {
222 then_command
223 } else {
224 else_command
225 };
226 log::debug!("if: {condition} is {eval_result}, execute {command}");
227 Box::pin(self.execute(command)).await?;
228 return Ok(true);
229 }
230 Ok(false)
231 }
232
233 fn parse_if_expr(text: &str) -> Option<(&str, &str, &str)> {
234 if let Some(text) = text.strip_prefix("if") {
235 if let Some(sep) = text.chars().nth(0) {
236 if sep.is_alphanumeric() {
237 return None;
238 }
239 let fields: Vec<&str> = text[1..].split_terminator(sep).collect();
240 match fields.len() {
241 2 => return Some((fields[0], fields[1], "")),
242 3 => return Some((fields[0], fields[1], fields[2])),
243 _ => {}
244 }
245 }
246 }
247 None
248 }
249
250 fn device_expr<'a>(&'a self, expr: &'a str) -> (&'a Device, &'a str) {
251 if let Some((device, expr)) = expr.split_once('.') {
252 if let Ok(device_indexes) = self.parse_device_indexes(device) {
253 return (&self.devices()[device_indexes[0]], expr);
254 }
255 }
256 (self.first_current_device(), expr)
257 }
258
259 fn execute_global_builtin_command(&self, text: &str) -> anyhow::Result<bool> {
260 if text == "devices" {
261 self.print_all_devices();
262 return Ok(true);
263 }
264 Ok(false)
265 }
266
267 async fn execute_device_builtin_command(&self, text: &str) -> anyhow::Result<bool> {
268 assert!(self.has_current_device());
269 if text == "status" {
270 self.update_status("").await?;
271 return Ok(true);
272 }
273 if let Some(key) = text.strip_prefix("status.") {
274 self.update_status(key).await?;
275 return Ok(true);
276 }
277 Ok(false)
278 }
279
280 async fn execute_command(&self, text: &str) -> anyhow::Result<()> {
281 assert!(self.has_current_device());
282 if text.is_empty() {
283 return Ok(());
284 }
285 if self.execute_device_builtin_command(text).await? {
286 return Ok(());
287 }
288 let command = CommandRequest::from(text);
289 self.for_each_selected_device(|device| device.command(&command), |_| Ok(()))
290 .await?;
291 Ok(())
292 }
293
294 async fn update_status(&self, key: &str) -> anyhow::Result<()> {
295 self.for_each_selected_device(
296 |device: &Device| device.update_status(),
297 |device| {
298 if key.is_empty() {
299 device.write_status_to(stdout())?;
300 } else if let Some(value) = device.status_by_key(key) {
301 println!("{}", value);
302 } else {
303 log::error!(r#"No status key "{key}" for {device}"#);
304 }
305 Ok(())
306 },
307 )
308 .await?;
309 Ok(())
310 }
311
312 async fn for_each_selected_device<'a, 'b, FnAsync, Fut>(
313 &'a self,
314 fn_async: FnAsync,
315 fn_post: impl Fn(&Device) -> anyhow::Result<()>,
316 ) -> anyhow::Result<()>
317 where
318 FnAsync: Fn(&'a Device) -> Fut + Send + Sync,
319 Fut: Future<Output = anyhow::Result<()>> + Send + 'b,
320 {
321 assert!(self.has_current_device());
322
323 let results = if self.num_current_devices() < self.args.parallel_threshold {
324 log::debug!("for_each: sequential ({})", self.num_current_devices());
325 let mut results = Vec::with_capacity(self.num_current_devices());
326 for device in self.current_devices() {
327 results.push(fn_async(device).await);
328 }
329 results
330 } else {
331 log::debug!("for_each: parallel ({})", self.num_current_devices());
332 let (_, join_results) = async_scoped::TokioScope::scope_and_block(|s| {
333 for device in self.current_devices() {
334 s.spawn(fn_async(device));
335 }
336 });
337 join_results
338 .into_iter()
339 .map(|result| result.unwrap_or_else(|error| Err(error.into())))
340 .collect()
341 };
342
343 let last_error_index = results.iter().rposition(|result| result.is_err());
344 for (i, (device, result)) in zip(self.current_devices(), results).enumerate() {
345 match result {
346 Ok(_) => fn_post(device)?,
347 Err(error) => {
348 if i == last_error_index.unwrap() {
349 return Err(error);
350 }
351 log::error!("{error}");
352 }
353 }
354 }
355 Ok(())
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn parse_device_indexes() {
365 let cli = Cli::new_for_test(10);
366 assert!(cli.parse_device_indexes("").is_err());
367 assert_eq!(cli.parse_device_indexes("4").unwrap(), vec![3]);
368 assert_eq!(cli.parse_device_indexes("device4").unwrap(), vec![3]);
369 assert_eq!(cli.parse_device_indexes("2,4").unwrap(), vec![1, 3]);
370 assert_eq!(cli.parse_device_indexes("2,device4").unwrap(), vec![1, 3]);
371 assert_eq!(cli.parse_device_indexes("4,2").unwrap(), vec![3, 1]);
373 assert_eq!(cli.parse_device_indexes("device4,2").unwrap(), vec![3, 1]);
374 assert_eq!(cli.parse_device_indexes("2,4,2").unwrap(), vec![1, 3]);
376 assert_eq!(cli.parse_device_indexes("4,2,4").unwrap(), vec![3, 1]);
377 }
378
379 #[test]
380 fn parse_device_indexes_alias() {
381 let mut cli = Cli::new_for_test(10);
382 cli.args.aliases.insert("k".into(), "3,5".into());
383 assert_eq!(cli.parse_device_indexes("k").unwrap(), vec![2, 4]);
384 assert_eq!(cli.parse_device_indexes("1,k,4").unwrap(), vec![0, 2, 4, 3]);
385 cli.args.aliases.insert("j".into(), "2,k".into());
386 assert_eq!(
387 cli.parse_device_indexes("1,j,4").unwrap(),
388 vec![0, 1, 2, 4, 3]
389 );
390 assert_eq!(cli.parse_device_indexes("1,j,5").unwrap(), vec![0, 1, 2, 4]);
391 }
392
393 #[test]
394 fn parse_if_expr() {
395 assert_eq!(Cli::parse_if_expr(""), None);
396 assert_eq!(Cli::parse_if_expr("a"), None);
397 assert_eq!(Cli::parse_if_expr("if"), None);
398 assert_eq!(Cli::parse_if_expr("if/a"), None);
399 assert_eq!(Cli::parse_if_expr("if/a/b"), Some(("a", "b", "")));
400 assert_eq!(Cli::parse_if_expr("if/a/b/c"), Some(("a", "b", "c")));
401 assert_eq!(Cli::parse_if_expr("if/a//c"), Some(("a", "", "c")));
402 assert_eq!(Cli::parse_if_expr("if;a;b;c"), Some(("a", "b", "c")));
404 assert_eq!(Cli::parse_if_expr("if.a.b.c"), Some(("a", "b", "c")));
405 assert_eq!(Cli::parse_if_expr("ifXaXbXc"), None);
407 }
408}