1use std::path::PathBuf;
10
11use serde::{Deserialize, Serialize};
12
13pub use blockworx_tools::file::{FilePick, FileRequest, PickReceiver, SaveScope};
14
15pub use blockworx_store::file::{
16 as_container_path, container_name, create_container, names_a_container, names_an_archive,
17 open_archive, open_container, refused_as_a_diagram, renamed_beside, save_container_through,
18};
19pub use blockworx_store::storage::CONTAINER_EXTENSION;
20
21pub fn document_name(root: &std::path::Path) -> String {
26 crate::import::file_stem(&container_name(root))
27}
28
29const RECENT_KEY: &str = "recent_containers";
31
32#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(transparent)]
36pub struct RecentFiles(Vec<PathBuf>);
37
38impl RecentFiles {
39 pub fn paths(&self) -> &[PathBuf] {
40 &self.0
41 }
42
43 pub fn remember(&mut self, path: &std::path::Path) {
45 blockworx_store::recent::remember(&mut self.0, &path.to_path_buf());
46 }
47
48 pub fn forget(&mut self, path: &std::path::Path) {
51 blockworx_store::recent::forget(&mut self.0, &path.to_path_buf());
52 }
53
54 pub fn restore(storage: &dyn eframe::Storage) -> Self {
55 storage
56 .get_string(RECENT_KEY)
57 .and_then(|text| serde_json::from_str(&text).ok())
58 .unwrap_or_default()
59 }
60
61 pub fn save(&self, storage: &mut dyn eframe::Storage) {
62 match serde_json::to_string(self) {
63 Ok(text) => storage.set_string(RECENT_KEY, text),
64 Err(e) => tracing::error!("Failed to serialize the recent-files list: {e}"),
65 }
66 }
67}
68
69pub fn spawn_file_dialog(ctx: &egui::Context, request: FileRequest) -> PickReceiver {
72 let (tx, rx) = std::sync::mpsc::channel();
73 let thread_ctx = ctx.clone();
74 std::thread::spawn(move || {
75 let _ = tx.send(pick(request));
76 thread_ctx.request_repaint();
77 });
78 ctx.request_repaint();
79 rx
80}
81
82fn pick(request: FileRequest) -> Option<FilePick> {
91 match request {
92 FileRequest::OpenContainer => rfd::FileDialog::new()
93 .set_title("Open a diagram (.bwx)")
94 .pick_folder()
95 .map(FilePick::Container),
96 FileRequest::SaveAsContainer(scope) => rfd::FileDialog::new()
97 .set_title("Save diagram as")
98 .set_file_name(format!("diagram.{CONTAINER_EXTENSION}"))
99 .save_file()
100 .map(|path| FilePick::NewContainer(as_container_path(&path), scope)),
101 }
102}
103
104#[cfg(test)]
105pub(crate) mod tests {
106 use super::*;
107 use std::collections::HashMap;
108 use std::path::Path;
109
110 #[derive(Default)]
113 pub struct MemoryStorage(HashMap<String, String>);
114
115 impl eframe::Storage for MemoryStorage {
116 fn get_string(&self, key: &str) -> Option<String> {
117 self.0.get(key).cloned()
118 }
119
120 fn set_string(&mut self, key: &str, value: String) {
121 self.0.insert(key.to_owned(), value);
122 }
123
124 fn remove_string(&mut self, key: &str) {
125 self.0.remove(key);
126 }
127
128 fn flush(&mut self) {}
129 }
130
131 #[test]
132 fn a_container_names_the_document_inside_it_without_its_extension() {
133 assert_eq!(
134 document_name(Path::new("/home/ada/motor-controller.bwx")),
135 "motor-controller"
136 );
137 assert_eq!(
138 document_name(Path::new("/home/ada/notes.v2.bwx")),
139 "notes.v2"
140 );
141 }
142
143 #[test]
144 fn the_recent_list_round_trips_through_the_storage_seam() {
145 let mut storage = MemoryStorage::default();
146 assert!(
147 RecentFiles::restore(&storage).paths().is_empty(),
148 "precondition: nothing has been stored yet",
149 );
150
151 let mut recent = RecentFiles::default();
152 recent.remember(Path::new("/tmp/one.bwx"));
153 recent.remember(Path::new("/tmp/two.bwx"));
154 recent.save(&mut storage);
155
156 assert_eq!(RecentFiles::restore(&storage), recent);
157 }
158
159 #[test]
163 fn a_malformed_stored_list_restores_empty() {
164 use eframe::Storage as _;
165
166 let mut storage = MemoryStorage::default();
167 storage.set_string(RECENT_KEY, "not a list".to_owned());
168 assert!(RecentFiles::restore(&storage).paths().is_empty());
169 }
170}