gglib_core/paths/
config.rs

1//! Configuration file utilities.
2//!
3//! Provides functions for reading and writing the `.env` file
4//! that stores user configuration overrides.
5
6use std::fs::{self, OpenOptions};
7use std::io::Write;
8use std::path::{Path, PathBuf};
9
10use super::error::PathError;
11use super::platform::data_root;
12
13/// Location of the `.env` file that stores user overrides.
14pub fn env_file_path() -> Result<PathBuf, PathError> {
15    Ok(data_root()?.join(".env"))
16}
17
18/// Persist a key=value pair into the `.env` file.
19///
20/// If the key already exists, its value is updated.
21/// If the key doesn't exist, it is appended to the file.
22pub fn persist_env_value(key: &str, value: &str) -> Result<(), PathError> {
23    let env_path = env_file_path()?;
24
25    let lines: Vec<String> = if env_path.exists() {
26        fs::read_to_string(&env_path)
27            .map_err(|e| PathError::EnvFileError {
28                path: env_path.clone(),
29                reason: e.to_string(),
30            })?
31            .lines()
32            .map(std::string::ToString::to_string)
33            .collect()
34    } else {
35        Vec::new()
36    };
37
38    let mut updated = false;
39    let mut output: Vec<String> = Vec::with_capacity(lines.len() + 1);
40
41    for line in lines {
42        match line.split_once('=') {
43            Some((lhs, _)) if lhs.trim() == key => {
44                if !updated {
45                    output.push(format!("{key}={value}"));
46                    updated = true;
47                }
48            }
49            _ => output.push(line),
50        }
51    }
52
53    if !updated {
54        if !output.is_empty() && !output.last().unwrap().is_empty() {
55            output.push(String::new());
56        }
57        output.push(format!("{key}={value}"));
58    }
59
60    // Ensure file ends with newline
61    if !output.is_empty() && !output.last().unwrap().is_empty() {
62        output.push(String::new());
63    }
64
65    let mut file = OpenOptions::new()
66        .create(true)
67        .write(true)
68        .truncate(true)
69        .open(&env_path)
70        .map_err(|e| PathError::EnvFileError {
71            path: env_path.clone(),
72            reason: e.to_string(),
73        })?;
74
75    let content = output.join("\n");
76    file.write_all(content.as_bytes())
77        .map_err(|e| PathError::EnvFileError {
78            path: env_path,
79            reason: e.to_string(),
80        })?;
81
82    Ok(())
83}
84
85/// Persist the selected models directory into `.env`.
86pub fn persist_models_dir(path: &Path) -> Result<(), PathError> {
87    let serialized = path.to_string_lossy().to_string();
88    persist_env_value("GGLIB_MODELS_DIR", &serialized)
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::paths::test_utils::{ENV_LOCK, EnvVarGuard};
95    use tempfile::tempdir;
96
97    #[test]
98    fn test_persist_models_dir_writes_env_file() {
99        let _guard = ENV_LOCK.lock().unwrap();
100        let temp = tempdir().unwrap();
101
102        // Capture and restore GGLIB_DATA_DIR in a guard to ensure cleanup
103        let _env_guard = EnvVarGuard::set("GGLIB_DATA_DIR", temp.path().to_string_lossy().as_ref());
104
105        let models_dir = temp.path().join("models");
106        persist_models_dir(&models_dir).unwrap();
107
108        let env_contents = fs::read_to_string(temp.path().join(".env")).unwrap();
109        assert!(env_contents.contains("GGLIB_MODELS_DIR"));
110        assert!(env_contents.contains(models_dir.to_string_lossy().as_ref()));
111
112        // _env_guard will restore the original value when dropped
113    }
114}