switchbot_cli/
cli.rs

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    /// Returns `true` if the current devices are changed.
166    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        // The result should not be sorted.
372        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        // The result should be unique.
375        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        // The separator can be any characters as long as they're consistent.
403        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        // But non-alphanumeric.
406        assert_eq!(Cli::parse_if_expr("ifXaXbXc"), None);
407    }
408}