ricecoder_keybinds/
persistence.rs1use std::fs;
54use std::path::{Path, PathBuf};
55
56use crate::error::PersistenceError;
57use crate::profile::Profile;
58
59pub trait KeybindPersistence: Send + Sync {
61 fn save_profile(&self, profile: &Profile) -> Result<(), PersistenceError>;
63
64 fn load_profile(&self, name: &str) -> Result<Profile, PersistenceError>;
66
67 fn delete_profile(&self, name: &str) -> Result<(), PersistenceError>;
69
70 fn list_profiles(&self) -> Result<Vec<String>, PersistenceError>;
72}
73
74pub struct FileSystemPersistence {
76 config_dir: PathBuf,
77}
78
79impl FileSystemPersistence {
80 pub fn new(config_dir: impl AsRef<Path>) -> Result<Self, PersistenceError> {
82 let config_dir = config_dir.as_ref().to_path_buf();
83
84 if !config_dir.exists() {
86 fs::create_dir_all(&config_dir).map_err(|e| {
87 PersistenceError::IoError(std::io::Error::new(
88 e.kind(),
89 format!("Failed to create config directory: {}", e),
90 ))
91 })?;
92 }
93
94 Ok(FileSystemPersistence { config_dir })
95 }
96
97 fn profile_path(&self, name: &str) -> PathBuf {
99 self.config_dir.join(format!("{}.json", name))
100 }
101
102 fn active_profile_path(&self) -> PathBuf {
104 self.config_dir.join("active_profile.txt")
105 }
106}
107
108impl KeybindPersistence for FileSystemPersistence {
109 fn save_profile(&self, profile: &Profile) -> Result<(), PersistenceError> {
110 let path = self.profile_path(&profile.name);
111
112 let json = serde_json::to_string_pretty(profile).map_err(|e| {
113 PersistenceError::SerializationError(format!("Failed to serialize profile: {}", e))
114 })?;
115
116 fs::write(&path, json).map_err(|e| {
117 PersistenceError::IoError(std::io::Error::new(
118 e.kind(),
119 format!("Failed to write profile file: {}", e),
120 ))
121 })?;
122
123 Ok(())
124 }
125
126 fn load_profile(&self, name: &str) -> Result<Profile, PersistenceError> {
127 let path = self.profile_path(name);
128
129 if !path.exists() {
130 return Err(PersistenceError::ProfileNotFound(name.to_string()));
131 }
132
133 let content = fs::read_to_string(&path).map_err(|e| {
134 if e.kind() == std::io::ErrorKind::NotFound {
135 PersistenceError::ProfileNotFound(name.to_string())
136 } else if e.kind() == std::io::ErrorKind::PermissionDenied {
137 PersistenceError::PermissionDenied(path.to_string_lossy().to_string())
138 } else {
139 PersistenceError::IoError(e)
140 }
141 })?;
142
143 let profile: Profile = serde_json::from_str(&content).map_err(|e| {
144 PersistenceError::CorruptedJson(format!("Failed to parse profile: {}", e))
145 })?;
146
147 Ok(profile)
148 }
149
150 fn delete_profile(&self, name: &str) -> Result<(), PersistenceError> {
151 let path = self.profile_path(name);
152
153 if !path.exists() {
154 return Err(PersistenceError::ProfileNotFound(name.to_string()));
155 }
156
157 fs::remove_file(&path).map_err(|e| {
158 if e.kind() == std::io::ErrorKind::PermissionDenied {
159 PersistenceError::PermissionDenied(path.to_string_lossy().to_string())
160 } else {
161 PersistenceError::IoError(e)
162 }
163 })?;
164
165 Ok(())
166 }
167
168 fn list_profiles(&self) -> Result<Vec<String>, PersistenceError> {
169 let mut profiles = Vec::new();
170
171 if !self.config_dir.exists() {
172 return Ok(profiles);
173 }
174
175 let entries = fs::read_dir(&self.config_dir).map_err(|e| {
176 PersistenceError::IoError(std::io::Error::new(
177 e.kind(),
178 format!("Failed to read config directory: {}", e),
179 ))
180 })?;
181
182 for entry in entries {
183 let entry = entry.map_err(|e| {
184 PersistenceError::IoError(std::io::Error::new(
185 e.kind(),
186 format!("Failed to read directory entry: {}", e),
187 ))
188 })?;
189
190 let path = entry.path();
191 if path.extension().is_some_and(|ext| ext == "json") {
192 if let Some(name) = path.file_stem().and_then(|n| n.to_str()) {
193 profiles.push(name.to_string());
194 }
195 }
196 }
197
198 profiles.sort();
199 Ok(profiles)
200 }
201}
202
203impl FileSystemPersistence {
204 pub fn save_active_profile(&self, name: &str) -> Result<(), PersistenceError> {
206 let path = self.active_profile_path();
207 fs::write(&path, name).map_err(|e| {
208 PersistenceError::IoError(std::io::Error::new(
209 e.kind(),
210 format!("Failed to write active profile: {}", e),
211 ))
212 })?;
213 Ok(())
214 }
215
216 pub fn load_active_profile(&self) -> Result<Option<String>, PersistenceError> {
218 let path = self.active_profile_path();
219
220 if !path.exists() {
221 return Ok(None);
222 }
223
224 let content = fs::read_to_string(&path).map_err(|e| {
225 PersistenceError::IoError(std::io::Error::new(
226 e.kind(),
227 format!("Failed to read active profile: {}", e),
228 ))
229 })?;
230
231 Ok(Some(content.trim().to_string()))
232 }
233
234 pub fn with_default_location() -> Result<Self, PersistenceError> {
239 let possible_paths = vec![
241 PathBuf::from("projects/ricecoder/config/keybinds"),
242 PathBuf::from("config/keybinds"),
243 PathBuf::from("../../config/keybinds"),
244 PathBuf::from("../../../config/keybinds"),
245 PathBuf::from("../../../../config/keybinds"),
246 ];
247
248 for path in possible_paths {
249 if let Ok(persistence) = FileSystemPersistence::new(&path) {
250 return Ok(persistence);
251 }
252 }
253
254 let default_path = PathBuf::from("projects/ricecoder/config/keybinds");
256 FileSystemPersistence::new(&default_path)
257 }
258
259 pub fn config_dir(&self) -> &Path {
261 &self.config_dir
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268 use crate::models::Keybind;
269 use crate::profile::Profile;
270
271 #[test]
272 fn test_save_and_load_profile() {
273 let temp_dir = tempfile::tempdir().unwrap();
274 let persistence = FileSystemPersistence::new(temp_dir.path()).unwrap();
275
276 let keybinds = vec![Keybind::new("editor.save", "Ctrl+S", "editing", "Save")];
277 let profile = Profile::new("default", keybinds);
278
279 assert!(persistence.save_profile(&profile).is_ok());
280
281 let loaded = persistence.load_profile("default").unwrap();
282 assert_eq!(loaded.name, "default");
283 assert_eq!(loaded.keybinds.len(), 1);
284 }
285
286 #[test]
287 fn test_delete_profile() {
288 let temp_dir = tempfile::tempdir().unwrap();
289 let persistence = FileSystemPersistence::new(temp_dir.path()).unwrap();
290
291 let keybinds = vec![Keybind::new("editor.save", "Ctrl+S", "editing", "Save")];
292 let profile = Profile::new("default", keybinds);
293
294 persistence.save_profile(&profile).unwrap();
295 assert!(persistence.delete_profile("default").is_ok());
296 assert!(persistence.load_profile("default").is_err());
297 }
298
299 #[test]
300 fn test_list_profiles() {
301 let temp_dir = tempfile::tempdir().unwrap();
302 let persistence = FileSystemPersistence::new(temp_dir.path()).unwrap();
303
304 let keybinds = vec![Keybind::new("editor.save", "Ctrl+S", "editing", "Save")];
305
306 let profile1 = Profile::new("default", keybinds.clone());
307 let profile2 = Profile::new("vim", keybinds);
308
309 persistence.save_profile(&profile1).unwrap();
310 persistence.save_profile(&profile2).unwrap();
311
312 let profiles = persistence.list_profiles().unwrap();
313 assert_eq!(profiles.len(), 2);
314 assert!(profiles.contains(&"default".to_string()));
315 assert!(profiles.contains(&"vim".to_string()));
316 }
317
318 #[test]
319 fn test_save_active_profile() {
320 let temp_dir = tempfile::tempdir().unwrap();
321 let persistence = FileSystemPersistence::new(temp_dir.path()).unwrap();
322
323 assert!(persistence.save_active_profile("default").is_ok());
324
325 let loaded = persistence.load_active_profile().unwrap();
326 assert_eq!(loaded, Some("default".to_string()));
327 }
328
329 #[test]
330 fn test_with_default_location() {
331 let result = FileSystemPersistence::with_default_location();
333 assert!(result.is_ok());
334
335 let persistence = result.unwrap();
336
337 assert!(persistence.config_dir().exists());
339 }
340
341 #[test]
342 fn test_with_default_location_creates_directory() {
343 let persistence = FileSystemPersistence::with_default_location().unwrap();
345
346 let keybinds = vec![Keybind::new("editor.save", "Ctrl+S", "editing", "Save")];
348 let profile = Profile::new("test_profile", keybinds);
349
350 assert!(persistence.save_profile(&profile).is_ok());
351 assert!(persistence.load_profile("test_profile").is_ok());
352
353 let _ = persistence.delete_profile("test_profile");
355 }
356
357 #[test]
358 fn test_config_dir_path() {
359 let temp_dir = tempfile::tempdir().unwrap();
360 let persistence = FileSystemPersistence::new(temp_dir.path()).unwrap();
361
362 assert_eq!(persistence.config_dir(), temp_dir.path());
363 }
364}