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
6use crate::detect::Kind;
7
8const ENV_OVERRIDE: &str = "BROWSER_CONTROL_DATA_DIR";
9const CONFIG_ENV_OVERRIDE: &str = "BROWSER_CONTROL_CONFIG_DIR";
10
11/// Root data directory for browser-control. Used for the registry DB and profile dirs.
12///
13/// macOS:   `~/Library/Application Support/browser-control`
14/// Linux:   `$XDG_DATA_HOME/browser-control` (or `~/.local/share/browser-control`)
15/// Windows: `%APPDATA%/browser-control`
16///
17/// Override with env var `BROWSER_CONTROL_DATA_DIR` (absolute path).
18pub fn data_dir() -> Result<PathBuf> {
19    if let Some(v) = std::env::var_os(ENV_OVERRIDE) {
20        let p = PathBuf::from(v);
21        if p.as_os_str().is_empty() {
22            return Err(anyhow!("{} is set but empty", ENV_OVERRIDE));
23        }
24        return Ok(p);
25    }
26
27    let pd = directories::ProjectDirs::from("", "", "browser-control")
28        .ok_or_else(|| anyhow!("could not determine user data directory (no home dir found)"))?;
29    Ok(pd.data_dir().to_path_buf())
30}
31
32/// Returns `<data_dir>/registry.db`. Ensures the parent directory exists.
33pub fn registry_db_path() -> Result<PathBuf> {
34    let dir = data_dir()?;
35    std::fs::create_dir_all(&dir)
36        .with_context(|| format!("creating data dir {}", dir.display()))?;
37    Ok(dir.join("registry.db"))
38}
39
40/// Root config directory for browser-control.
41///
42/// macOS:   `~/Library/Application Support/browser-control` (same as `data_dir`)
43/// Linux:   `$XDG_CONFIG_HOME/browser-control` (or `~/.config/browser-control`)
44/// Windows: `%APPDATA%/browser-control` (same as `data_dir`)
45///
46/// Override with env var `BROWSER_CONTROL_CONFIG_DIR` (absolute path).
47pub fn config_dir() -> Result<PathBuf> {
48    if let Some(v) = std::env::var_os(CONFIG_ENV_OVERRIDE) {
49        let p = PathBuf::from(v);
50        if p.as_os_str().is_empty() {
51            return Err(anyhow!("{} is set but empty", CONFIG_ENV_OVERRIDE));
52        }
53        return Ok(p);
54    }
55
56    let pd = directories::ProjectDirs::from("", "", "browser-control")
57        .ok_or_else(|| anyhow!("could not determine user config directory (no home dir found)"))?;
58    Ok(pd.config_dir().to_path_buf())
59}
60
61/// Returns `<config_dir>/config.toml`. Ensures the parent directory exists.
62pub fn config_file_path() -> Result<PathBuf> {
63    let dir = config_dir()?;
64    std::fs::create_dir_all(&dir)
65        .with_context(|| format!("creating config dir {}", dir.display()))?;
66    Ok(dir.join("config.toml"))
67}
68
69/// Returns `<data_dir>/profiles`. Ensures the directory exists.
70pub fn profiles_dir() -> Result<PathBuf> {
71    let dir = data_dir()?.join("profiles");
72    std::fs::create_dir_all(&dir)
73        .with_context(|| format!("creating profiles dir {}", dir.display()))?;
74    Ok(dir)
75}
76
77/// Returns the stable per-kind default profile directory used when
78/// `browser-control start` is invoked without `--profile`.
79///
80/// The directory is rooted under [`config_dir`] (so it tracks
81/// `BROWSER_CONTROL_CONFIG_DIR` for tests and user overrides):
82///
83/// * macOS:   `~/Library/Application Support/browser-control/profiles/<kind>/default/`
84/// * Linux:   `~/.config/browser-control/profiles/<kind>/default/`
85/// * Windows: `%APPDATA%/browser-control/profiles/<kind>/default/`
86///
87/// `<kind>` is the lowercase [`Kind`] variant name (`chrome`, `edge`,
88/// `chromium`, `brave`, `firefox`). The split-by-kind is intentional:
89/// Chromium and Firefox profile layouts are not interchangeable.
90///
91/// The directory is created lazily on first call.
92pub fn default_profile_dir(kind: Kind) -> Result<PathBuf> {
93    let dir = config_dir()?
94        .join("profiles")
95        .join(kind.as_str())
96        .join("default");
97    std::fs::create_dir_all(&dir)
98        .with_context(|| format!("creating default profile dir {}", dir.display()))?;
99    Ok(dir)
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn paths_work() {
108        let _g = crate::test_support::ENV_LOCK
109            .lock()
110            .unwrap_or_else(|e| e.into_inner());
111        // Test 1: override via env var.
112        let tmp = tempfile::TempDir::new().unwrap();
113        let tmp_path = tmp.path().to_path_buf();
114        // Safety: tests in this module share process env; we run them sequentially
115        // within a single #[test] to avoid races with other tests.
116        std::env::set_var(ENV_OVERRIDE, &tmp_path);
117
118        assert_eq!(data_dir().unwrap(), tmp_path);
119
120        let db = registry_db_path().unwrap();
121        assert_eq!(db, tmp_path.join("registry.db"));
122        assert!(db.parent().unwrap().exists());
123        assert_eq!(db.file_name().unwrap(), "registry.db");
124
125        let pdir = profiles_dir().unwrap();
126        assert_eq!(pdir, tmp_path.join("profiles"));
127        assert!(pdir.is_dir());
128        assert_eq!(pdir.file_name().unwrap(), "profiles");
129
130        // Test 2: default path has the expected suffix.
131        std::env::remove_var(ENV_OVERRIDE);
132        let d = data_dir().unwrap();
133        assert!(
134            d.ends_with("browser-control"),
135            "expected default data_dir to end with 'browser-control', got {}",
136            d.display()
137        );
138    }
139
140    #[test]
141    fn config_paths_work() {
142        let _g = crate::test_support::ENV_LOCK
143            .lock()
144            .unwrap_or_else(|e| e.into_inner());
145        let tmp = tempfile::TempDir::new().unwrap();
146        let tmp_path = tmp.path().to_path_buf();
147        std::env::set_var(CONFIG_ENV_OVERRIDE, &tmp_path);
148
149        assert_eq!(config_dir().unwrap(), tmp_path);
150
151        let cfg = config_file_path().unwrap();
152        assert_eq!(cfg, tmp_path.join("config.toml"));
153        assert!(cfg.parent().unwrap().exists());
154        assert_eq!(cfg.file_name().unwrap(), "config.toml");
155
156        std::env::remove_var(CONFIG_ENV_OVERRIDE);
157        let d = config_dir().unwrap();
158        assert!(
159            d.ends_with("browser-control"),
160            "expected default config_dir to end with 'browser-control', got {}",
161            d.display()
162        );
163    }
164
165    #[test]
166    fn default_profile_dir_honours_config_env_override() {
167        let _g = crate::test_support::ENV_LOCK
168            .lock()
169            .unwrap_or_else(|e| e.into_inner());
170        let tmp = tempfile::TempDir::new().unwrap();
171        let tmp_path = tmp.path().to_path_buf();
172        std::env::set_var(CONFIG_ENV_OVERRIDE, &tmp_path);
173
174        for k in [
175            Kind::Chrome,
176            Kind::Edge,
177            Kind::Chromium,
178            Kind::Brave,
179            Kind::Firefox,
180        ] {
181            let p = default_profile_dir(k).unwrap();
182            assert_eq!(
183                p,
184                tmp_path.join("profiles").join(k.as_str()).join("default")
185            );
186            assert!(p.is_dir(), "expected {} to be created", p.display());
187        }
188
189        std::env::remove_var(CONFIG_ENV_OVERRIDE);
190    }
191}