Skip to main content

libsurfer/
state_file_io.rs

1use std::path::PathBuf;
2
3#[cfg(not(target_arch = "wasm32"))]
4use camino::Utf8PathBuf;
5use eyre::Context;
6use rfd::FileHandle;
7use tracing::error;
8
9#[cfg(not(target_arch = "wasm32"))]
10use crate::async_util::perform_async_work;
11use crate::channels::{checked_send, checked_send_many};
12
13use crate::{
14    SystemState, async_util::AsyncJob, message::Message, wave_source::STATE_FILE_EXTENSION,
15};
16
17impl SystemState {
18    #[cfg(target_arch = "wasm32")]
19    pub fn load_state_file(&mut self, path: Option<PathBuf>) {
20        if path.is_some() {
21            return;
22        }
23        let message = move |bytes: Vec<u8>| match ron::de::from_bytes(&bytes)
24            .context("Failed loading state file")
25        {
26            Ok(s) => vec![Message::LoadState(s, path)],
27            Err(e) => {
28                error!("Failed to load state: {e:#?}");
29                vec![]
30            }
31        };
32        self.file_dialog_open(
33            "Load state",
34            (
35                format!("Surfer state files (*.{STATE_FILE_EXTENSION})"),
36                vec![STATE_FILE_EXTENSION.to_string()],
37            ),
38            message,
39        );
40    }
41
42    #[cfg(not(target_arch = "wasm32"))]
43    pub fn load_state_file(&mut self, path: Option<PathBuf>) {
44        let messages = move |path: PathBuf| {
45            let source = if let Ok(p) = Utf8PathBuf::from_path_buf(path.clone()) {
46                p
47            } else {
48                let err = eyre::eyre!("File path '{}' contains invalid UTF-8", path.display());
49                error!("{err:#?}");
50                return vec![Message::Error(err)];
51            };
52
53            match std::fs::read(source.as_std_path()) {
54                Ok(bytes) => match ron::de::from_bytes(&bytes)
55                    .context(format!("Failed loading {}", source.as_str()))
56                {
57                    Ok(s) => vec![Message::LoadState(s, Some(path))],
58                    Err(e) => {
59                        error!("Failed to load state: {e:#?}");
60                        vec![Message::Error(e)]
61                    }
62                },
63                Err(e) => {
64                    error!("Failed to load state file: {path:#?} {e:#?}");
65                    vec![Message::Error(eyre::eyre!(
66                        "Failed to read state file '{}': {e}",
67                        path.display()
68                    ))]
69                }
70            }
71        };
72        if let Some(path) = path {
73            let sender = self.channels.msg_sender.clone();
74            checked_send_many(&sender, messages(path));
75        } else {
76            // macos cannot handle dual prefixes
77            #[cfg(target_os = "macos")]
78            let ext = "ron";
79            #[cfg(not(target_os = "macos"))]
80            let ext = STATE_FILE_EXTENSION;
81            self.file_dialog_open(
82                "Load state",
83                (
84                    format!("Surfer state files (*.{STATE_FILE_EXTENSION})"),
85                    vec![ext.to_string()],
86                ),
87                messages,
88            );
89        }
90    }
91
92    #[cfg(not(target_arch = "wasm32"))]
93    pub fn save_state_file(&mut self, path: Option<PathBuf>) {
94        let Some(encoded) = self.encode_state() else {
95            return;
96        };
97
98        let messages = async move |destination: FileHandle| {
99            destination
100                .write(encoded.as_bytes())
101                .await
102                .map_err(|e| error!("Failed to write state to {destination:#?} {e:#?}"))
103                .ok();
104            vec![
105                Message::SetStateFile(destination.path().into()),
106                Message::AsyncDone(AsyncJob::SaveState),
107            ]
108        };
109        if let Some(path) = path {
110            let sender = self.channels.msg_sender.clone();
111            perform_async_work(async move {
112                checked_send_many(&sender, messages(path.into()).await);
113            });
114        } else {
115            // macos cannot handle dual prefixes
116            #[cfg(target_os = "macos")]
117            let ext = "ron";
118            #[cfg(not(target_os = "macos"))]
119            let ext = STATE_FILE_EXTENSION;
120
121            self.file_dialog_save(
122                "Save state",
123                (
124                    format!("Surfer state files (*.{STATE_FILE_EXTENSION})"),
125                    vec![ext.to_string()],
126                ),
127                messages,
128            );
129        }
130    }
131
132    #[cfg(target_arch = "wasm32")]
133    pub fn save_state_file(&mut self, path: Option<PathBuf>) {
134        if path.is_some() {
135            return;
136        }
137        let Some(encoded) = self.encode_state() else {
138            return;
139        };
140        let messages = async move |destination: FileHandle| {
141            destination
142                .write(encoded.as_bytes())
143                .await
144                .map_err(|e| error!("Failed to write state to {destination:#?} {e:#?}"))
145                .ok();
146            vec![Message::AsyncDone(AsyncJob::SaveState)]
147        };
148        self.file_dialog_save(
149            "Save state",
150            (
151                format!("Surfer state files (*.{STATE_FILE_EXTENSION})"),
152                vec![STATE_FILE_EXTENSION.to_string()],
153            ),
154            messages,
155        );
156    }
157
158    pub fn encode_state(&self) -> Option<String> {
159        let opt = ron::Options::default();
160
161        opt.to_string_pretty(&self.user, ron::ser::PrettyConfig::default())
162            .context("Failed to encode state")
163            .map_err(|e| error!("Failed to encode state. {e:#?}"))
164            .ok()
165    }
166
167    pub fn load_state_from_bytes(&mut self, bytes: Vec<u8>) {
168        match ron::de::from_bytes(&bytes).context("Failed loading state from bytes") {
169            Ok(s) => {
170                let sender = self.channels.msg_sender.clone();
171                checked_send(&sender, Message::LoadState(s, None));
172            }
173            Err(e) => {
174                error!("Failed to load state: {e:#?}");
175            }
176        }
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::StartupParams;
184
185    #[test]
186    fn test_encode_state() {
187        let state = SystemState::new_default_config()
188            .unwrap()
189            .with_params(StartupParams::default());
190        let encoded = state.encode_state();
191        assert!(encoded.is_some());
192        let encoded = encoded.unwrap();
193        assert!(encoded.contains("show_about"));
194    }
195
196    #[test]
197    fn test_load_state_from_bytes() {
198        let mut state = SystemState::new_default_config()
199            .unwrap()
200            .with_params(StartupParams::default());
201        let encoded = state.encode_state().unwrap();
202        let bytes = encoded.as_bytes().to_vec();
203
204        state.load_state_from_bytes(bytes);
205
206        let msg = state.channels.msg_receiver.try_recv().unwrap();
207        match msg {
208            Message::LoadState(..) => {}
209            _ => panic!("Expected LoadState message, got {:?}", msg),
210        }
211    }
212}