use std::path::{Path, PathBuf};
pub fn get_config_dir() -> PathBuf {
std::env::var("XDG_CONFIG_HOME")
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| {
dirs::home_dir()
.unwrap_or_else(|| {
tracing::warn!("Home directory not found, using current directory for config");
PathBuf::from(".")
})
.join(".config")
})
.join("snp")
}
pub fn ensure_config_dir() -> std::io::Result<PathBuf> {
let dir = get_config_dir();
if dir.exists() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = std::fs::metadata(&dir) {
let mode = metadata.permissions().mode();
if mode & 0o077 != 0 {
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700))?;
}
}
}
} else {
std::fs::create_dir_all(&dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700))?;
}
}
Ok(dir)
}
pub fn get_legacy_macos_config_dir() -> Option<PathBuf> {
if !cfg!(target_os = "macos") {
return None;
}
let new_dir = get_config_dir();
if new_dir.exists() {
return None;
}
let legacy_dir = dirs::config_dir()?.join("snp");
if legacy_dir.exists() && legacy_dir != new_dir {
Some(legacy_dir)
} else {
None
}
}
fn copy_recursively(src: &Path, dst: &Path) -> std::io::Result<()> {
if src.is_dir() {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
copy_recursively(&entry.path(), &dst.join(entry.file_name()))?;
}
} else {
std::fs::copy(src, dst)?;
}
Ok(())
}
pub fn migrate_macos_config_dir() -> std::io::Result<()> {
let legacy_dir = match get_legacy_macos_config_dir() {
Some(d) => d,
None => return Ok(()),
};
let new_dir = get_config_dir();
tracing::info!(
from = %legacy_dir.display(),
to = %new_dir.display(),
"Migrating config directory"
);
std::fs::create_dir_all(&new_dir)?;
for entry in std::fs::read_dir(&legacy_dir)? {
let entry = entry?;
let src = entry.path();
let dst = new_dir.join(entry.file_name());
if std::fs::rename(&src, &dst).is_ok() {
continue;
}
copy_recursively(&src, &dst)?;
if src.is_dir() {
let _ = std::fs::remove_dir_all(&src);
} else {
let _ = std::fs::remove_file(&src);
}
}
if std::fs::read_dir(&legacy_dir)?.next().is_none() {
let _ = std::fs::remove_dir(&legacy_dir);
}
tracing::info!("Config migration complete");
Ok(())
}
pub fn get_config_path(filename: &str) -> PathBuf {
get_config_dir().join(filename)
}
pub fn get_snippets_path() -> PathBuf {
get_config_path("snippets.toml")
}
pub fn get_sync_config_path() -> PathBuf {
get_config_path("sync.toml")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_config_dir_contains_snp() {
let dir = get_config_dir();
assert!(dir.to_string_lossy().ends_with("snp"));
}
#[test]
fn test_get_config_path_adds_filename() {
let path = get_config_path("test.toml");
assert!(path.to_string_lossy().ends_with("test.toml"));
}
#[test]
fn test_get_snippets_path_ends_with_snippets_toml() {
let path = get_snippets_path();
assert!(path.to_string_lossy().ends_with("snippets.toml"));
}
#[test]
fn test_get_sync_config_path_ends_with_sync_toml() {
let path = get_sync_config_path();
assert!(path.to_string_lossy().ends_with("sync.toml"));
}
#[test]
fn test_get_config_dir_is_deterministic() {
let a = get_config_dir();
let b = get_config_dir();
assert_eq!(a, b);
}
#[test]
fn test_ensure_config_dir_is_idempotent() {
let a = ensure_config_dir().expect("first ensure_config_dir");
let b = ensure_config_dir().expect("second ensure_config_dir");
assert_eq!(a, b);
assert!(
a.exists(),
"config dir should exist after ensure_config_dir"
);
}
}