libsurfer/
batch_commands.rs1use camino::Utf8PathBuf;
2use eyre::Context as _;
3use futures::FutureExt as _;
4use tracing::{error, info, trace};
5
6use crate::{
7 SystemState,
8 async_util::perform_async_work,
9 command_parser::get_parser,
10 fzcmd::parse_command,
11 message::Message,
12 wave_source::{LoadProgress, LoadProgressStatus},
13};
14
15impl SystemState {
16 pub(crate) fn handle_batch_commands(&mut self) {
18 let mut should_exit = false;
19 while self.can_start_batch_command() {
21 if let Some(cmd) = self.batch_messages.pop_front() {
22 if matches!(cmd, Message::Exit) {
23 should_exit = true;
24 }
25 info!("Applying batch command: {cmd:?}");
26 self.update(cmd);
27 } else {
28 break; }
30 }
31
32 if !self.batch_messages_completed
34 && self.batch_messages.is_empty()
35 && self.can_start_batch_command()
36 {
37 self.batch_messages_completed = true;
38
39 if should_exit {
40 info!("Exiting due to batch command");
41 let sender = self.channels.msg_sender.clone();
42 if let Err(e) = sender.send(Message::Exit) {
43 error!("Failed to send exit message: {e}");
44 }
45 }
46 }
47 }
48
49 pub(crate) fn can_start_batch_command(&self) -> bool {
51 self.progress_tracker.is_none()
53 }
54
55 pub fn batch_commands_completed(&self) -> bool {
57 debug_assert!(
58 self.batch_messages_completed || !self.batch_messages.is_empty(),
59 "completed implies no commands"
60 );
61 self.batch_messages_completed
62 }
63
64 pub fn add_batch_commands<I: IntoIterator<Item = String>>(&mut self, commands: I) {
65 let parsed = self.parse_batch_commands(commands);
66 for msg in parsed {
67 self.batch_messages.push_back(msg);
68 self.batch_messages_completed = false;
69 }
70 }
71
72 pub fn add_batch_messages<I: IntoIterator<Item = Message>>(&mut self, messages: I) {
73 for msg in messages {
74 self.batch_messages.push_back(msg);
75 self.batch_messages_completed = false;
76 }
77 }
78
79 pub fn add_batch_message(&mut self, msg: Message) {
80 self.add_batch_messages([msg]);
81 }
82
83 pub fn parse_batch_commands<I: IntoIterator<Item = String>>(
84 &mut self,
85 cmds: I,
86 ) -> Vec<Message> {
87 trace!("Parsing batch commands");
88
89 cmds
90 .into_iter()
91 .enumerate()
93 .map(|(no, line)| {
95 trace!("{no: >2} {line}");
96 (no, line)
97 })
98 .map(|(no, line)| (no + 1, line))
100 .map(|(no, line)| (no, line.trim().to_string()))
101 .map(|(no, line)| (no, line.split('#').next().unwrap().to_string()))
103 .filter(|(_no, line)| !line.is_empty())
104 .flat_map(|(no, line)| {
105 line.split(';')
106 .map(|cmd| (no, cmd.trim().to_string()))
107 .collect::<Vec<_>>()
108 })
109 .filter_map(|(no, command)| {
110 if command.starts_with("run_command_file ") {
111 #[cfg(not(target_arch = "wasm32"))]
115 {
116 if let Some(path_str) = command.split_ascii_whitespace().nth(1) {
117 match Utf8PathBuf::from_path_buf(path_str.into()) {
118 Ok(utf8_path) => {
119 self.add_batch_commands(read_command_file(&utf8_path));
120 }
121 Err(_) => {
122 error!("Invalid UTF-8 path in run_command_file on line {no}: {path_str}");
123 }
124 }
125 } else {
126 error!("Missing file path in run_command_file command on line {no}");
127 }
128 }
129 #[cfg(target_arch = "wasm32")]
130 error!("Cannot use run_command_file in command files running on WASM");
131 None
132 } else {
133 parse_command(&command, get_parser(self))
134 .map_err(|e| {
135 error!("Error on batch commands line {no}: {e:#?}");
136 e
137 })
138 .ok()
139 }
140 })
141 .collect::<Vec<_>>()
142 }
143
144 pub fn load_commands_from_url(&mut self, url: String) {
145 let sender = self.channels.msg_sender.clone();
146 let url_ = url.clone();
147 perform_async_work(async move {
148 let maybe_response = reqwest::get(&url)
149 .map(|e| e.with_context(|| format!("Failed fetch download {url}")))
150 .await;
151 let response: reqwest::Response = match maybe_response {
152 Ok(r) => r,
153 Err(e) => {
154 if let Err(e) = sender.send(Message::Error(e)) {
155 error!("Failed to send error message: {e}");
156 }
157 return;
158 }
159 };
160
161 let bytes = response
163 .bytes()
164 .map(|e| e.with_context(|| format!("Failed to download {url}")))
165 .await;
166
167 let msg = match bytes {
168 Ok(b) => Message::CommandFileDownloaded(url, b),
169 Err(e) => Message::Error(e),
170 };
171 if let Err(e) = sender.send(msg) {
172 error!("Failed to send message: {e}");
173 }
174 });
175
176 self.progress_tracker = Some(LoadProgress::new(LoadProgressStatus::Downloading(url_)));
177 }
178}
179
180#[must_use]
181pub fn read_command_file(cmd_file: &Utf8PathBuf) -> Vec<String> {
182 std::fs::read_to_string(cmd_file)
183 .map_err(|e| error!("Failed to read commands from {cmd_file}. {e:#?}"))
184 .ok()
185 .map(|file_content| file_content.lines().map(str::to_string).collect())
186 .unwrap_or_default()
187}
188
189#[must_use]
190pub fn read_command_bytes(bytes: Vec<u8>) -> Vec<String> {
191 String::from_utf8(bytes)
192 .map_err(|e| error!("Failed to read commands from file. {e:#?}"))
193 .ok()
194 .map(|file_content| file_content.lines().map(str::to_string).collect())
195 .unwrap_or_default()
196}