Skip to main content

kaish_kernel/
paths.rs

1//! XDG Base Directory paths for kaish and embedders.
2//!
3//! This module provides two layers of path helpers:
4//!
5//! 1. **XDG primitives** — Generic XDG base directories that embedders can use
6//!    to compose their own application-specific paths.
7//!
8//! 2. **kaish-specific paths** — Convenience functions for kaish's own paths,
9//!    built on top of the primitives.
10//!
11//! # XDG Base Directory Specification
12//!
13//! | Purpose | XDG Variable | Default |
14//! |---------|--------------|---------|
15//! | Runtime | `$XDG_RUNTIME_DIR` | `/run/user/$UID` or `/tmp` |
16//! | Data | `$XDG_DATA_HOME` | `~/.local/share` |
17//! | Config | `$XDG_CONFIG_HOME` | `~/.config` |
18//! | Cache | `$XDG_CACHE_HOME` | `~/.cache` |
19//!
20//! # Example: Embedder Path Composition
21//!
22//! ```
23//! use kaish_kernel::paths::{xdg_data_home, home_dir};
24//! use std::path::PathBuf;
25//!
26//! // Embedders compose their own paths on top of XDG primitives
27//! fn my_app_data_dir() -> PathBuf {
28//!     xdg_data_home().join("myapp")
29//! }
30//!
31//! fn my_app_worktrees_dir() -> PathBuf {
32//!     my_app_data_dir().join("worktrees")
33//! }
34//! ```
35
36use std::path::PathBuf;
37
38use directories::BaseDirs;
39
40// ═══════════════════════════════════════════════════════════════════════════
41// XDG Primitives — For embedders to compose their own paths
42// ═══════════════════════════════════════════════════════════════════════════
43
44/// Get the user's home directory.
45///
46/// Returns `$HOME` or falls back to `/tmp` if not set.
47///
48/// # Example
49///
50/// ```
51/// use kaish_kernel::paths::home_dir;
52///
53/// let home = home_dir();
54/// assert!(home.is_absolute());
55/// ```
56pub fn home_dir() -> PathBuf {
57    std::env::var("HOME")
58        .map(PathBuf::from)
59        .unwrap_or_else(|_| PathBuf::from("/tmp"))
60}
61
62/// Get XDG data home directory.
63///
64/// Returns `$XDG_DATA_HOME` or falls back to `~/.local/share`.
65///
66/// Embedders use this to compose their own data paths:
67/// ```
68/// use kaish_kernel::paths::xdg_data_home;
69///
70/// let myapp_data = xdg_data_home().join("myapp");
71/// ```
72pub fn xdg_data_home() -> PathBuf {
73    BaseDirs::new()
74        .map(|d| d.data_dir().to_path_buf())
75        .unwrap_or_else(|| home_dir().join(".local").join("share"))
76}
77
78/// Get XDG config home directory.
79///
80/// Returns `$XDG_CONFIG_HOME` or falls back to `~/.config`.
81///
82/// Embedders use this to compose their own config paths:
83/// ```
84/// use kaish_kernel::paths::xdg_config_home;
85///
86/// let myapp_config = xdg_config_home().join("myapp");
87/// ```
88pub fn xdg_config_home() -> PathBuf {
89    BaseDirs::new()
90        .map(|d| d.config_dir().to_path_buf())
91        .unwrap_or_else(|| home_dir().join(".config"))
92}
93
94/// Get XDG cache home directory.
95///
96/// Returns `$XDG_CACHE_HOME` or falls back to `~/.cache`.
97///
98/// Embedders use this to compose their own cache paths:
99/// ```
100/// use kaish_kernel::paths::xdg_cache_home;
101///
102/// let myapp_cache = xdg_cache_home().join("myapp");
103/// ```
104pub fn xdg_cache_home() -> PathBuf {
105    BaseDirs::new()
106        .map(|d| d.cache_dir().to_path_buf())
107        .unwrap_or_else(|| home_dir().join(".cache"))
108}
109
110/// Get XDG runtime directory.
111///
112/// Returns `$XDG_RUNTIME_DIR` or falls back to system temp directory.
113///
114/// Embedders use this to compose their own runtime paths:
115/// ```
116/// use kaish_kernel::paths::xdg_runtime_dir;
117///
118/// let myapp_sockets = xdg_runtime_dir().join("myapp");
119/// ```
120pub fn xdg_runtime_dir() -> PathBuf {
121    std::env::var("XDG_RUNTIME_DIR")
122        .map(PathBuf::from)
123        .unwrap_or_else(|_| std::env::temp_dir())
124}
125
126// ═══════════════════════════════════════════════════════════════════════════
127// kaish-Specific Paths — Built on XDG primitives
128// ═══════════════════════════════════════════════════════════════════════════
129
130/// Get the kaish runtime directory for sockets.
131///
132/// Uses `$XDG_RUNTIME_DIR/kaish` or falls back to `/tmp/kaish`.
133pub fn runtime_dir() -> PathBuf {
134    xdg_runtime_dir().join("kaish")
135}
136
137/// Get the kaish data directory for persistent state.
138///
139/// Uses `$XDG_DATA_HOME/kaish` or falls back to `~/.local/share/kaish`.
140pub fn data_dir() -> PathBuf {
141    xdg_data_home().join("kaish")
142}
143
144/// Get the kaish config directory.
145///
146/// Uses `$XDG_CONFIG_HOME/kaish` or falls back to `~/.config/kaish`.
147pub fn config_dir() -> PathBuf {
148    xdg_config_home().join("kaish")
149}
150
151/// Get the kaish cache directory.
152///
153/// Uses `$XDG_CACHE_HOME/kaish` or falls back to `~/.cache/kaish`.
154pub fn cache_dir() -> PathBuf {
155    xdg_cache_home().join("kaish")
156}
157
158/// Get the kernels directory.
159pub fn kernels_dir() -> PathBuf {
160    data_dir().join("kernels")
161}
162
163/// Get the spill directory for output truncation.
164///
165/// Uses `$XDG_RUNTIME_DIR/kaish/spill` (RAM-backed tmpfs on systemd systems).
166/// Cleared on reboot, user-scoped, survives across MCP calls.
167pub fn spill_dir() -> PathBuf {
168    runtime_dir().join("spill")
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    // ═══════════════════════════════════════════════════════════════════════
176    // XDG Primitive Tests
177    // ═══════════════════════════════════════════════════════════════════════
178
179    #[test]
180    fn home_dir_is_absolute() {
181        let home = home_dir();
182        assert!(home.is_absolute());
183    }
184
185    #[test]
186    fn xdg_data_home_defaults_to_local_share() {
187        // When $XDG_DATA_HOME is not set, should be under home
188        let data = xdg_data_home();
189        assert!(data.is_absolute());
190        // Should end with .local/share or be the XDG override
191        let path_str = data.to_string_lossy();
192        assert!(
193            path_str.ends_with(".local/share") || std::env::var("XDG_DATA_HOME").is_ok(),
194            "Expected .local/share or XDG override, got: {}",
195            path_str
196        );
197    }
198
199    #[test]
200    fn xdg_config_home_defaults_to_config() {
201        let config = xdg_config_home();
202        assert!(config.is_absolute());
203        let path_str = config.to_string_lossy();
204        assert!(
205            path_str.ends_with(".config") || std::env::var("XDG_CONFIG_HOME").is_ok(),
206            "Expected .config or XDG override, got: {}",
207            path_str
208        );
209    }
210
211    #[test]
212    fn xdg_cache_home_defaults_to_cache() {
213        let cache = xdg_cache_home();
214        assert!(cache.is_absolute());
215        let path_str = cache.to_string_lossy();
216        assert!(
217            path_str.ends_with(".cache") || std::env::var("XDG_CACHE_HOME").is_ok(),
218            "Expected .cache or XDG override, got: {}",
219            path_str
220        );
221    }
222
223    #[test]
224    fn xdg_runtime_dir_is_absolute() {
225        let runtime = xdg_runtime_dir();
226        assert!(runtime.is_absolute());
227    }
228
229    // ═══════════════════════════════════════════════════════════════════════
230    // kaish-Specific Path Tests
231    // ═══════════════════════════════════════════════════════════════════════
232
233    #[test]
234    fn kaish_paths_are_under_kaish() {
235        assert!(runtime_dir().ends_with("kaish"));
236        assert!(data_dir().ends_with("kaish"));
237        assert!(config_dir().ends_with("kaish"));
238        assert!(cache_dir().ends_with("kaish"));
239    }
240
241    #[test]
242    fn kaish_paths_build_on_xdg_primitives() {
243        // kaish paths should be XDG base + "kaish"
244        assert_eq!(data_dir(), xdg_data_home().join("kaish"));
245        assert_eq!(config_dir(), xdg_config_home().join("kaish"));
246        assert_eq!(cache_dir(), xdg_cache_home().join("kaish"));
247        assert_eq!(runtime_dir(), xdg_runtime_dir().join("kaish"));
248    }
249
250    #[test]
251    fn spill_dir_is_under_runtime() {
252        let spill = spill_dir();
253        assert!(spill.starts_with(&runtime_dir()));
254        assert!(spill.ends_with("spill"));
255    }
256
257    #[test]
258    fn kernels_dir_is_under_data() {
259        let kernels = kernels_dir();
260        let data = data_dir();
261        assert!(kernels.starts_with(&data));
262        assert!(kernels.ends_with("kernels"));
263    }
264}