Skip to main content

browser_control/
paths.rs

1//! OS app-data directory resolution.
2
3use anyhow::{anyhow, Context, Result};
4use std::path::PathBuf;
5
6const ENV_OVERRIDE: &str = "BROWSER_CONTROL_DATA_DIR";
7const CONFIG_ENV_OVERRIDE: &str = "BROWSER_CONTROL_CONFIG_DIR";
8
9/// Root data directory for browser-control. Used for the registry DB and profile dirs.
10///
11/// macOS:   `~/Library/Application Support/browser-control`
12/// Linux:   `$XDG_DATA_HOME/browser-control` (or `~/.local/share/browser-control`)
13/// Windows: `%APPDATA%/browser-control`
14///
15/// Override with env var `BROWSER_CONTROL_DATA_DIR` (absolute path).
16pub fn data_dir() -> Result<PathBuf> {
17    if let Some(v) = std::env::var_os(ENV_OVERRIDE) {
18        let p = PathBuf::from(v);
19        if p.as_os_str().is_empty() {
20            return Err(anyhow!("{} is set but empty", ENV_OVERRIDE));
21        }
22        return Ok(p);
23    }
24
25    let pd = directories::ProjectDirs::from("", "", "browser-control")
26        .ok_or_else(|| anyhow!("could not determine user data directory (no home dir found)"))?;
27    Ok(pd.data_dir().to_path_buf())
28}
29
30/// Returns `<data_dir>/registry.db`. Ensures the parent directory exists.
31pub fn registry_db_path() -> Result<PathBuf> {
32    let dir = data_dir()?;
33    std::fs::create_dir_all(&dir)
34        .with_context(|| format!("creating data dir {}", dir.display()))?;
35    Ok(dir.join("registry.db"))
36}
37
38/// Root config directory for browser-control.
39///
40/// macOS:   `~/Library/Application Support/browser-control` (same as `data_dir`)
41/// Linux:   `$XDG_CONFIG_HOME/browser-control` (or `~/.config/browser-control`)
42/// Windows: `%APPDATA%/browser-control` (same as `data_dir`)
43///
44/// Override with env var `BROWSER_CONTROL_CONFIG_DIR` (absolute path).
45pub fn config_dir() -> Result<PathBuf> {
46    if let Some(v) = std::env::var_os(CONFIG_ENV_OVERRIDE) {
47        let p = PathBuf::from(v);
48        if p.as_os_str().is_empty() {
49            return Err(anyhow!("{} is set but empty", CONFIG_ENV_OVERRIDE));
50        }
51        return Ok(p);
52    }
53
54    let pd = directories::ProjectDirs::from("", "", "browser-control")
55        .ok_or_else(|| anyhow!("could not determine user config directory (no home dir found)"))?;
56    Ok(pd.config_dir().to_path_buf())
57}
58
59/// Returns `<config_dir>/config.toml`. Ensures the parent directory exists.
60pub fn config_file_path() -> Result<PathBuf> {
61    let dir = config_dir()?;
62    std::fs::create_dir_all(&dir)
63        .with_context(|| format!("creating config dir {}", dir.display()))?;
64    Ok(dir.join("config.toml"))
65}
66
67/// Returns `<data_dir>/profiles`. Ensures the directory exists.
68pub fn profiles_dir() -> Result<PathBuf> {
69    let dir = data_dir()?.join("profiles");
70    std::fs::create_dir_all(&dir)
71        .with_context(|| format!("creating profiles dir {}", dir.display()))?;
72    Ok(dir)
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn paths_work() {
81        let _g = crate::test_support::ENV_LOCK
82            .lock()
83            .unwrap_or_else(|e| e.into_inner());
84        // Test 1: override via env var.
85        let tmp = tempfile::TempDir::new().unwrap();
86        let tmp_path = tmp.path().to_path_buf();
87        // Safety: tests in this module share process env; we run them sequentially
88        // within a single #[test] to avoid races with other tests.
89        std::env::set_var(ENV_OVERRIDE, &tmp_path);
90
91        assert_eq!(data_dir().unwrap(), tmp_path);
92
93        let db = registry_db_path().unwrap();
94        assert_eq!(db, tmp_path.join("registry.db"));
95        assert!(db.parent().unwrap().exists());
96        assert_eq!(db.file_name().unwrap(), "registry.db");
97
98        let pdir = profiles_dir().unwrap();
99        assert_eq!(pdir, tmp_path.join("profiles"));
100        assert!(pdir.is_dir());
101        assert_eq!(pdir.file_name().unwrap(), "profiles");
102
103        // Test 2: default path has the expected suffix.
104        std::env::remove_var(ENV_OVERRIDE);
105        let d = data_dir().unwrap();
106        assert!(
107            d.ends_with("browser-control"),
108            "expected default data_dir to end with 'browser-control', got {}",
109            d.display()
110        );
111    }
112
113    #[test]
114    fn config_paths_work() {
115        let _g = crate::test_support::ENV_LOCK
116            .lock()
117            .unwrap_or_else(|e| e.into_inner());
118        let tmp = tempfile::TempDir::new().unwrap();
119        let tmp_path = tmp.path().to_path_buf();
120        std::env::set_var(CONFIG_ENV_OVERRIDE, &tmp_path);
121
122        assert_eq!(config_dir().unwrap(), tmp_path);
123
124        let cfg = config_file_path().unwrap();
125        assert_eq!(cfg, tmp_path.join("config.toml"));
126        assert!(cfg.parent().unwrap().exists());
127        assert_eq!(cfg.file_name().unwrap(), "config.toml");
128
129        std::env::remove_var(CONFIG_ENV_OVERRIDE);
130        let d = config_dir().unwrap();
131        assert!(
132            d.ends_with("browser-control"),
133            "expected default config_dir to end with 'browser-control', got {}",
134            d.display()
135        );
136    }
137}