1use std::{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 current_devices_as<'a, T, F>(&'a self, f: F) -> impl Iterator<Item = T> + 'a
40 where
41 F: Fn(usize) -> T + 'a,
42 {
43 self.current_device_indexes
44 .iter()
45 .map(move |&index| f(index))
46 }
47
48 fn current_devices(&self) -> impl Iterator<Item = &Device> {
49 self.current_devices_as(|index| &self.devices()[index])
50 }
51
52 fn current_devices_with_index(&self) -> impl Iterator<Item = (usize, &Device)> {
53 self.current_devices_as(|index| (index, &self.devices()[index]))
54 }
55
56 fn first_current_device(&self) -> &Device {
57 &self.devices()[self.current_device_indexes[0]]
58 }
59
60 pub async fn run(&mut self) -> anyhow::Result<()> {
61 self.run_core().await?;
62 self.args.save()?;
63 Ok(())
64 }
65
66 async fn run_core(&mut self) -> anyhow::Result<()> {
67 if !self.args.alias_updates.is_empty() {
68 self.args.print_aliases();
69 if self.args.commands.is_empty() {
70 return Ok(());
71 }
72 }
73
74 self.switch_bot = self.args.create_switch_bot()?;
75 self.switch_bot.load_devices().await?;
76
77 if !self.args.commands.is_empty() {
78 self.execute_args(&self.args.commands.clone()).await?;
79 return Ok(());
80 }
81
82 self.run_interactive().await?;
83 Ok(())
84 }
85
86 async fn run_interactive(&mut self) -> anyhow::Result<()> {
87 let mut input = UserInput::new();
88 self.print_devices();
89 loop {
90 input.set_prompt(if self.has_current_device() {
91 "Command> "
92 } else {
93 "Device> "
94 });
95
96 let input_text = input.read_line()?;
97 match input_text {
98 "q" => break,
99 "" => {
100 if self.has_current_device() {
101 self.current_device_indexes.clear();
102 self.print_devices();
103 continue;
104 }
105 break;
106 }
107 _ => match self.execute(input_text).await {
108 Ok(true) => self.print_devices(),
109 Ok(false) => {}
110 Err(error) => log::error!("{error}"),
111 },
112 }
113 }
114 Ok(())
115 }
116
117 fn print_devices(&self) {
118 if !self.has_current_device() {
119 for (i, device) in self.devices().iter().enumerate() {
120 println!("{}: {device}", i + 1);
121 }
122 return;
123 }
124
125 if self.current_device_indexes.len() >= 2 {
126 for (i, device) in self.current_devices_with_index() {
127 println!("{}: {device}", i + 1);
128 }
129 return;
130 }
131
132 let device = self.first_current_device();
133 print!("{device:#}");
134 }
135
136 async fn execute_args(&mut self, list: &[String]) -> anyhow::Result<()> {
137 for command in list {
138 self.execute(command).await?;
139 }
140 Ok(())
141 }
142
143 async fn execute(&mut self, text: &str) -> anyhow::Result<bool> {
144 if let Some(alias) = self.args.aliases.get(text) {
145 log::debug!(r#"alias: "{text}" -> "{alias}""#);
146 return self.execute_no_alias(&alias.clone()).await;
147 }
148 self.execute_no_alias(text).await
149 }
150
151 async fn execute_no_alias(&mut self, text: &str) -> anyhow::Result<bool> {
152 let set_device_result = self.set_current_devices(text);
153 if set_device_result.is_ok() {
154 return Ok(true);
155 }
156 if self.has_current_device() {
157 if text == "status" {
158 self.update_status("").await?;
159 return Ok(false);
160 }
161 if let Some(key) = text.strip_prefix("status.") {
162 self.update_status(key).await?;
163 return Ok(false);
164 }
165 if self.execute_if_expr(text).await? {
166 return Ok(false);
167 }
168 self.execute_command(text).await?;
169 return Ok(false);
170 }
171 Err(set_device_result.unwrap_err())
172 }
173
174 fn set_current_devices(&mut self, text: &str) -> anyhow::Result<()> {
175 self.current_device_indexes = self.parse_device_indexes(text)?;
176 log::debug!("current_device_indexes={:?}", self.current_device_indexes);
177 Ok(())
178 }
179
180 fn parse_device_indexes(&self, value: &str) -> anyhow::Result<Vec<usize>> {
181 let values = value.split(',');
182 let mut indexes: Vec<usize> = Vec::new();
183 for s in values {
184 if let Some(alias) = self.args.aliases.get(s) {
185 indexes.extend(self.parse_device_indexes(alias)?);
186 continue;
187 }
188 indexes.push(self.parse_device_index(s)?);
189 }
190 indexes = indexes.into_iter().unique().collect::<Vec<_>>();
191 Ok(indexes)
192 }
193
194 fn parse_device_index(&self, value: &str) -> anyhow::Result<usize> {
195 if let Ok(number) = value.parse::<usize>() {
196 if number > 0 && number <= self.devices().len() {
197 return Ok(number - 1);
198 }
199 }
200 self.devices()
201 .index_by_device_id(value)
202 .ok_or_else(|| anyhow::anyhow!("Not a valid device: \"{value}\""))
203 }
204
205 async fn execute_if_expr(&mut self, expr: &str) -> anyhow::Result<bool> {
206 if let Some((condition, then_command, else_command)) = Self::parse_if_expr(expr) {
207 let device = self.first_current_device();
208 device.update_status().await?;
209 let eval_result = device.eval_condition(condition)?;
210 let command = if eval_result {
211 then_command
212 } else {
213 else_command
214 };
215 log::debug!("if: {condition} is {eval_result}, execute {command}");
216 if let Some(alias) = self.args.aliases.get(command) {
217 self.execute_command(alias.as_str()).await?;
218 } else {
219 self.execute_command(command).await?;
220 }
221 return Ok(true);
222 }
223 Ok(false)
224 }
225
226 fn parse_if_expr(text: &str) -> Option<(&str, &str, &str)> {
227 if let Some(text) = text.strip_prefix("if") {
228 if let Some(sep) = text.chars().nth(0) {
229 if sep.is_alphanumeric() {
230 return None;
231 }
232 let fields: Vec<&str> = text[1..].split_terminator(sep).collect();
233 match fields.len() {
234 2 => return Some((fields[0], fields[1], "")),
235 3 => return Some((fields[0], fields[1], fields[2])),
236 _ => {}
237 }
238 }
239 }
240 None
241 }
242
243 async fn execute_command(&self, text: &str) -> anyhow::Result<()> {
244 if text.is_empty() {
245 return Ok(());
246 }
247 let command = CommandRequest::from(text);
248 let (_, results) = async_scoped::TokioScope::scope_and_block(|s| {
249 for device in self.current_devices() {
250 s.spawn(device.command(&command));
251 }
252 });
253
254 let results = Self::merge_join_errors(results);
255 for (result, is_last_error) in results {
256 if is_last_error {
257 return result;
258 }
259 if let Err(error) = result {
260 log::error!("{error}");
261 continue;
262 }
263 }
264 Ok(())
265 }
266
267 async fn update_status(&self, key: &str) -> anyhow::Result<()> {
268 let (_, results) = async_scoped::TokioScope::scope_and_block(|s| {
269 for device in self.current_devices() {
270 s.spawn(device.update_status());
271 }
272 });
273
274 let results = Self::merge_join_errors(results);
275 for (device, (result, is_last_error)) in zip(self.current_devices(), results) {
276 if is_last_error {
277 return result;
278 }
279 if let Err(error) = result {
280 log::error!("{error}");
281 continue;
282 }
283 if key.is_empty() {
284 device.write_status_to(stdout())?;
285 } else if let Some(value) = device.status_by_key(key) {
286 println!("{}", value);
287 } else {
288 log::error!(r#"No status key "{key}" for {device}"#);
289 }
290 }
291 Ok(())
292 }
293
294 fn merge_join_errors(
295 results: Vec<Result<anyhow::Result<()>, tokio::task::JoinError>>,
296 ) -> Vec<(anyhow::Result<()>, bool)> {
297 let mut output: Vec<(anyhow::Result<()>, bool)> = vec![];
298 let mut last_error_index = None;
299 for result in results {
300 let result = result.unwrap_or_else(|error| Err(error.into()));
301 if result.is_err() {
302 last_error_index = Some(output.len());
303 }
304 output.push((result, false));
305 }
306 if let Some(last_error_index) = last_error_index {
307 output[last_error_index].1 = true;
308 }
309 output
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn parse_device_indexes() {
319 let cli = Cli::new_for_test(10);
320 assert!(cli.parse_device_indexes("").is_err());
321 assert_eq!(cli.parse_device_indexes("4").unwrap(), vec![3]);
322 assert_eq!(cli.parse_device_indexes("device4").unwrap(), vec![3]);
323 assert_eq!(cli.parse_device_indexes("2,4").unwrap(), vec![1, 3]);
324 assert_eq!(cli.parse_device_indexes("2,device4").unwrap(), vec![1, 3]);
325 assert_eq!(cli.parse_device_indexes("4,2").unwrap(), vec![3, 1]);
327 assert_eq!(cli.parse_device_indexes("device4,2").unwrap(), vec![3, 1]);
328 assert_eq!(cli.parse_device_indexes("2,4,2").unwrap(), vec![1, 3]);
330 assert_eq!(cli.parse_device_indexes("4,2,4").unwrap(), vec![3, 1]);
331 }
332
333 #[test]
334 fn parse_device_indexes_alias() {
335 let mut cli = Cli::new_for_test(10);
336 cli.args.aliases.insert("k".into(), "3,5".into());
337 assert_eq!(cli.parse_device_indexes("k").unwrap(), vec![2, 4]);
338 assert_eq!(cli.parse_device_indexes("1,k,4").unwrap(), vec![0, 2, 4, 3]);
339 cli.args.aliases.insert("j".into(), "2,k".into());
340 assert_eq!(
341 cli.parse_device_indexes("1,j,4").unwrap(),
342 vec![0, 1, 2, 4, 3]
343 );
344 assert_eq!(cli.parse_device_indexes("1,j,5").unwrap(), vec![0, 1, 2, 4]);
345 }
346
347 #[test]
348 fn parse_if_expr() {
349 assert_eq!(Cli::parse_if_expr(""), None);
350 assert_eq!(Cli::parse_if_expr("a"), None);
351 assert_eq!(Cli::parse_if_expr("if"), None);
352 assert_eq!(Cli::parse_if_expr("if/a"), None);
353 assert_eq!(Cli::parse_if_expr("if/a/b"), Some(("a", "b", "")));
354 assert_eq!(Cli::parse_if_expr("if/a/b/c"), Some(("a", "b", "c")));
355 assert_eq!(Cli::parse_if_expr("if/a//c"), Some(("a", "", "c")));
356 assert_eq!(Cli::parse_if_expr("if;a;b;c"), Some(("a", "b", "c")));
358 assert_eq!(Cli::parse_if_expr("if.a.b.c"), Some(("a", "b", "c")));
359 assert_eq!(Cli::parse_if_expr("ifXaXbXc"), None);
361 }
362}