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