1use std::{env, path::PathBuf};
2
3pub const APP_ID: &str = "dev.taskers.app";
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum HostPlatform {
7 Linux,
8 Macos,
9 Other,
10}
11
12impl HostPlatform {
13 pub const fn detect() -> Self {
14 if cfg!(target_os = "linux") {
15 Self::Linux
16 } else if cfg!(target_os = "macos") {
17 Self::Macos
18 } else {
19 Self::Other
20 }
21 }
22}
23
24#[derive(Debug, Clone, Default)]
25struct EnvPaths {
26 home: Option<PathBuf>,
27 xdg_cache_home: Option<PathBuf>,
28 xdg_config_home: Option<PathBuf>,
29 xdg_data_home: Option<PathBuf>,
30 xdg_runtime_dir: Option<PathBuf>,
31 xdg_state_home: Option<PathBuf>,
32 taskers_config_path: Option<PathBuf>,
33 taskers_ghostty_runtime_dir: Option<PathBuf>,
34 taskers_runtime_dir: Option<PathBuf>,
35 taskers_session_path: Option<PathBuf>,
36 taskers_socket_path: Option<PathBuf>,
37 taskers_terminal_socket_path: Option<PathBuf>,
38}
39
40impl EnvPaths {
41 fn current() -> Self {
42 Self {
43 home: env::var_os("HOME").map(PathBuf::from),
44 xdg_cache_home: env::var_os("XDG_CACHE_HOME").map(PathBuf::from),
45 xdg_config_home: env::var_os("XDG_CONFIG_HOME").map(PathBuf::from),
46 xdg_data_home: env::var_os("XDG_DATA_HOME").map(PathBuf::from),
47 xdg_runtime_dir: env::var_os("XDG_RUNTIME_DIR").map(PathBuf::from),
48 xdg_state_home: env::var_os("XDG_STATE_HOME").map(PathBuf::from),
49 taskers_config_path: env::var_os("TASKERS_CONFIG_PATH").map(PathBuf::from),
50 taskers_ghostty_runtime_dir: env::var_os("TASKERS_GHOSTTY_RUNTIME_DIR")
51 .map(PathBuf::from),
52 taskers_runtime_dir: env::var_os("TASKERS_RUNTIME_DIR").map(PathBuf::from),
53 taskers_session_path: env::var_os("TASKERS_SESSION_PATH").map(PathBuf::from),
54 taskers_socket_path: env::var_os("TASKERS_SOCKET_PATH").map(PathBuf::from),
55 taskers_terminal_socket_path: env::var_os("TASKERS_TERMINAL_SOCKET_PATH")
56 .map(PathBuf::from),
57 }
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct TaskersPaths {
63 config_dir: PathBuf,
64 state_dir: PathBuf,
65 cache_dir: PathBuf,
66 data_dir: PathBuf,
67 shell_runtime_dir: PathBuf,
68 ghostty_runtime_dir: PathBuf,
69 socket_path: PathBuf,
70 terminal_socket_path: PathBuf,
71 session_path: PathBuf,
72 config_path: PathBuf,
73 theme_dir: PathBuf,
74}
75
76impl TaskersPaths {
77 pub fn detect() -> Self {
78 Self::from_env(HostPlatform::detect(), &EnvPaths::current())
79 }
80
81 fn from_env(platform: HostPlatform, env_paths: &EnvPaths) -> Self {
82 if let Some(config_path) = env_paths.taskers_config_path.clone() {
83 let config_dir = config_path
84 .parent()
85 .map(PathBuf::from)
86 .unwrap_or_else(|| temp_root().join("config"));
87 let state_dir = env_paths
88 .taskers_session_path
89 .clone()
90 .and_then(|path| path.parent().map(PathBuf::from))
91 .unwrap_or_else(|| platform_state_dir(platform, env_paths));
92 let cache_dir = platform_cache_dir(platform, env_paths);
93 let data_dir = platform_data_dir(platform, env_paths);
94 let shell_runtime_dir = shell_runtime_dir(platform, env_paths, &cache_dir);
95 let ghostty_runtime_dir = env_paths
96 .taskers_ghostty_runtime_dir
97 .clone()
98 .unwrap_or_else(|| data_dir.join("ghostty"));
99 let socket_path = env_paths
100 .taskers_socket_path
101 .clone()
102 .unwrap_or_else(|| socket_path(platform, &cache_dir));
103 let terminal_socket_path = env_paths
104 .taskers_terminal_socket_path
105 .clone()
106 .unwrap_or_else(|| terminal_socket_path(platform, env_paths, &cache_dir));
107 let session_path = env_paths
108 .taskers_session_path
109 .clone()
110 .unwrap_or_else(|| state_dir.join("session.json"));
111 return Self {
112 theme_dir: config_dir.join("themes"),
113 config_dir,
114 state_dir,
115 cache_dir,
116 data_dir,
117 shell_runtime_dir,
118 ghostty_runtime_dir,
119 socket_path,
120 terminal_socket_path,
121 session_path,
122 config_path,
123 };
124 }
125
126 let config_dir = platform_config_dir(platform, env_paths);
127 let state_dir = platform_state_dir(platform, env_paths);
128 let cache_dir = platform_cache_dir(platform, env_paths);
129 let data_dir = platform_data_dir(platform, env_paths);
130 let shell_runtime_dir = shell_runtime_dir(platform, env_paths, &cache_dir);
131 let ghostty_runtime_dir = env_paths
132 .taskers_ghostty_runtime_dir
133 .clone()
134 .unwrap_or_else(|| data_dir.join("ghostty"));
135 let socket_path = env_paths
136 .taskers_socket_path
137 .clone()
138 .unwrap_or_else(|| socket_path(platform, &cache_dir));
139 let terminal_socket_path = env_paths
140 .taskers_terminal_socket_path
141 .clone()
142 .unwrap_or_else(|| terminal_socket_path(platform, env_paths, &cache_dir));
143 let session_path = env_paths
144 .taskers_session_path
145 .clone()
146 .unwrap_or_else(|| state_dir.join("session.json"));
147
148 Self {
149 config_path: config_dir.join("config.json"),
150 theme_dir: config_dir.join("themes"),
151 config_dir,
152 state_dir,
153 cache_dir,
154 data_dir,
155 shell_runtime_dir,
156 ghostty_runtime_dir,
157 socket_path,
158 terminal_socket_path,
159 session_path,
160 }
161 }
162
163 pub fn config_dir(&self) -> &PathBuf {
164 &self.config_dir
165 }
166
167 pub fn state_dir(&self) -> &PathBuf {
168 &self.state_dir
169 }
170
171 pub fn cache_dir(&self) -> &PathBuf {
172 &self.cache_dir
173 }
174
175 pub fn data_dir(&self) -> &PathBuf {
176 &self.data_dir
177 }
178
179 pub fn shell_runtime_dir(&self) -> &PathBuf {
180 &self.shell_runtime_dir
181 }
182
183 pub fn ghostty_runtime_dir(&self) -> &PathBuf {
184 &self.ghostty_runtime_dir
185 }
186
187 pub fn socket_path(&self) -> &PathBuf {
188 &self.socket_path
189 }
190
191 pub fn terminal_socket_path(&self) -> &PathBuf {
192 &self.terminal_socket_path
193 }
194
195 pub fn session_path(&self) -> &PathBuf {
196 &self.session_path
197 }
198
199 pub fn config_path(&self) -> &PathBuf {
200 &self.config_path
201 }
202
203 pub fn theme_dir(&self) -> &PathBuf {
204 &self.theme_dir
205 }
206}
207
208pub fn default_socket_path() -> PathBuf {
209 TaskersPaths::detect().socket_path
210}
211
212pub fn default_session_path() -> PathBuf {
213 TaskersPaths::detect().session_path
214}
215
216pub fn default_terminal_socket_path() -> PathBuf {
217 TaskersPaths::detect().terminal_socket_path
218}
219
220pub fn default_config_path() -> PathBuf {
221 TaskersPaths::detect().config_path
222}
223
224pub fn default_theme_dir() -> PathBuf {
225 TaskersPaths::detect().theme_dir
226}
227
228pub fn default_shell_runtime_dir() -> PathBuf {
229 TaskersPaths::detect().shell_runtime_dir
230}
231
232pub fn default_ghostty_runtime_dir() -> PathBuf {
233 TaskersPaths::detect().ghostty_runtime_dir
234}
235
236pub fn default_release_install_root() -> PathBuf {
237 TaskersPaths::detect().data_dir.join("releases")
238}
239
240fn platform_config_dir(platform: HostPlatform, env_paths: &EnvPaths) -> PathBuf {
241 match platform {
242 HostPlatform::Macos => home_library_dir(env_paths, "Application Support"),
243 HostPlatform::Linux => env_paths
244 .xdg_config_home
245 .clone()
246 .map(|path| path.join("taskers"))
247 .or_else(|| {
248 env_paths
249 .home
250 .clone()
251 .map(|path| path.join(".config").join("taskers"))
252 })
253 .unwrap_or_else(|| temp_root().join("config")),
254 HostPlatform::Other => env_paths
255 .home
256 .clone()
257 .map(|path| path.join(".taskers"))
258 .unwrap_or_else(|| temp_root().join("config")),
259 }
260}
261
262fn platform_state_dir(platform: HostPlatform, env_paths: &EnvPaths) -> PathBuf {
263 match platform {
264 HostPlatform::Macos => home_library_dir(env_paths, "Application Support"),
265 HostPlatform::Linux => env_paths
266 .xdg_state_home
267 .clone()
268 .map(|path| path.join("taskers"))
269 .or_else(|| {
270 env_paths
271 .home
272 .clone()
273 .map(|path| path.join(".local").join("state").join("taskers"))
274 })
275 .unwrap_or_else(|| temp_root().join("state")),
276 HostPlatform::Other => env_paths
277 .home
278 .clone()
279 .map(|path| path.join(".taskers"))
280 .unwrap_or_else(|| temp_root().join("state")),
281 }
282}
283
284fn platform_cache_dir(platform: HostPlatform, env_paths: &EnvPaths) -> PathBuf {
285 match platform {
286 HostPlatform::Macos => home_library_cache_dir(env_paths),
287 HostPlatform::Linux => env_paths
288 .xdg_cache_home
289 .clone()
290 .map(|path| path.join("taskers"))
291 .or_else(|| {
292 env_paths
293 .home
294 .clone()
295 .map(|path| path.join(".cache").join("taskers"))
296 })
297 .unwrap_or_else(|| temp_root().join("cache")),
298 HostPlatform::Other => env_paths
299 .home
300 .clone()
301 .map(|path| path.join(".taskers").join("cache"))
302 .unwrap_or_else(|| temp_root().join("cache")),
303 }
304}
305
306fn platform_data_dir(platform: HostPlatform, env_paths: &EnvPaths) -> PathBuf {
307 match platform {
308 HostPlatform::Macos => home_library_dir(env_paths, "Application Support"),
309 HostPlatform::Linux => env_paths
310 .xdg_data_home
311 .clone()
312 .map(|path| path.join("taskers"))
313 .or_else(|| {
314 env_paths
315 .home
316 .clone()
317 .map(|path| path.join(".local").join("share").join("taskers"))
318 })
319 .unwrap_or_else(|| temp_root().join("data")),
320 HostPlatform::Other => env_paths
321 .home
322 .clone()
323 .map(|path| path.join(".taskers").join("data"))
324 .unwrap_or_else(|| temp_root().join("data")),
325 }
326}
327
328fn shell_runtime_dir(platform: HostPlatform, env_paths: &EnvPaths, cache_dir: &PathBuf) -> PathBuf {
329 if let Some(path) = env_paths.taskers_runtime_dir.clone() {
330 return path.join("shell");
331 }
332
333 match platform {
334 HostPlatform::Linux => env_paths
335 .xdg_runtime_dir
336 .clone()
337 .map(|path| path.join("taskers").join("shell"))
338 .unwrap_or_else(|| env::temp_dir().join("taskers-runtime").join("shell")),
339 HostPlatform::Macos => cache_dir.join("runtime").join("shell"),
340 HostPlatform::Other => env::temp_dir().join("taskers-runtime").join("shell"),
341 }
342}
343
344fn socket_path(platform: HostPlatform, cache_dir: &PathBuf) -> PathBuf {
345 match platform {
346 HostPlatform::Macos => cache_dir.join("control.sock"),
347 HostPlatform::Linux | HostPlatform::Other => PathBuf::from("/tmp/taskers.sock"),
348 }
349}
350
351fn terminal_socket_path(
352 platform: HostPlatform,
353 env_paths: &EnvPaths,
354 cache_dir: &PathBuf,
355) -> PathBuf {
356 match platform {
357 HostPlatform::Linux => env_paths
358 .xdg_runtime_dir
359 .clone()
360 .map(|path| path.join("taskers").join("terminal.sock"))
361 .unwrap_or_else(|| PathBuf::from("/tmp/taskers-terminal.sock")),
362 HostPlatform::Macos => cache_dir.join("terminal.sock"),
363 HostPlatform::Other => PathBuf::from("/tmp/taskers-terminal.sock"),
364 }
365}
366
367fn home_library_dir(env_paths: &EnvPaths, leaf: &str) -> PathBuf {
368 env_paths
369 .home
370 .clone()
371 .map(|path| path.join("Library").join(leaf).join(APP_ID))
372 .unwrap_or_else(|| temp_root().join(leaf.replace(' ', "-").to_ascii_lowercase()))
373}
374
375fn home_library_cache_dir(env_paths: &EnvPaths) -> PathBuf {
376 env_paths
377 .home
378 .clone()
379 .map(|path| path.join("Library").join("Caches").join(APP_ID))
380 .unwrap_or_else(|| temp_root().join("cache"))
381}
382
383fn temp_root() -> PathBuf {
384 env::temp_dir().join("taskers")
385}
386
387#[cfg(test)]
388mod tests {
389 use super::{APP_ID, EnvPaths, HostPlatform, TaskersPaths};
390 use std::path::PathBuf;
391
392 #[test]
393 fn macos_paths_use_library_directories() {
394 let env = EnvPaths {
395 home: Some(PathBuf::from("/Users/notes")),
396 ..EnvPaths::default()
397 };
398 let paths = TaskersPaths::from_env(HostPlatform::Macos, &env);
399
400 assert_eq!(
401 paths.config_path(),
402 &PathBuf::from(format!(
403 "/Users/notes/Library/Application Support/{APP_ID}/config.json"
404 ))
405 );
406 assert_eq!(
407 paths.session_path(),
408 &PathBuf::from(format!(
409 "/Users/notes/Library/Application Support/{APP_ID}/session.json"
410 ))
411 );
412 assert_eq!(
413 paths.socket_path(),
414 &PathBuf::from(format!("/Users/notes/Library/Caches/{APP_ID}/control.sock"))
415 );
416 assert_eq!(
417 paths.terminal_socket_path(),
418 &PathBuf::from(format!(
419 "/Users/notes/Library/Caches/{APP_ID}/terminal.sock"
420 ))
421 );
422 assert_eq!(
423 paths.shell_runtime_dir(),
424 &PathBuf::from(format!(
425 "/Users/notes/Library/Caches/{APP_ID}/runtime/shell"
426 ))
427 );
428 }
429
430 #[test]
431 fn linux_paths_preserve_xdg_defaults() {
432 let env = EnvPaths {
433 home: Some(PathBuf::from("/home/notes")),
434 xdg_config_home: Some(PathBuf::from("/tmp/config")),
435 xdg_state_home: Some(PathBuf::from("/tmp/state")),
436 xdg_cache_home: Some(PathBuf::from("/tmp/cache")),
437 xdg_data_home: Some(PathBuf::from("/tmp/data")),
438 xdg_runtime_dir: Some(PathBuf::from("/tmp/runtime")),
439 ..EnvPaths::default()
440 };
441 let paths = TaskersPaths::from_env(HostPlatform::Linux, &env);
442
443 assert_eq!(
444 paths.config_path(),
445 &PathBuf::from("/tmp/config/taskers/config.json")
446 );
447 assert_eq!(
448 paths.session_path(),
449 &PathBuf::from("/tmp/state/taskers/session.json")
450 );
451 assert_eq!(
452 paths.ghostty_runtime_dir(),
453 &PathBuf::from("/tmp/data/taskers/ghostty")
454 );
455 assert_eq!(
456 paths.shell_runtime_dir(),
457 &PathBuf::from("/tmp/runtime/taskers/shell")
458 );
459 assert_eq!(paths.socket_path(), &PathBuf::from("/tmp/taskers.sock"));
460 assert_eq!(
461 paths.terminal_socket_path(),
462 &PathBuf::from("/tmp/runtime/taskers/terminal.sock")
463 );
464 }
465
466 #[test]
467 fn explicit_overrides_win() {
468 let env = EnvPaths {
469 taskers_config_path: Some(PathBuf::from("/work/config.json")),
470 taskers_session_path: Some(PathBuf::from("/work/session.json")),
471 taskers_socket_path: Some(PathBuf::from("/work/control.sock")),
472 taskers_terminal_socket_path: Some(PathBuf::from("/work/terminal.sock")),
473 taskers_runtime_dir: Some(PathBuf::from("/work/runtime")),
474 taskers_ghostty_runtime_dir: Some(PathBuf::from("/work/ghostty")),
475 ..EnvPaths::default()
476 };
477 let paths = TaskersPaths::from_env(HostPlatform::Macos, &env);
478
479 assert_eq!(paths.config_path(), &PathBuf::from("/work/config.json"));
480 assert_eq!(paths.session_path(), &PathBuf::from("/work/session.json"));
481 assert_eq!(paths.socket_path(), &PathBuf::from("/work/control.sock"));
482 assert_eq!(
483 paths.terminal_socket_path(),
484 &PathBuf::from("/work/terminal.sock")
485 );
486 assert_eq!(
487 paths.shell_runtime_dir(),
488 &PathBuf::from("/work/runtime/shell")
489 );
490 assert_eq!(paths.ghostty_runtime_dir(), &PathBuf::from("/work/ghostty"));
491 }
492
493 #[test]
494 fn release_install_roots_follow_platform_defaults() {
495 let mac = EnvPaths {
496 home: Some(PathBuf::from("/Users/notes")),
497 ..EnvPaths::default()
498 };
499 let linux = EnvPaths {
500 home: Some(PathBuf::from("/home/notes")),
501 xdg_data_home: Some(PathBuf::from("/tmp/data")),
502 ..EnvPaths::default()
503 };
504
505 assert_eq!(
506 TaskersPaths::from_env(HostPlatform::Macos, &mac)
507 .data_dir()
508 .join("releases"),
509 PathBuf::from(format!(
510 "/Users/notes/Library/Application Support/{APP_ID}/releases"
511 ))
512 );
513 assert_eq!(
514 TaskersPaths::from_env(HostPlatform::Linux, &linux)
515 .data_dir()
516 .join("releases"),
517 PathBuf::from("/tmp/data/taskers/releases")
518 );
519 }
520}