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;
9use tracing::error;
10
11use crate::SystemState;
12use crate::async_util::perform_async_work;
13use crate::message::Message;
14use crate::transactions::TRANSACTIONS_FILE_EXTENSION;
15
16#[derive(Debug, Deserialize)]
17pub enum OpenMode {
18 Open,
19 Switch,
20}
21
22impl SystemState {
23 #[cfg(not(target_arch = "wasm32"))]
24 pub fn file_dialog_open<F>(
25 &mut self,
26 title: &'static str,
27 filter: (String, Vec<String>),
28 messages: F,
29 ) where
30 F: FnOnce(PathBuf) -> Vec<Message> + Send + 'static,
31 {
32 let sender = self.channels.msg_sender.clone();
33
34 perform_async_work(async move {
35 if let Some(file) = create_file_dialog(filter, title).pick_file().await {
36 for message in messages(file.path().to_path_buf()) {
37 if let Err(e) = sender.send(message) {
38 error!("Failed to send message: {e}");
39 }
40 }
41 }
42 });
43 }
44
45 #[cfg(target_arch = "wasm32")]
46 pub fn file_dialog_open<F>(
47 &mut self,
48 title: &'static str,
49 filter: (String, Vec<String>),
50 messages: F,
51 ) where
52 F: FnOnce(Vec<u8>) -> Vec<Message> + 'static,
53 {
54 let sender = self.channels.msg_sender.clone();
55
56 perform_async_work(async move {
57 if let Some(file) = create_file_dialog(filter, title).pick_file().await {
58 for message in messages(file.read().await) {
59 if let Err(e) = sender.send(message) {
60 error!("Failed to send message: {e}");
61 }
62 }
63 }
64 });
65 }
66
67 #[cfg(not(target_arch = "wasm32"))]
68 pub fn file_dialog_save<F, Fut>(
69 &mut self,
70 title: &'static str,
71 filter: (String, Vec<String>),
72 messages: F,
73 ) where
74 F: FnOnce(FileHandle) -> Fut + Send + 'static,
75 Fut: Future<Output = Vec<Message>> + Send + 'static,
76 {
77 let sender = self.channels.msg_sender.clone();
78
79 perform_async_work(async move {
80 if let Some(file) = create_file_dialog(filter, title).save_file().await {
81 let msgs = messages(file).await;
82 for message in msgs {
83 if let Err(e) = sender.send(message) {
84 error!("Failed to send message: {e}");
85 }
86 }
87 }
88 });
89 }
90
91 #[cfg(target_arch = "wasm32")]
92 pub fn file_dialog_save<F, Fut>(
93 &mut self,
94 title: &'static str,
95 filter: (String, Vec<String>),
96 messages: F,
97 ) where
98 F: FnOnce(FileHandle) -> Fut + 'static,
99 Fut: Future<Output = Vec<Message>> + 'static,
100 {
101 let sender = self.channels.msg_sender.clone();
102
103 perform_async_work(async move {
104 if let Some(file) = create_file_dialog(filter, title).save_file().await {
105 let msgs = messages(file).await;
106 for message in msgs {
107 if let Err(e) = sender.send(message) {
108 error!("Failed to send message: {e}");
109 }
110 }
111 }
112 });
113 }
114
115 pub fn open_file_dialog(&mut self, mode: OpenMode) {
116 let load_options = (mode, self.user.config.behavior.keep_during_reload).into();
117
118 #[cfg(not(target_arch = "wasm32"))]
119 let message = move |file: PathBuf| match Utf8PathBuf::from_path_buf(file.clone()) {
120 Ok(utf8_path) => vec![Message::LoadFile(utf8_path, load_options)],
121 Err(_) => {
122 vec![Message::Error(eyre::eyre!(
123 "File path '{}' contains invalid UTF-8",
124 file.display()
125 ))]
126 }
127 };
128
129 #[cfg(target_arch = "wasm32")]
130 let message = move |file: Vec<u8>| vec![Message::LoadFromData(file, load_options)];
131
132 self.file_dialog_open(
133 "Open waveform file",
134 (
135 "Waveform/Transaction-files (*.vcd, *.fst, *.ghw, *.ftr)".to_string(),
136 vec![
137 "vcd".to_string(),
138 "fst".to_string(),
139 "ghw".to_string(),
140 TRANSACTIONS_FILE_EXTENSION.to_string(),
141 ],
142 ),
143 message,
144 );
145 }
146
147 pub fn open_command_file_dialog(&mut self) {
148 #[cfg(not(target_arch = "wasm32"))]
149 let message = move |file: PathBuf| match Utf8PathBuf::from_path_buf(file.clone()) {
150 Ok(utf8_path) => vec![Message::LoadCommandFile(utf8_path)],
151 Err(_) => {
152 vec![Message::Error(eyre::eyre!(
153 "File path '{}' contains invalid UTF-8",
154 file.display()
155 ))]
156 }
157 };
158
159 #[cfg(target_arch = "wasm32")]
160 let message = move |file: Vec<u8>| vec![Message::LoadCommandFromData(file)];
161
162 self.file_dialog_open(
163 "Open command file",
164 (
165 "Command-file (*.sucl)".to_string(),
166 vec!["sucl".to_string()],
167 ),
168 message,
169 );
170 }
171
172 #[cfg(feature = "python")]
173 pub fn open_python_file_dialog(&mut self) {
174 self.file_dialog_open(
175 "Open Python translator file",
176 ("Python files (*.py)".to_string(), vec!["py".to_string()]),
177 |file| match Utf8PathBuf::from_path_buf(file.clone()) {
178 Ok(utf8_path) => vec![Message::LoadPythonTranslator(utf8_path)],
179 Err(_) => {
180 vec![Message::Error(eyre::eyre!(
181 "File path '{}' contains invalid UTF-8",
182 file.display()
183 ))]
184 }
185 },
186 );
187 }
188}
189
190#[cfg(not(target_os = "macos"))]
191fn create_file_dialog(filter: (String, Vec<String>), title: &'static str) -> AsyncFileDialog {
192 AsyncFileDialog::new()
193 .set_title(title)
194 .add_filter(filter.0, &filter.1)
195 .add_filter("All files", &["*"])
196}
197
198#[cfg(target_os = "macos")]
199fn create_file_dialog(filter: (String, Vec<String>), title: &'static str) -> AsyncFileDialog {
200 AsyncFileDialog::new()
201 .set_title(title)
202 .add_filter(filter.0, &filter.1)
203}