Skip to main content

libsurfer/
file_dialog.rs

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