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 = self.first_current_device();
219 device.update_status().await?;
220 let eval_result = device.eval_condition(condition)?;
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 execute_global_builtin_command(&self, text: &str) -> anyhow::Result<bool> {
251 if text == "devices" {
252 self.print_all_devices();
253 return Ok(true);
254 }
255 Ok(false)
256 }
257
258 async fn execute_device_builtin_command(&self, text: &str) -> anyhow::Result<bool> {
259 assert!(self.has_current_device());
260 if text == "status" {
261 self.update_status("").await?;
262 return Ok(true);
263 }
264 if let Some(key) = text.strip_prefix("status.") {
265 self.update_status(key).await?;
266 return Ok(true);
267 }
268 Ok(false)
269 }
270
271 async fn execute_command(&self, text: &str) -> anyhow::Result<()> {
272 assert!(self.has_current_device());
273 if text.is_empty() {
274 return Ok(());
275 }
276 if self.execute_device_builtin_command(text).await? {
277 return Ok(());
278 }
279 let command = CommandRequest::from(text);
280 self.for_each_selected_device(|device| device.command(&command), |_| Ok(()))
281 .await?;
282 Ok(())
283 }
284
285 async fn update_status(&self, key: &str) -> anyhow::Result<()> {
286 self.for_each_selected_device(
287 |device: &Device| device.update_status(),
288 |device| {
289 if key.is_empty() {
290 device.write_status_to(stdout())?;
291 } else if let Some(value) = device.status_by_key(key) {
292 println!("{}", value);
293 } else {
294 log::error!(r#"No status key "{key}" for {device}"#);
295 }
296 Ok(())
297 },
298 )
299 .await?;
300 Ok(())
301 }
302
303 async fn for_each_selected_device<'a, 'b, FnAsync, Fut>(
304 &'a self,
305 fn_async: FnAsync,
306 fn_post: impl Fn(&Device) -> anyhow::Result<()>,
307 ) -> anyhow::Result<()>
308 where
309 FnAsync: Fn(&'a Device) -> Fut + Send + Sync,
310 Fut: Future<Output = anyhow::Result<()>> + Send + 'b,
311 {
312 assert!(self.has_current_device());
313
314 let results = if self.num_current_devices() < self.args.parallel_threshold {
315 log::debug!("for_each: sequential ({})", self.num_current_devices());
316 let mut results = Vec::with_capacity(self.num_current_devices());
317 for device in self.current_devices() {
318 results.push(fn_async(device).await);
319 }
320 results
321 } else {
322 log::debug!("for_each: parallel ({})", self.num_current_devices());
323 let (_, join_results) = async_scoped::TokioScope::scope_and_block(|s| {
324 for device in self.current_devices() {
325 s.spawn(fn_async(device));
326 }
327 });
328 join_results
329 .into_iter()
330 .map(|result| result.unwrap_or_else(|error| Err(error.into())))
331 .collect()
332 };
333
334 let last_error_index = results.iter().rposition(|result| result.is_err());
335 for (i, (device, result)) in zip(self.current_devices(), results).enumerate() {
336 match result {
337 Ok(_) => fn_post(device)?,
338 Err(error) => {
339 if i == last_error_index.unwrap() {
340 return Err(error);
341 }
342 log::error!("{error}");
343 }
344 }
345 }
346 Ok(())
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353
354 #[test]
355 fn parse_device_indexes() {
356 let cli = Cli::new_for_test(10);
357 assert!(cli.parse_device_indexes("").is_err());
358 assert_eq!(cli.parse_device_indexes("4").unwrap(), vec![3]);
359 assert_eq!(cli.parse_device_indexes("device4").unwrap(), vec![3]);
360 assert_eq!(cli.parse_device_indexes("2,4").unwrap(), vec![1, 3]);
361 assert_eq!(cli.parse_device_indexes("2,device4").unwrap(), vec![1, 3]);
362 assert_eq!(cli.parse_device_indexes("4,2").unwrap(), vec![3, 1]);
364 assert_eq!(cli.parse_device_indexes("device4,2").unwrap(), vec![3, 1]);
365 assert_eq!(cli.parse_device_indexes("2,4,2").unwrap(), vec![1, 3]);
367 assert_eq!(cli.parse_device_indexes("4,2,4").unwrap(), vec![3, 1]);
368 }
369
370 #[test]
371 fn parse_device_indexes_alias() {
372 let mut cli = Cli::new_for_test(10);
373 cli.args.aliases.insert("k".into(), "3,5".into());
374 assert_eq!(cli.parse_device_indexes("k").unwrap(), vec![2, 4]);
375 assert_eq!(cli.parse_device_indexes("1,k,4").unwrap(), vec![0, 2, 4, 3]);
376 cli.args.aliases.insert("j".into(), "2,k".into());
377 assert_eq!(
378 cli.parse_device_indexes("1,j,4").unwrap(),
379 vec![0, 1, 2, 4, 3]
380 );
381 assert_eq!(cli.parse_device_indexes("1,j,5").unwrap(), vec![0, 1, 2, 4]);
382 }
383
384 #[test]
385 fn parse_if_expr() {
386 assert_eq!(Cli::parse_if_expr(""), None);
387 assert_eq!(Cli::parse_if_expr("a"), None);
388 assert_eq!(Cli::parse_if_expr("if"), None);
389 assert_eq!(Cli::parse_if_expr("if/a"), None);
390 assert_eq!(Cli::parse_if_expr("if/a/b"), Some(("a", "b", "")));
391 assert_eq!(Cli::parse_if_expr("if/a/b/c"), Some(("a", "b", "c")));
392 assert_eq!(Cli::parse_if_expr("if/a//c"), Some(("a", "", "c")));
393 assert_eq!(Cli::parse_if_expr("if;a;b;c"), Some(("a", "b", "c")));
395 assert_eq!(Cli::parse_if_expr("if.a.b.c"), Some(("a", "b", "c")));
396 assert_eq!(Cli::parse_if_expr("ifXaXbXc"), None);
398 }
399}