1use std::env;
2use std::path::{Path, PathBuf};
3use thiserror::Error;
4
5#[derive(Debug, Error)]
6pub enum XdgError {
7 #[error("Failed to determine home directory")]
8 NoHomeDirectory,
9 #[error("IO error: {0}")]
10 Io(#[from] std::io::Error),
11}
12
13#[derive(Debug, Clone)]
15pub struct XdgDirectories {
16 data_dir: PathBuf,
17 runtime_dir: PathBuf,
18 config_dir: PathBuf,
19}
20
21impl XdgDirectories {
22 pub fn new() -> Result<Self, XdgError> {
24 let data_dir = Self::resolve_data_dir()?;
25 let runtime_dir = Self::resolve_runtime_dir()?;
26 let config_dir = Self::resolve_config_dir()?;
27
28 Ok(Self {
29 data_dir,
30 runtime_dir,
31 config_dir,
32 })
33 }
34
35 #[allow(dead_code)]
37 pub fn new_with_paths(
38 data_dir: PathBuf,
39 runtime_dir: PathBuf,
40 config_dir: PathBuf,
41 _cache_dir: PathBuf,
42 ) -> Self {
43 Self {
44 data_dir,
45 runtime_dir,
46 config_dir,
47 }
48 }
49
50 #[allow(dead_code)]
52 pub fn data_dir(&self) -> &Path {
53 &self.data_dir
54 }
55
56 #[allow(dead_code)]
58 pub fn runtime_dir(&self) -> &Path {
59 &self.runtime_dir
60 }
61
62 pub fn config_dir(&self) -> &Path {
64 &self.config_dir
65 }
66
67 #[allow(dead_code)]
69 pub fn templates_dir(&self) -> PathBuf {
70 self.config_dir.join("templates")
71 }
72
73 pub fn tasks_file(&self) -> PathBuf {
75 self.data_dir.join("tasks.json")
76 }
77
78 pub fn task_dir(&self, task_id: &str, repo_hash: &str) -> PathBuf {
80 self.data_dir
81 .join("tasks")
82 .join(format!("{repo_hash}-{task_id}"))
83 }
84
85 pub fn socket_path(&self) -> PathBuf {
87 self.runtime_dir.join("tsk.sock")
88 }
89
90 pub fn pid_file(&self) -> PathBuf {
92 self.runtime_dir.join("tsk.pid")
93 }
94
95 pub fn ensure_directories(&self) -> Result<(), XdgError> {
97 std::fs::create_dir_all(&self.data_dir)?;
98 std::fs::create_dir_all(self.data_dir.join("tasks"))?;
99 std::fs::create_dir_all(&self.runtime_dir)?;
100 std::fs::create_dir_all(&self.config_dir)?;
101 Ok(())
102 }
103
104 fn resolve_data_dir() -> Result<PathBuf, XdgError> {
105 if let Ok(xdg_data) = env::var("XDG_DATA_HOME") {
107 return Ok(PathBuf::from(xdg_data).join("tsk"));
108 }
109
110 let home = env::var("HOME")
112 .or_else(|_| env::var("USERPROFILE"))
113 .map_err(|_| XdgError::NoHomeDirectory)?;
114
115 Ok(PathBuf::from(home).join(".local").join("share").join("tsk"))
116 }
117
118 fn resolve_runtime_dir() -> Result<PathBuf, XdgError> {
119 if let Ok(xdg_runtime) = env::var("XDG_RUNTIME_DIR") {
121 return Ok(PathBuf::from(xdg_runtime).join("tsk"));
122 }
123
124 let uid = env::var("UID").unwrap_or_else(|_| {
126 #[cfg(unix)]
128 {
129 unsafe { libc::getuid().to_string() }
130 }
131 #[cfg(not(unix))]
132 {
133 "0".to_string()
134 }
135 });
136
137 Ok(PathBuf::from("/tmp").join(format!("tsk-{uid}")))
138 }
139
140 fn resolve_config_dir() -> Result<PathBuf, XdgError> {
141 if let Ok(xdg_config) = env::var("XDG_CONFIG_HOME") {
143 return Ok(PathBuf::from(xdg_config).join("tsk"));
144 }
145
146 let home = env::var("HOME")
148 .or_else(|_| env::var("USERPROFILE"))
149 .map_err(|_| XdgError::NoHomeDirectory)?;
150
151 Ok(PathBuf::from(home).join(".config").join("tsk"))
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use std::env;
159
160 #[test]
161 fn test_xdg_directories_with_env_vars() {
162 let original_data = env::var("XDG_DATA_HOME").ok();
163 let original_runtime = env::var("XDG_RUNTIME_DIR").ok();
164 let original_config = env::var("XDG_CONFIG_HOME").ok();
165
166 unsafe {
167 env::set_var("XDG_DATA_HOME", "/custom/data");
168 }
169 unsafe {
170 env::set_var("XDG_RUNTIME_DIR", "/custom/runtime");
171 }
172 unsafe {
173 env::set_var("XDG_CONFIG_HOME", "/custom/config");
174 }
175
176 let dirs = XdgDirectories::new().expect("Failed to create XDG directories");
177
178 assert_eq!(dirs.data_dir(), Path::new("/custom/data/tsk"));
179 assert_eq!(dirs.runtime_dir(), Path::new("/custom/runtime/tsk"));
180 assert_eq!(dirs.config_dir(), Path::new("/custom/config/tsk"));
181 assert_eq!(dirs.tasks_file(), Path::new("/custom/data/tsk/tasks.json"));
182 assert_eq!(
183 dirs.socket_path(),
184 Path::new("/custom/runtime/tsk/tsk.sock")
185 );
186 assert_eq!(dirs.pid_file(), Path::new("/custom/runtime/tsk/tsk.pid"));
187 assert_eq!(
188 dirs.templates_dir(),
189 Path::new("/custom/config/tsk/templates")
190 );
191
192 match original_data {
194 Some(val) => unsafe { env::set_var("XDG_DATA_HOME", val) },
195 None => unsafe { env::remove_var("XDG_DATA_HOME") },
196 }
197 match original_runtime {
198 Some(val) => unsafe { env::set_var("XDG_RUNTIME_DIR", val) },
199 None => unsafe { env::remove_var("XDG_RUNTIME_DIR") },
200 }
201 match original_config {
202 Some(val) => unsafe { env::set_var("XDG_CONFIG_HOME", val) },
203 None => unsafe { env::remove_var("XDG_CONFIG_HOME") },
204 }
205 }
206
207 #[test]
208 fn test_xdg_directories_fallback() {
209 let original_data = env::var("XDG_DATA_HOME").ok();
210 let original_runtime = env::var("XDG_RUNTIME_DIR").ok();
211 let original_config = env::var("XDG_CONFIG_HOME").ok();
212
213 unsafe {
214 env::remove_var("XDG_DATA_HOME");
215 }
216 unsafe {
217 env::remove_var("XDG_RUNTIME_DIR");
218 }
219 unsafe {
220 env::remove_var("XDG_CONFIG_HOME");
221 }
222
223 let dirs = XdgDirectories::new().expect("Failed to create XDG directories");
224
225 let home = env::var("HOME")
226 .or_else(|_| env::var("USERPROFILE"))
227 .unwrap();
228 let expected_data = PathBuf::from(&home)
229 .join(".local")
230 .join("share")
231 .join("tsk");
232 assert_eq!(dirs.data_dir(), expected_data);
233
234 let expected_config = PathBuf::from(&home).join(".config").join("tsk");
235 assert_eq!(dirs.config_dir(), expected_config);
236
237 match original_data {
239 Some(val) => unsafe { env::set_var("XDG_DATA_HOME", val) },
240 None => unsafe { env::remove_var("XDG_DATA_HOME") },
241 }
242 match original_runtime {
243 Some(val) => unsafe { env::set_var("XDG_RUNTIME_DIR", val) },
244 None => unsafe { env::remove_var("XDG_RUNTIME_DIR") },
245 }
246 match original_config {
247 Some(val) => unsafe { env::set_var("XDG_CONFIG_HOME", val) },
248 None => unsafe { env::remove_var("XDG_CONFIG_HOME") },
249 }
250 }
251
252 #[test]
253 fn test_config_dir_resolution() {
254 let original_config = env::var("XDG_CONFIG_HOME").ok();
255
256 unsafe {
258 env::set_var("XDG_CONFIG_HOME", "/test/config");
259 }
260 let config_dir = XdgDirectories::resolve_config_dir().unwrap();
261 assert_eq!(config_dir, PathBuf::from("/test/config/tsk"));
262
263 unsafe {
265 env::remove_var("XDG_CONFIG_HOME");
266 }
267 let config_dir = XdgDirectories::resolve_config_dir().unwrap();
268 let home = env::var("HOME")
269 .or_else(|_| env::var("USERPROFILE"))
270 .unwrap();
271 assert_eq!(config_dir, PathBuf::from(home).join(".config").join("tsk"));
272
273 match original_config {
275 Some(val) => unsafe { env::set_var("XDG_CONFIG_HOME", val) },
276 None => unsafe { env::remove_var("XDG_CONFIG_HOME") },
277 }
278 }
279
280 #[test]
281 fn test_task_dir_generation() {
282 let dirs = XdgDirectories::new().expect("Failed to create XDG directories");
283 let task_dir = dirs.task_dir("task-123", "repo-abc");
284
285 assert!(task_dir.to_string_lossy().contains("repo-abc-task-123"));
286 }
287}