open_sesame/config/
loader.rs1use crate::config::schema::Config;
6use crate::util::{Error, Result};
7use std::io::Read;
8use std::path::{Path, PathBuf};
9
10fn xdg_config_home() -> Option<PathBuf> {
15 dirs::config_dir().or_else(|| {
16 dirs::home_dir().map(|h| h.join(".config"))
18 })
19}
20
21fn system_config_dir() -> PathBuf {
23 PathBuf::from("/etc/open-sesame")
24}
25
26pub fn user_config_dir() -> Option<PathBuf> {
30 xdg_config_home().map(|c| c.join("open-sesame"))
31}
32
33pub fn user_config_path() -> Option<PathBuf> {
37 user_config_dir().map(|d| d.join("config.toml"))
38}
39
40fn user_config_d_path() -> Option<PathBuf> {
42 user_config_dir().map(|d| d.join("config.d"))
43}
44
45fn deep_merge(base: &mut Config, overlay: Config) {
47 let defaults = Config::default();
48
49 if overlay.settings.activation_key != defaults.settings.activation_key {
51 base.settings.activation_key = overlay.settings.activation_key;
52 }
53 if overlay.settings.activation_delay != defaults.settings.activation_delay {
54 base.settings.activation_delay = overlay.settings.activation_delay;
55 }
56 if overlay.settings.overlay_delay != defaults.settings.overlay_delay {
57 base.settings.overlay_delay = overlay.settings.overlay_delay;
58 }
59 if overlay.settings.quick_switch_threshold != defaults.settings.quick_switch_threshold {
60 base.settings.quick_switch_threshold = overlay.settings.quick_switch_threshold;
61 }
62 if overlay.settings.border_width != defaults.settings.border_width {
63 base.settings.border_width = overlay.settings.border_width;
64 }
65 if overlay.settings.border_color != defaults.settings.border_color {
66 base.settings.border_color = overlay.settings.border_color;
67 }
68 if overlay.settings.background_color != defaults.settings.background_color {
69 base.settings.background_color = overlay.settings.background_color;
70 }
71 if overlay.settings.card_color != defaults.settings.card_color {
72 base.settings.card_color = overlay.settings.card_color;
73 }
74 if overlay.settings.text_color != defaults.settings.text_color {
75 base.settings.text_color = overlay.settings.text_color;
76 }
77 if overlay.settings.hint_color != defaults.settings.hint_color {
78 base.settings.hint_color = overlay.settings.hint_color;
79 }
80 if overlay.settings.hint_matched_color != defaults.settings.hint_matched_color {
81 base.settings.hint_matched_color = overlay.settings.hint_matched_color;
82 }
83 if !overlay.settings.env_files.is_empty() {
84 base.settings.env_files = overlay.settings.env_files;
85 }
86
87 for (key, binding) in overlay.keys {
89 base.keys.insert(key, binding);
90 }
91}
92
93fn merge_from_content(base: &mut Config, content: &str, source: &str) -> Result<()> {
95 let overlay: Config = toml::from_str(content)?;
96 deep_merge(base, overlay);
97 tracing::debug!("Merged config from {}", source);
98 Ok(())
99}
100
101fn merge_config_file(base: &mut Config, path: &Path) -> Result<bool> {
103 if !path.exists() {
104 return Ok(false);
105 }
106
107 let content = std::fs::read_to_string(path).map_err(|source| Error::ConfigRead {
108 path: path.to_path_buf(),
109 source,
110 })?;
111
112 merge_from_content(base, &content, &path.display().to_string())?;
113 Ok(true)
114}
115
116fn read_stdin() -> Result<String> {
118 let mut content = String::new();
119 std::io::stdin().read_to_string(&mut content)?;
120 Ok(content)
121}
122
123pub fn load_config_from_paths(paths: &[String]) -> Result<Config> {
128 let mut config = Config::default();
129
130 for path in paths {
131 if path == "-" {
132 let content = read_stdin()?;
133 merge_from_content(&mut config, &content, "stdin")?;
134 tracing::info!("Loaded config from stdin");
135 } else {
136 let path = PathBuf::from(path);
137 if !path.exists() {
138 return Err(Error::ConfigRead {
139 path: path.clone(),
140 source: std::io::Error::new(
141 std::io::ErrorKind::NotFound,
142 "Config file not found",
143 ),
144 });
145 }
146
147 let canonical = path.canonicalize().map_err(|source| Error::ConfigRead {
149 path: path.clone(),
150 source,
151 })?;
152
153 if !canonical.is_file() {
155 return Err(Error::ConfigRead {
156 path: canonical,
157 source: std::io::Error::new(
158 std::io::ErrorKind::InvalidInput,
159 "Path is not a regular file",
160 ),
161 });
162 }
163
164 if merge_config_file(&mut config, &canonical)? {
165 tracing::info!("Loaded config from {:?}", canonical);
166 }
167 }
168 }
169
170 Ok(config)
171}
172
173pub fn load_config() -> Result<Config> {
180 let mut config = Config::default();
181 let mut loaded_any = false;
182
183 let system_path = system_config_dir().join("config.toml");
185 if merge_config_file(&mut config, &system_path)? {
186 loaded_any = true;
187 tracing::info!("Loaded system config: {:?}", system_path);
188 }
189
190 if let Some(user_path) = user_config_path()
192 && merge_config_file(&mut config, &user_path)?
193 {
194 loaded_any = true;
195 tracing::info!("Loaded user config: {:?}", user_path);
196 }
197
198 if let Some(config_d) = user_config_d_path()
200 && config_d.exists()
201 && config_d.is_dir()
202 {
203 let mut entries: Vec<_> = std::fs::read_dir(&config_d)
204 .map_err(|source| Error::ConfigRead {
205 path: config_d.clone(),
206 source,
207 })?
208 .filter_map(|e| e.ok())
209 .filter(|e| {
210 e.path()
211 .extension()
212 .map(|ext| ext == "toml")
213 .unwrap_or(false)
214 })
215 .collect();
216
217 entries.sort_by_key(|e| e.path());
218
219 for entry in entries {
220 let path = entry.path();
221 if merge_config_file(&mut config, &path)? {
222 loaded_any = true;
223 tracing::info!("Loaded config.d: {:?}", path);
224 }
225 }
226 }
227
228 if !loaded_any {
229 tracing::debug!("No config files found, using defaults");
230 }
231
232 Ok(config)
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_load_default_config() {
241 let config = load_config().unwrap();
243 assert!(!config.keys.is_empty());
244 }
245
246 #[test]
247 fn test_user_config_paths() {
248 if let Some(dir) = user_config_dir() {
250 assert!(dir.to_string_lossy().contains("open-sesame"));
251 }
252
253 if let Some(path) = user_config_path() {
254 assert!(path.to_string_lossy().contains("config.toml"));
255 }
256 }
257}