libsurfer/
state_file_io.rs1use 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 #[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 #[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}