1use std::path::{Path, PathBuf};
2
3use crate::types::MxrConfig;
4
5#[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
25pub 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
35pub 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
56pub fn config_file_path() -> PathBuf {
58 config_dir().join("config.toml")
59}
60
61pub 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
71pub 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
97pub fn load_config() -> Result<MxrConfig, ConfigError> {
99 load_config_from_path(&config_file_path())
100}
101
102pub fn save_config(config: &MxrConfig) -> Result<(), ConfigError> {
104 save_config_to_path(config, &config_file_path())
105}
106
107pub 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
136pub 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
154pub 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
162fn 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}