1use std::io::stdout;
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 for device in self.current_devices() {
249 device.command(command).await?;
250 }
251 Ok(())
252 }
253
254 async fn update_status(&mut self, key: &str) -> anyhow::Result<()> {
255 for device in self.current_devices() {
256 device.update_status().await?;
257
258 if key.is_empty() {
259 device.write_status_to(stdout())?;
260 } else if let Some(value) = device.status_by_key(key) {
261 println!("{}", value);
262 } else {
263 log::error!(r#"No status key "{key}" for {device}"#);
264 }
265 }
266 Ok(())
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn parse_device_indexes() {
276 let cli = Cli::new_for_test(10);
277 assert!(cli.parse_device_indexes("").is_err());
278 assert_eq!(cli.parse_device_indexes("4").unwrap(), vec![3]);
279 assert_eq!(cli.parse_device_indexes("device4").unwrap(), vec![3]);
280 assert_eq!(cli.parse_device_indexes("2,4").unwrap(), vec![1, 3]);
281 assert_eq!(cli.parse_device_indexes("2,device4").unwrap(), vec![1, 3]);
282 assert_eq!(cli.parse_device_indexes("4,2").unwrap(), vec![3, 1]);
284 assert_eq!(cli.parse_device_indexes("device4,2").unwrap(), vec![3, 1]);
285 assert_eq!(cli.parse_device_indexes("2,4,2").unwrap(), vec![1, 3]);
287 assert_eq!(cli.parse_device_indexes("4,2,4").unwrap(), vec![3, 1]);
288 }
289
290 #[test]
291 fn parse_device_indexes_alias() {
292 let mut cli = Cli::new_for_test(10);
293 cli.args.aliases.insert("k".into(), "3,5".into());
294 assert_eq!(cli.parse_device_indexes("k").unwrap(), vec![2, 4]);
295 assert_eq!(cli.parse_device_indexes("1,k,4").unwrap(), vec![0, 2, 4, 3]);
296 cli.args.aliases.insert("j".into(), "2,k".into());
297 assert_eq!(
298 cli.parse_device_indexes("1,j,4").unwrap(),
299 vec![0, 1, 2, 4, 3]
300 );
301 assert_eq!(cli.parse_device_indexes("1,j,5").unwrap(), vec![0, 1, 2, 4]);
302 }
303
304 #[test]
305 fn parse_if_expr() {
306 assert_eq!(Cli::parse_if_expr(""), None);
307 assert_eq!(Cli::parse_if_expr("a"), None);
308 assert_eq!(Cli::parse_if_expr("if"), None);
309 assert_eq!(Cli::parse_if_expr("if/a"), None);
310 assert_eq!(Cli::parse_if_expr("if/a/b"), Some(("a", "b", "")));
311 assert_eq!(Cli::parse_if_expr("if/a/b/c"), Some(("a", "b", "c")));
312 assert_eq!(Cli::parse_if_expr("if/a//c"), Some(("a", "", "c")));
313 assert_eq!(Cli::parse_if_expr("if;a;b;c"), Some(("a", "b", "c")));
315 assert_eq!(Cli::parse_if_expr("if.a.b.c"), Some(("a", "b", "c")));
316 assert_eq!(Cli::parse_if_expr("ifXaXbXc"), None);
318 }
319}