1use std::future::Future;
2
3#[cfg(not(target_arch = "wasm32"))]
4use camino::Utf8PathBuf;
5use rfd::{AsyncFileDialog, FileHandle};
6use serde::Deserialize;
7use tracing::error;
8#[cfg(all(target_arch = "wasm32", feature = "vscode"))]
9use wasm_bindgen::prelude::*;
10
11use crate::SystemState;
12use crate::async_util::perform_async_work;
13use crate::channels::checked_send_many;
14use crate::message::Message;
15use crate::transactions::TRANSACTIONS_FILE_EXTENSION;
16use crate::wave_source::LoadOptions;
17
18#[cfg(all(target_arch = "wasm32", feature = "vscode"))]
36#[wasm_bindgen]
37extern "C" {
38 fn vscode_show_open_dialog(kind: &str, filters_json: &str);
39}
40
41#[derive(Debug, Deserialize)]
42pub enum OpenMode {
43 Open,
44 Switch,
45}
46
47pub(crate) struct FileFilter {
48 name: &'static str,
49 extensions: &'static [&'static str],
50}
51
52static WAVEFORM_FILE_FILTER: FileFilter = FileFilter {
53 name: "Waveform/Transaction-files (*.vcd, *.fst, *.ghw, *.ftr)",
54 extensions: &["vcd", "fst", "ghw", TRANSACTIONS_FILE_EXTENSION],
55};
56
57static COMMAND_FILE_FILTER: FileFilter = FileFilter {
58 name: "Command-file (*.sucl)",
59 extensions: &["sucl"],
60};
61
62pub(crate) static STATE_FILE_FILTER: FileFilter = FileFilter {
63 name: "Surfer state files (*.surf.ron)",
64 extensions: &["surf.ron"],
65};
66
67#[cfg(not(target_arch = "wasm32"))]
68pub(crate) static FST_EXPORT_FILTER: FileFilter = FileFilter {
69 name: "FST files (*.fst)",
70 extensions: &["fst"],
71};
72
73#[cfg(any(
74 target_os = "macos",
75 all(target_arch = "wasm32", not(feature = "vscode"))
76))]
77pub(crate) static STATE_FILE_FILTER_MACOS: FileFilter = FileFilter {
79 name: "Surfer state files (*.ron)",
80 extensions: &["ron"],
81};
82
83#[cfg(feature = "python")]
84static PYTHON_FILE_FILTER: FileFilter = FileFilter {
85 name: "Python files (*.py)",
86 extensions: &["py"],
87};
88
89impl SystemState {
90 #[cfg(not(target_arch = "wasm32"))]
91 pub(crate) fn file_dialog_open<F>(
92 &mut self,
93 title: &'static str,
94 filter: &'static FileFilter,
95 messages: F,
96 ) where
97 F: FnOnce(Utf8PathBuf) -> Vec<Message> + Send + 'static,
98 {
99 let sender = self.channels.msg_sender.clone();
100
101 perform_async_work(async move {
102 if let Some(file) = create_file_dialog(filter, title).pick_file().await {
103 let path = file.path().to_path_buf();
104 let result = match Utf8PathBuf::from_path_buf(path.clone()) {
105 Ok(utf8_path) => messages(utf8_path),
106 Err(_) => {
107 error!("File path '{}' contains invalid UTF-8", path.display());
108 vec![]
109 }
110 };
111 checked_send_many(&sender, result);
112 }
113 });
114 }
115
116 #[cfg(all(target_arch = "wasm32", not(feature = "vscode")))]
117 pub(crate) fn file_dialog_open<F>(
118 &mut self,
119 title: &'static str,
120 filter: &'static FileFilter,
121 messages: F,
122 ) where
123 F: FnOnce(Vec<u8>) -> Vec<Message> + 'static,
124 {
125 let sender = self.channels.msg_sender.clone();
126
127 perform_async_work(async move {
128 if let Some(file) = create_file_dialog(filter, title).pick_file().await {
129 checked_send_many(&sender, messages(file.read().await));
130 }
131 });
132 }
133
134 #[cfg(not(target_arch = "wasm32"))]
135 pub(crate) fn file_dialog_save<F, Fut>(
136 &mut self,
137 title: &'static str,
138 filter: &'static FileFilter,
139 default_file_name: Option<String>,
140 messages: F,
141 ) where
142 F: FnOnce(FileHandle) -> Fut + Send + 'static,
143 Fut: Future<Output = Vec<Message>> + Send + 'static,
144 {
145 let sender = self.channels.msg_sender.clone();
146
147 perform_async_work(async move {
148 let mut dialog = create_file_dialog(filter, title);
149 if let Some(file_name) = default_file_name {
150 dialog = dialog.set_file_name(&file_name);
151 }
152 if let Some(file) = dialog.save_file().await {
153 checked_send_many(&sender, messages(file).await);
154 }
155 });
156 }
157
158 #[cfg(all(target_arch = "wasm32", not(feature = "vscode")))]
159 pub(crate) fn file_dialog_save<F, Fut>(
160 &mut self,
161 title: &'static str,
162 filter: &'static FileFilter,
163 default_file_name: Option<String>,
164 messages: F,
165 ) where
166 F: FnOnce(FileHandle) -> Fut + 'static,
167 Fut: Future<Output = Vec<Message>> + 'static,
168 {
169 let sender = self.channels.msg_sender.clone();
170
171 perform_async_work(async move {
172 let mut dialog = create_file_dialog(filter, title);
173 if let Some(file_name) = default_file_name {
174 dialog = dialog.set_file_name(&file_name);
175 }
176 if let Some(file) = dialog.save_file().await {
177 checked_send_many(&sender, messages(file).await);
178 }
179 });
180 }
181
182 #[cfg(all(target_arch = "wasm32", feature = "vscode"))]
183 pub(crate) fn open_file_dialog(&mut self, mode: OpenMode) {
184 let load_options: LoadOptions = (mode, self.user.config.behavior.keep_during_reload).into();
185
186 let kind = match load_options {
187 LoadOptions::Clear => "waveform_clear",
188 LoadOptions::KeepAvailable => "waveform_keep_available",
189 LoadOptions::KeepAll => "waveform_keep_all",
190 };
191 vscode_open_dialog_with_filter(kind, &WAVEFORM_FILE_FILTER);
192 }
193
194 #[cfg(not(target_arch = "wasm32"))]
195 pub(crate) fn open_file_dialog(&mut self, mode: OpenMode) {
196 let load_options: LoadOptions = (mode, self.user.config.behavior.keep_during_reload).into();
197
198 let message = move |file: Utf8PathBuf| vec![Message::LoadFile(file, load_options)];
199
200 self.file_dialog_open("Open waveform file", &WAVEFORM_FILE_FILTER, message);
201 }
202
203 #[cfg(all(target_arch = "wasm32", not(feature = "vscode")))]
204 pub(crate) fn open_file_dialog(&mut self, mode: OpenMode) {
205 let load_options: LoadOptions = (mode, self.user.config.behavior.keep_during_reload).into();
206
207 let message = move |file: Vec<u8>| vec![Message::LoadFromData(file, load_options)];
208
209 self.file_dialog_open("Open waveform file", &WAVEFORM_FILE_FILTER, message);
210 }
211
212 #[cfg(all(target_arch = "wasm32", feature = "vscode"))]
213 pub(crate) fn open_command_file_dialog(&mut self) {
214 vscode_open_dialog_with_filter("command_file", &COMMAND_FILE_FILTER);
215 }
216
217 #[cfg(not(target_arch = "wasm32"))]
218 pub(crate) fn open_command_file_dialog(&mut self) {
219 let message = move |file: Utf8PathBuf| vec![Message::LoadCommandFile(file)];
220
221 self.file_dialog_open("Open command file", &COMMAND_FILE_FILTER, message);
222 }
223
224 #[cfg(all(target_arch = "wasm32", not(feature = "vscode")))]
225 pub(crate) fn open_command_file_dialog(&mut self) {
226 self.file_dialog_open(
227 "Open command file",
228 &COMMAND_FILE_FILTER,
229 |file: Vec<u8>| vec![Message::LoadCommandFromData(file)],
230 );
231 }
232
233 #[cfg(feature = "python")]
234 pub(crate) fn open_python_file_dialog(&mut self) {
235 self.file_dialog_open("Open Python translator file", &PYTHON_FILE_FILTER, |file| {
236 vec![Message::LoadPythonTranslator(file)]
237 });
238 }
239}
240
241#[cfg(not(all(target_arch = "wasm32", feature = "vscode")))]
242#[cfg(not(target_os = "macos"))]
243fn create_file_dialog(filter: &'static FileFilter, title: &'static str) -> AsyncFileDialog {
244 AsyncFileDialog::new()
245 .set_title(title)
246 .add_filter(filter.name, filter.extensions)
247 .add_filter("All files", &["*"])
248}
249
250#[cfg(not(all(target_arch = "wasm32", feature = "vscode")))]
251#[cfg(target_os = "macos")]
252fn create_file_dialog(filter: &'static FileFilter, title: &'static str) -> AsyncFileDialog {
253 AsyncFileDialog::new()
254 .set_title(title)
255 .add_filter(filter.name, &filter.extensions)
256}
257
258#[cfg(all(target_arch = "wasm32", feature = "vscode"))]
263pub(crate) fn vscode_open_dialog_with_filter(kind: &str, filter: &'static FileFilter) {
264 let filters_json = filters_to_json(filter);
265 vscode_show_open_dialog(kind, &filters_json);
266}
267
268#[cfg(all(target_arch = "wasm32", feature = "vscode"))]
269fn filters_to_json(filter: &'static FileFilter) -> String {
270 let exts = filter
271 .extensions
272 .iter()
273 .map(|e| format!("{e:?}"))
274 .collect::<Vec<_>>()
275 .join(",");
276 format!("[{{\"name\":{:?},\"extensions\":[{exts}]}}]", filter.name)
277}