gravityfile_core/
paths.rs1use std::path::{Path, PathBuf};
9
10use etcetera::BaseStrategy;
11
12const APP: &str = "gravityfile";
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct Dirs {
17 pub config: PathBuf,
19 pub cache: PathBuf,
21 pub data: PathBuf,
23 pub state: PathBuf,
25}
26
27impl Dirs {
28 pub fn discover() -> Option<Self> {
31 if let Some(home) = home_override() {
32 return Some(Self::under(&home));
33 }
34 let base = etcetera::choose_base_strategy().ok()?;
35 let config = base.config_dir().join(APP);
36 Some(Self {
37 config: match legacy_config() {
38 Some(legacy) if !config.exists() && legacy.is_dir() => legacy,
39 _ => config,
40 },
41 cache: base.cache_dir().join(APP),
42 data: base.data_dir().join(APP),
43 state: base
44 .state_dir()
45 .unwrap_or_else(|| base.data_dir())
46 .join(APP),
47 })
48 }
49
50 pub fn under(home: &Path) -> Self {
52 Self {
53 config: home.join("config"),
54 cache: home.join("cache"),
55 data: home.join("data"),
56 state: home.join("state"),
57 }
58 }
59
60 pub fn settings_file(&self) -> PathBuf {
62 self.config.join("settings.toml")
63 }
64
65 pub fn keymap_file(&self) -> PathBuf {
67 self.config.join("keymap.toml")
68 }
69
70 pub fn history_file(&self) -> PathBuf {
72 self.data.join("history.jsonl")
73 }
74
75 pub fn plugin_dir(&self) -> PathBuf {
77 self.config.join("plugins")
78 }
79}
80
81fn home_override() -> Option<PathBuf> {
82 std::env::var_os("GRAVITYFILE_HOME")
83 .filter(|home| !home.is_empty())
84 .map(PathBuf::from)
85}
86
87fn legacy_config() -> Option<PathBuf> {
89 if !cfg!(target_os = "macos") || home_override().is_some() {
90 return None;
91 }
92 Some(home()?.join("Library/Application Support").join(APP))
93}
94
95pub fn migrate_legacy_config() -> std::io::Result<Option<(PathBuf, PathBuf)>> {
100 let (Some(legacy), Some(base)) = (legacy_config(), etcetera::choose_base_strategy().ok())
101 else {
102 return Ok(None);
103 };
104 let current = base.config_dir().join(APP);
105 Ok(move_legacy(&legacy, ¤t)?.then_some((legacy, current)))
106}
107
108fn move_legacy(legacy: &Path, current: &Path) -> std::io::Result<bool> {
110 if current.exists() || !legacy.is_dir() {
111 return Ok(false);
112 }
113 if let Some(parent) = current.parent() {
114 std::fs::create_dir_all(parent)?;
115 }
116 match std::fs::rename(legacy, current) {
117 Ok(()) => Ok(true),
118 Err(_) if current.exists() => Ok(false),
120 Err(error) => Err(error),
121 }
122}
123
124pub fn home() -> Option<PathBuf> {
126 etcetera::home_dir().ok()
127}
128
129pub fn config_root() -> Option<PathBuf> {
132 etcetera::choose_base_strategy()
133 .ok()
134 .map(|base| base.config_dir())
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn one_home_holds_every_directory() {
143 let dirs = Dirs::under(Path::new("/portable"));
144 assert_eq!(
145 dirs.settings_file(),
146 Path::new("/portable/config/settings.toml")
147 );
148 assert_eq!(dirs.plugin_dir(), Path::new("/portable/config/plugins"));
149 assert_eq!(dirs.cache, Path::new("/portable/cache"));
150 }
151
152 #[test]
153 fn a_legacy_directory_is_moved_once_and_never_over_a_current_one() {
154 let temp = tempfile::tempdir().unwrap();
155 let legacy = temp.path().join("Library/Application Support/gravityfile");
156 let current = temp.path().join(".config/gravityfile");
157 std::fs::create_dir_all(legacy.join("plugins")).unwrap();
158 std::fs::write(legacy.join("settings.toml"), "theme = 'light'").unwrap();
159
160 assert!(move_legacy(&legacy, ¤t).unwrap());
161 assert!(!legacy.exists());
162 assert_eq!(
163 std::fs::read_to_string(current.join("settings.toml")).unwrap(),
164 "theme = 'light'"
165 );
166 assert!(current.join("plugins").is_dir());
167
168 std::fs::create_dir_all(&legacy).unwrap();
170 assert!(!move_legacy(&legacy, ¤t).unwrap());
171 assert!(legacy.exists());
172
173 let fresh = temp.path().join("fresh/gravityfile");
175 assert!(!move_legacy(&temp.path().join("missing"), &fresh).unwrap());
176 assert!(!fresh.exists());
177 }
178}