use std::path::{Path, PathBuf};
#[allow(dead_code)] pub fn config_file() -> Option<PathBuf> {
if let Ok(override_path) = std::env::var("KIZU_CONFIG") {
return Some(PathBuf::from(override_path));
}
std::env::var("XDG_CONFIG_HOME")
.ok()
.map(PathBuf::from)
.or_else(|| dirs::home_dir().map(|h| h.join(".config")))
.map(|d| d.join("kizu").join("config.toml"))
}
pub fn state_dir() -> Option<PathBuf> {
if let Ok(override_dir) = std::env::var("KIZU_STATE_DIR") {
return Some(PathBuf::from(override_dir));
}
#[cfg(target_os = "macos")]
{
dirs::home_dir().map(|h| h.join("Library/Application Support/kizu"))
}
#[cfg(not(target_os = "macos"))]
{
std::env::var("XDG_STATE_HOME")
.ok()
.map(PathBuf::from)
.or_else(|| dirs::home_dir().map(|h| h.join(".local/state")))
.map(|d| d.join("kizu"))
}
}
pub fn project_hash(root: &Path) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
root.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
pub fn session_file(root: &Path) -> Option<PathBuf> {
state_dir().map(|d| {
d.join("sessions")
.join(format!("{}.json", project_hash(root)))
})
}
pub fn events_dir(root: &Path) -> Option<PathBuf> {
state_dir().map(|d| d.join("events").join(project_hash(root)))
}
#[cfg(unix)]
pub fn ensure_private_dir(path: &Path) -> anyhow::Result<()> {
use std::os::unix::fs::PermissionsExt;
std::fs::create_dir_all(path)
.with_context(|| format!("creating directory {}", path.display()))?;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
.with_context(|| format!("setting permissions on {}", path.display()))?;
Ok(())
}
#[cfg(not(unix))]
pub fn ensure_private_dir(path: &Path) -> anyhow::Result<()> {
std::fs::create_dir_all(path)
.with_context(|| format!("creating directory {}", path.display()))?;
Ok(())
}
use anyhow::Context;
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn project_hash_is_deterministic() {
let a = project_hash(Path::new("/home/user/project"));
let b = project_hash(Path::new("/home/user/project"));
assert_eq!(a, b);
}
#[test]
fn project_hash_differs_for_different_roots() {
let a = project_hash(Path::new("/home/user/project-a"));
let b = project_hash(Path::new("/home/user/project-b"));
assert_ne!(a, b);
}
#[test]
fn session_file_with_override() {
unsafe { std::env::set_var("KIZU_STATE_DIR", "/tmp/kizu-test-state") };
let path = session_file(Path::new("/project")).unwrap();
unsafe { std::env::remove_var("KIZU_STATE_DIR") };
assert!(path.starts_with("/tmp/kizu-test-state/sessions/"));
assert!(path.to_str().unwrap().ends_with(".json"));
}
#[test]
fn events_dir_is_per_project() {
unsafe { std::env::set_var("KIZU_STATE_DIR", "/tmp/kizu-test-state") };
let path_a = events_dir(Path::new("/project-a")).unwrap();
let path_b = events_dir(Path::new("/project-b")).unwrap();
unsafe { std::env::remove_var("KIZU_STATE_DIR") };
assert_ne!(path_a, path_b);
assert!(path_a.starts_with("/tmp/kizu-test-state/events/"));
assert!(path_b.starts_with("/tmp/kizu-test-state/events/"));
}
#[test]
fn config_file_with_override() {
unsafe { std::env::set_var("KIZU_CONFIG", "/tmp/kizu-test.toml") };
let path = config_file().unwrap();
unsafe { std::env::remove_var("KIZU_CONFIG") };
assert_eq!(path, PathBuf::from("/tmp/kizu-test.toml"));
}
#[test]
fn ensure_private_dir_creates_with_correct_permissions() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("private_test");
ensure_private_dir(&dir).unwrap();
assert!(dir.is_dir());
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&dir).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o700);
}
}
#[test]
fn ensure_private_dir_is_idempotent() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path().join("idem_test");
ensure_private_dir(&dir).unwrap();
ensure_private_dir(&dir).unwrap(); assert!(dir.is_dir());
}
}