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 = 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        // The result should not be sorted.
363        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        // The result should be unique.
366        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        // The separator can be any characters as long as they're consistent.
394        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        // But non-alphanumeric.
397        assert_eq!(Cli::parse_if_expr("ifXaXbXc"), None);
398    }
399}