slowshell_core/
persistence.rs1use serde::{Serialize, de::DeserializeOwned};
2use std::{
3 env, fs,
4 path::{Path, PathBuf},
5};
6
7pub fn persistence_location() -> PathBuf {
8 env::var_os("XDG_STATE_HOME")
9 .map(PathBuf::from)
10 .unwrap_or_else(|| PathBuf::from(env::var_os("HOME").expect("HOME not set")).join(".local"))
11 .join("slowshell")
12}
13
14fn file_for(name: &str) -> Option<PathBuf> {
15 if name.is_empty() || name.contains('/') || name.contains('\\') || name == "." || name == ".." {
16 return None;
17 }
18 Some(persistence_location().join(format!("{name}.ron")))
19}
20
21pub fn load<T: DeserializeOwned>(name: impl AsRef<str>) -> Option<T> {
22 let name = name.as_ref();
23 let path = file_for(name)?;
24 let content = fs::read_to_string(path).ok()?;
25 match ron::from_str(&content) {
26 Ok(value) => Some(value),
27 Err(e) => {
28 eprintln!("[persistence] failed to load {name:?}: {e}");
29 None
30 }
31 }
32}
33
34use miette::IntoDiagnostic;
35
36pub fn save<T: Serialize>(name: impl AsRef<str>, value: &T) -> miette::Result<()> {
37 let name = name.as_ref();
38 let path =
39 file_for(name).ok_or_else(|| miette::miette!("[persistence] invalid name {name:?}"))?;
40 let dir = path
41 .parent()
42 .ok_or_else(|| miette::miette!("invalid path"))?;
43 fs::create_dir_all(dir).into_diagnostic()?;
44
45 let content = ron::ser::to_string_pretty(value, ron::ser::PrettyConfig::default())
46 .map_err(|e| miette::miette!("[persistence] failed to serialize {name:?}: {e}"))?;
47
48 atomic_write(&path, content.as_bytes())?;
49 Ok(())
50}
51
52pub fn has(name: impl AsRef<str>) -> bool {
53 file_for(name.as_ref()).is_some_and(|p| p.exists())
54}
55
56pub fn remove(name: impl AsRef<str>) -> miette::Result<()> {
57 let name = name.as_ref();
58 let path =
59 file_for(name).ok_or_else(|| miette::miette!("[persistence] invalid name {name:?}"))?;
60 if path.exists() {
61 fs::remove_file(&path).into_diagnostic()?;
62 }
63 Ok(())
64}
65
66fn atomic_write(path: &Path, content: &[u8]) -> miette::Result<()> {
67 let mut tmp = path.as_os_str().to_owned();
68 tmp.push(format!(".{}.tmp", std::process::id()));
69 let tmp = PathBuf::from(tmp);
70
71 let result = fs::write(&tmp, content)
72 .into_diagnostic()
73 .and_then(|()| fs::rename(&tmp, path).into_diagnostic());
74
75 if result.is_err() {
76 let _ = fs::remove_file(&tmp);
77 }
78
79 result
80}