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