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;
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}