Skip to main content

taskers_paths/
lib.rs

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}