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
38#[cfg(feature = "os-integration")]
39use directories::BaseDirs;
40
41// ═══════════════════════════════════════════════════════════════════════════
42// XDG Primitives — For embedders to compose their own paths
43// ═══════════════════════════════════════════════════════════════════════════
44
45/// Get the user's home directory.
46///
47/// Returns `$HOME` or falls back to `/tmp` if not set.
48///
49/// # Example
50///
51/// ```
52/// use kaish_kernel::paths::home_dir;
53///
54/// let home = home_dir();
55/// assert!(home.is_absolute());
56/// ```
57pub fn home_dir() -> PathBuf {
58    std::env::var("HOME")
59        .map(PathBuf::from)
60        .unwrap_or_else(|_| PathBuf::from("/tmp"))
61}
62
63/// Get XDG data home directory.
64///
65/// Returns `$XDG_DATA_HOME` or falls back to `~/.local/share`.
66///
67/// Embedders use this to compose their own data paths:
68/// ```
69/// use kaish_kernel::paths::xdg_data_home;
70///
71/// let myapp_data = xdg_data_home().join("myapp");
72/// ```
73pub fn xdg_data_home() -> PathBuf {
74    std::env::var("XDG_DATA_HOME")
75        .map(PathBuf::from)
76        .ok()
77        .or({
78            #[cfg(feature = "os-integration")]
79            { BaseDirs::new().map(|d| d.data_dir().to_path_buf()) }
80            #[cfg(not(feature = "os-integration"))]
81            { None }
82        })
83        .unwrap_or_else(|| home_dir().join(".local").join("share"))
84}
85
86/// Get XDG config home directory.
87///
88/// Returns `$XDG_CONFIG_HOME` or falls back to `~/.config`.
89///
90/// Embedders use this to compose their own config paths:
91/// ```
92/// use kaish_kernel::paths::xdg_config_home;
93///
94/// let myapp_config = xdg_config_home().join("myapp");
95/// ```
96pub fn xdg_config_home() -> PathBuf {
97    std::env::var("XDG_CONFIG_HOME")
98        .map(PathBuf::from)
99        .ok()
100        .or({
101            #[cfg(feature = "os-integration")]
102            { BaseDirs::new().map(|d| d.config_dir().to_path_buf()) }
103            #[cfg(not(feature = "os-integration"))]
104            { None }
105        })
106        .unwrap_or_else(|| home_dir().join(".config"))
107}
108
109/// Get XDG cache home directory.
110///
111/// Returns `$XDG_CACHE_HOME` or falls back to `~/.cache`.
112///
113/// Embedders use this to compose their own cache paths:
114/// ```
115/// use kaish_kernel::paths::xdg_cache_home;
116///
117/// let myapp_cache = xdg_cache_home().join("myapp");
118/// ```
119pub fn xdg_cache_home() -> PathBuf {
120    std::env::var("XDG_CACHE_HOME")
121        .map(PathBuf::from)
122        .ok()
123        .or({
124            #[cfg(feature = "os-integration")]
125            { BaseDirs::new().map(|d| d.cache_dir().to_path_buf()) }
126            #[cfg(not(feature = "os-integration"))]
127            { None }
128        })
129        .unwrap_or_else(|| home_dir().join(".cache"))
130}
131
132/// Get XDG runtime directory.
133///
134/// Returns `$XDG_RUNTIME_DIR` or falls back to system temp directory.
135///
136/// Embedders use this to compose their own runtime paths:
137/// ```
138/// use kaish_kernel::paths::xdg_runtime_dir;
139///
140/// let myapp_sockets = xdg_runtime_dir().join("myapp");
141/// ```
142pub fn xdg_runtime_dir() -> PathBuf {
143    std::env::var("XDG_RUNTIME_DIR")
144        .map(PathBuf::from)
145        .unwrap_or_else(|_| std::env::temp_dir())
146}
147
148// ═══════════════════════════════════════════════════════════════════════════
149// kaish-Specific Paths — Built on XDG primitives
150// ═══════════════════════════════════════════════════════════════════════════
151
152/// Get the kaish runtime directory for sockets.
153///
154/// Uses `$XDG_RUNTIME_DIR/kaish` or falls back to `/tmp/kaish`.
155pub fn runtime_dir() -> PathBuf {
156    xdg_runtime_dir().join("kaish")
157}
158
159/// Get the kaish data directory for persistent state.
160///
161/// Uses `$XDG_DATA_HOME/kaish` or falls back to `~/.local/share/kaish`.
162pub fn data_dir() -> PathBuf {
163    xdg_data_home().join("kaish")
164}
165
166/// Get the kaish config directory.
167///
168/// Uses `$XDG_CONFIG_HOME/kaish` or falls back to `~/.config/kaish`.
169pub fn config_dir() -> PathBuf {
170    xdg_config_home().join("kaish")
171}
172
173/// Get the kaish cache directory.
174///
175/// Uses `$XDG_CACHE_HOME/kaish` or falls back to `~/.cache/kaish`.
176pub fn cache_dir() -> PathBuf {
177    xdg_cache_home().join("kaish")
178}
179
180/// Get the kernels directory.
181pub fn kernels_dir() -> PathBuf {
182    data_dir().join("kernels")
183}
184
185/// Get the spill directory for output truncation.
186///
187/// Uses `$XDG_RUNTIME_DIR/kaish/spill` (RAM-backed tmpfs on systemd systems).
188/// Cleared on reboot, user-scoped, survives across MCP calls.
189pub fn spill_dir() -> PathBuf {
190    runtime_dir().join("spill")
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    // ═══════════════════════════════════════════════════════════════════════
198    // XDG Primitive Tests
199    // ═══════════════════════════════════════════════════════════════════════
200
201    #[test]
202    fn home_dir_is_absolute() {
203        let home = home_dir();
204        assert!(home.is_absolute());
205    }
206
207    #[test]
208    fn xdg_data_home_defaults_to_local_share() {
209        // When $XDG_DATA_HOME is not set, should be under home
210        let data = xdg_data_home();
211        assert!(data.is_absolute());
212        // Should end with .local/share or be the XDG override
213        let path_str = data.to_string_lossy();
214        assert!(
215            path_str.ends_with(".local/share") || std::env::var("XDG_DATA_HOME").is_ok(),
216            "Expected .local/share or XDG override, got: {}",
217            path_str
218        );
219    }
220
221    #[test]
222    fn xdg_config_home_defaults_to_config() {
223        let config = xdg_config_home();
224        assert!(config.is_absolute());
225        let path_str = config.to_string_lossy();
226        assert!(
227            path_str.ends_with(".config") || std::env::var("XDG_CONFIG_HOME").is_ok(),
228            "Expected .config or XDG override, got: {}",
229            path_str
230        );
231    }
232
233    #[test]
234    fn xdg_cache_home_defaults_to_cache() {
235        let cache = xdg_cache_home();
236        assert!(cache.is_absolute());
237        let path_str = cache.to_string_lossy();
238        assert!(
239            path_str.ends_with(".cache") || std::env::var("XDG_CACHE_HOME").is_ok(),
240            "Expected .cache or XDG override, got: {}",
241            path_str
242        );
243    }
244
245    #[test]
246    fn xdg_runtime_dir_is_absolute() {
247        let runtime = xdg_runtime_dir();
248        assert!(runtime.is_absolute());
249    }
250
251    // ═══════════════════════════════════════════════════════════════════════
252    // kaish-Specific Path Tests
253    // ═══════════════════════════════════════════════════════════════════════
254
255    #[test]
256    fn kaish_paths_are_under_kaish() {
257        assert!(runtime_dir().ends_with("kaish"));
258        assert!(data_dir().ends_with("kaish"));
259        assert!(config_dir().ends_with("kaish"));
260        assert!(cache_dir().ends_with("kaish"));
261    }
262
263    #[test]
264    fn kaish_paths_build_on_xdg_primitives() {
265        // kaish paths should be XDG base + "kaish"
266        assert_eq!(data_dir(), xdg_data_home().join("kaish"));
267        assert_eq!(config_dir(), xdg_config_home().join("kaish"));
268        assert_eq!(cache_dir(), xdg_cache_home().join("kaish"));
269        assert_eq!(runtime_dir(), xdg_runtime_dir().join("kaish"));
270    }
271
272    #[test]
273    fn spill_dir_is_under_runtime() {
274        let spill = spill_dir();
275        assert!(spill.starts_with(&runtime_dir()));
276        assert!(spill.ends_with("spill"));
277    }
278
279    #[test]
280    fn kernels_dir_is_under_data() {
281        let kernels = kernels_dir();
282        let data = data_dir();
283        assert!(kernels.starts_with(&data));
284        assert!(kernels.ends_with("kernels"));
285    }
286}