Skip to main content

mxr_config/
resolve.rs

1use std::path::{Path, PathBuf};
2
3use crate::types::MxrConfig;
4
5/// Errors that can occur during config loading.
6#[derive(Debug, thiserror::Error)]
7pub enum ConfigError {
8    #[error("failed to read config file at {path}")]
9    ReadFile {
10        path: PathBuf,
11        source: std::io::Error,
12    },
13    #[error("failed to parse TOML config at {path}")]
14    ParseToml {
15        path: PathBuf,
16        source: toml::de::Error,
17    },
18    #[error("failed to serialize TOML config at {path}")]
19    SerializeToml {
20        path: PathBuf,
21        source: toml::ser::Error,
22    },
23}
24
25/// Returns the mxr config directory (e.g. `~/.config/mxr` on Linux/macOS).
26pub fn config_dir() -> PathBuf {
27    if let Some(path) = env_path("MXR_CONFIG_DIR") {
28        return path;
29    }
30    dirs::config_dir()
31        .unwrap_or_else(|| PathBuf::from("."))
32        .join("mxr")
33}
34
35/// Returns the runtime instance name used for data/socket namespacing.
36///
37/// Defaults:
38/// - release builds: `mxr`
39/// - debug builds: `mxr-dev`
40/// - override with `MXR_INSTANCE`
41pub fn app_instance_name() -> String {
42    if let Ok(value) = std::env::var("MXR_INSTANCE") {
43        let trimmed = value.trim();
44        if !trimmed.is_empty() {
45            return trimmed.to_string();
46        }
47    }
48
49    if cfg!(debug_assertions) {
50        "mxr-dev".to_string()
51    } else {
52        "mxr".to_string()
53    }
54}
55
56/// Returns the path to the main config file.
57pub fn config_file_path() -> PathBuf {
58    config_dir().join("config.toml")
59}
60
61/// Returns the mxr data directory (e.g. `~/.local/share/mxr` on Linux).
62pub fn data_dir() -> PathBuf {
63    if let Some(path) = env_path("MXR_DATA_DIR") {
64        return path;
65    }
66    dirs::data_dir()
67        .unwrap_or_else(|| PathBuf::from("."))
68        .join(app_instance_name())
69}
70
71/// Returns the IPC socket path for the current instance.
72pub fn socket_path() -> PathBuf {
73    if let Some(path) = env_path("MXR_SOCKET_PATH") {
74        return path;
75    }
76    if cfg!(target_os = "macos") {
77        dirs::home_dir()
78            .unwrap_or_else(|| PathBuf::from("."))
79            .join("Library")
80            .join("Application Support")
81            .join(app_instance_name())
82            .join("mxr.sock")
83    } else {
84        dirs::runtime_dir()
85            .unwrap_or_else(|| PathBuf::from("/tmp"))
86            .join(app_instance_name())
87            .join("mxr.sock")
88    }
89}
90
91fn env_path(key: &str) -> Option<PathBuf> {
92    std::env::var_os(key)
93        .map(PathBuf::from)
94        .filter(|path| !path.as_os_str().is_empty())
95}
96
97/// Load config from the default config file path, falling back to defaults.
98pub fn load_config() -> Result<MxrConfig, ConfigError> {
99    load_config_from_path(&config_file_path())
100}
101
102/// Save config to the default config file path.
103pub fn save_config(config: &MxrConfig) -> Result<(), ConfigError> {
104    save_config_to_path(config, &config_file_path())
105}
106
107/// Load config from a specific file path. Returns defaults if file doesn't exist.
108pub fn load_config_from_path(path: &Path) -> Result<MxrConfig, ConfigError> {
109    let mut config = match std::fs::read_to_string(path) {
110        Ok(contents) => load_config_from_str(&contents).map_err(|e| match e {
111            ConfigError::ParseToml { source, .. } => ConfigError::ParseToml {
112                path: path.to_path_buf(),
113                source,
114            },
115            other => other,
116        })?,
117        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
118            tracing::debug!(
119                "config file not found at {}, using defaults",
120                path.display()
121            );
122            MxrConfig::default()
123        }
124        Err(e) => {
125            return Err(ConfigError::ReadFile {
126                path: path.to_path_buf(),
127                source: e,
128            });
129        }
130    };
131
132    apply_env_overrides(&mut config);
133    Ok(config)
134}
135
136/// Save config to a specific path.
137pub fn save_config_to_path(config: &MxrConfig, path: &Path) -> Result<(), ConfigError> {
138    if let Some(parent) = path.parent() {
139        std::fs::create_dir_all(parent).map_err(|source| ConfigError::ReadFile {
140            path: parent.to_path_buf(),
141            source,
142        })?;
143    }
144    let contents = toml::to_string_pretty(config).map_err(|source| ConfigError::SerializeToml {
145        path: path.to_path_buf(),
146        source,
147    })?;
148    std::fs::write(path, contents).map_err(|source| ConfigError::ReadFile {
149        path: path.to_path_buf(),
150        source,
151    })
152}
153
154/// Load config from a TOML string.
155pub fn load_config_from_str(toml_str: &str) -> Result<MxrConfig, ConfigError> {
156    toml::from_str(toml_str).map_err(|e| ConfigError::ParseToml {
157        path: PathBuf::from("<string>"),
158        source: e,
159    })
160}
161
162/// Apply environment variable overrides to the config.
163fn apply_env_overrides(config: &mut MxrConfig) {
164    if let Ok(val) = std::env::var("MXR_EDITOR") {
165        config.general.editor = Some(val);
166    }
167    if let Ok(val) = std::env::var("MXR_SYNC_INTERVAL") {
168        if let Ok(interval) = val.parse::<u64>() {
169            config.general.sync_interval = interval;
170        }
171    }
172    if let Ok(val) = std::env::var("MXR_DEFAULT_ACCOUNT") {
173        config.general.default_account = Some(val);
174    }
175    if let Ok(val) = std::env::var("MXR_ATTACHMENT_DIR") {
176        config.general.attachment_dir = PathBuf::from(val);
177    }
178}