Skip to main content

cove_cli/
paths.rs

1// ── XDG-compliant paths for cove ──
2//
3// Cove's data (event logs, context log) is operational state — safe to delete,
4// regenerated on next session. Per XDG Base Directory spec, this goes in
5// XDG_STATE_HOME/cove (~/.local/state/cove by default).
6//
7// On first run, migrates from the legacy ~/.cove/ directory if it exists.
8
9use std::env;
10use std::fs;
11use std::path::PathBuf;
12
13/// Legacy data directory (~/.cove/).
14fn legacy_dir() -> PathBuf {
15    let home = env::var("HOME").unwrap_or_default();
16    PathBuf::from(home).join(".cove")
17}
18
19/// XDG_STATE_HOME/cove — event logs, context log, session state.
20pub fn state_dir() -> PathBuf {
21    env::var("XDG_STATE_HOME")
22        .map(PathBuf::from)
23        .unwrap_or_else(|_| {
24            let home = env::var("HOME").unwrap_or_default();
25            PathBuf::from(home).join(".local").join("state")
26        })
27        .join("cove")
28}
29
30/// XDG_STATE_HOME/cove/events/
31pub fn events_dir() -> PathBuf {
32    state_dir().join("events")
33}
34
35/// Migrate from ~/.cove/ to XDG_STATE_HOME/cove/ if needed.
36///
37/// Runs once — if the new path already exists, this is a no-op.
38/// Moves the directory (rename is atomic on the same filesystem).
39/// If rename fails (cross-device), falls back to keeping legacy path.
40pub fn migrate_legacy() {
41    let legacy = legacy_dir();
42    let xdg = state_dir();
43
44    // Nothing to migrate
45    if !legacy.is_dir() {
46        return;
47    }
48
49    // Already migrated (or user set XDG_STATE_HOME to something with existing data)
50    if xdg.is_dir() {
51        return;
52    }
53
54    // Ensure parent exists (~/.local/state/)
55    if let Some(parent) = xdg.parent() {
56        let _ = fs::create_dir_all(parent);
57    }
58
59    // Atomic rename (same filesystem)
60    match fs::rename(&legacy, &xdg) {
61        Ok(()) => {
62            eprintln!("cove: migrated {} → {}", legacy.display(), xdg.display());
63            // Leave a symlink at the old path for anything that might reference it
64            #[cfg(unix)]
65            {
66                let _ = std::os::unix::fs::symlink(&xdg, &legacy);
67            }
68        }
69        Err(e) => {
70            // Cross-device or permission error — keep using legacy path
71            eprintln!(
72                "cove: could not migrate {} → {}: {e}",
73                legacy.display(),
74                xdg.display()
75            );
76            eprintln!("cove: continuing with {}", legacy.display());
77        }
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use std::sync::Mutex;
85
86    static ENV_LOCK: Mutex<()> = Mutex::new(());
87
88    #[test]
89    fn state_dir_respects_xdg_env() {
90        let _lock = ENV_LOCK.lock().unwrap();
91        unsafe { env::set_var("XDG_STATE_HOME", "/tmp/test-xdg-state") };
92        let dir = state_dir();
93        unsafe { env::remove_var("XDG_STATE_HOME") };
94        assert_eq!(dir, PathBuf::from("/tmp/test-xdg-state/cove"));
95    }
96
97    #[test]
98    fn events_dir_is_under_state() {
99        let _lock = ENV_LOCK.lock().unwrap();
100        unsafe { env::set_var("XDG_STATE_HOME", "/tmp/test-xdg-state") };
101        let dir = events_dir();
102        unsafe { env::remove_var("XDG_STATE_HOME") };
103        assert_eq!(dir, PathBuf::from("/tmp/test-xdg-state/cove/events"));
104    }
105
106    #[test]
107    fn migrate_noop_when_no_legacy() {
108        let _lock = ENV_LOCK.lock().unwrap();
109        let orig_home = env::var("HOME").ok();
110        unsafe { env::set_var("HOME", "/tmp/nonexistent-home-for-cove-test") };
111        unsafe { env::set_var("XDG_STATE_HOME", "/tmp/nonexistent-xdg-for-cove-test") };
112        migrate_legacy();
113        unsafe { env::remove_var("XDG_STATE_HOME") };
114        if let Some(h) = orig_home {
115            unsafe { env::set_var("HOME", h) };
116        }
117    }
118}