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