use std::path::{Path, PathBuf};
use etcetera::BaseStrategy;
const APP: &str = "gravityfile";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Dirs {
pub config: PathBuf,
pub cache: PathBuf,
pub data: PathBuf,
pub state: PathBuf,
}
impl Dirs {
pub fn discover() -> Option<Self> {
if let Some(home) = home_override() {
return Some(Self::under(&home));
}
let base = etcetera::choose_base_strategy().ok()?;
let config = base.config_dir().join(APP);
Some(Self {
config: match legacy_config() {
Some(legacy) if !config.exists() && legacy.is_dir() => legacy,
_ => config,
},
cache: base.cache_dir().join(APP),
data: base.data_dir().join(APP),
state: base
.state_dir()
.unwrap_or_else(|| base.data_dir())
.join(APP),
})
}
pub fn under(home: &Path) -> Self {
Self {
config: home.join("config"),
cache: home.join("cache"),
data: home.join("data"),
state: home.join("state"),
}
}
pub fn settings_file(&self) -> PathBuf {
self.config.join("settings.toml")
}
pub fn keymap_file(&self) -> PathBuf {
self.config.join("keymap.toml")
}
pub fn history_file(&self) -> PathBuf {
self.data.join("history.jsonl")
}
pub fn plugin_dir(&self) -> PathBuf {
self.config.join("plugins")
}
}
fn home_override() -> Option<PathBuf> {
std::env::var_os("GRAVITYFILE_HOME")
.filter(|home| !home.is_empty())
.map(PathBuf::from)
}
fn legacy_config() -> Option<PathBuf> {
if !cfg!(target_os = "macos") || home_override().is_some() {
return None;
}
Some(home()?.join("Library/Application Support").join(APP))
}
pub fn migrate_legacy_config() -> std::io::Result<Option<(PathBuf, PathBuf)>> {
let (Some(legacy), Some(base)) = (legacy_config(), etcetera::choose_base_strategy().ok())
else {
return Ok(None);
};
let current = base.config_dir().join(APP);
Ok(move_legacy(&legacy, ¤t)?.then_some((legacy, current)))
}
fn move_legacy(legacy: &Path, current: &Path) -> std::io::Result<bool> {
if current.exists() || !legacy.is_dir() {
return Ok(false);
}
if let Some(parent) = current.parent() {
std::fs::create_dir_all(parent)?;
}
match std::fs::rename(legacy, current) {
Ok(()) => Ok(true),
Err(_) if current.exists() => Ok(false),
Err(error) => Err(error),
}
}
pub fn home() -> Option<PathBuf> {
etcetera::home_dir().ok()
}
pub fn config_root() -> Option<PathBuf> {
etcetera::choose_base_strategy()
.ok()
.map(|base| base.config_dir())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_home_holds_every_directory() {
let dirs = Dirs::under(Path::new("/portable"));
assert_eq!(
dirs.settings_file(),
Path::new("/portable/config/settings.toml")
);
assert_eq!(dirs.plugin_dir(), Path::new("/portable/config/plugins"));
assert_eq!(dirs.cache, Path::new("/portable/cache"));
}
#[test]
fn a_legacy_directory_is_moved_once_and_never_over_a_current_one() {
let temp = tempfile::tempdir().unwrap();
let legacy = temp.path().join("Library/Application Support/gravityfile");
let current = temp.path().join(".config/gravityfile");
std::fs::create_dir_all(legacy.join("plugins")).unwrap();
std::fs::write(legacy.join("settings.toml"), "theme = 'light'").unwrap();
assert!(move_legacy(&legacy, ¤t).unwrap());
assert!(!legacy.exists());
assert_eq!(
std::fs::read_to_string(current.join("settings.toml")).unwrap(),
"theme = 'light'"
);
assert!(current.join("plugins").is_dir());
std::fs::create_dir_all(&legacy).unwrap();
assert!(!move_legacy(&legacy, ¤t).unwrap());
assert!(legacy.exists());
let fresh = temp.path().join("fresh/gravityfile");
assert!(!move_legacy(&temp.path().join("missing"), &fresh).unwrap());
assert!(!fresh.exists());
}
}