1use crate::config;
8use std::sync::OnceLock;
9
10#[derive(Debug, Clone, Default)]
12pub struct RuntimeOverrides {
13 pub embedding_dim: Option<u32>,
15 pub claude_binary: Option<String>,
17 pub codex_binary: Option<String>,
19 pub opencode_binary: Option<String>, pub llm_model: Option<String>,
23 pub llm_fallback: Option<String>,
25 pub skip_embedding_on_failure: bool,
27 pub llm_max_host_concurrency: Option<usize>,
29 pub llm_slot_wait_secs: Option<u64>,
31 pub llm_slot_no_wait: bool,
33 pub strict_env_clear: bool,
35 pub log_level: Option<String>,
37 pub log_format: Option<String>,
39 pub lang: Option<String>,
41 pub display_tz: Option<String>,
43 pub db_path: Option<String>,
45}
46
47#[derive(Debug, Clone, Default)]
56pub struct PathOverrides {
57 pub config_dir: Option<String>,
59 pub cache_dir: Option<String>,
61}
62
63static PATHS: OnceLock<PathOverrides> = OnceLock::new();
64
65pub fn init_paths(overrides: PathOverrides) {
69 let _ = PATHS.set(overrides);
70}
71
72fn paths() -> PathOverrides {
73 PATHS.get().cloned().unwrap_or_default()
74}
75
76static RUNTIME: OnceLock<RuntimeOverrides> = OnceLock::new();
77
78pub fn init(overrides: RuntimeOverrides) {
80 let _ = RUNTIME.set(overrides);
81}
82
83pub fn get() -> RuntimeOverrides {
85 RUNTIME.get().cloned().unwrap_or_default()
86}
87
88pub fn config_dir_override() -> Option<String> {
94 paths()
95 .config_dir
96 .map(|s| s.trim().to_string())
97 .filter(|s| !s.is_empty())
98}
99
100pub fn cache_dir_override() -> Option<String> {
102 if let Some(v) = paths().cache_dir {
103 if !v.trim().is_empty() {
104 return Some(v.trim().to_string());
105 }
106 }
107 config::get_setting("cache.dir")
108 .ok()
109 .flatten()
110 .map(|s| s.trim().to_string())
111 .filter(|s| !s.is_empty())
112}
113
114pub fn resolve_string(flag: Option<&str>, xdg_key: &str, default: &str) -> String {
116 if let Some(v) = flag {
117 if !v.is_empty() {
118 return v.to_string();
119 }
120 }
121 if let Ok(Some(v)) = config::get_setting(xdg_key) {
122 if !v.is_empty() {
123 return v;
124 }
125 }
126 default.to_string()
127}
128
129pub fn resolve_optional_string(flag: Option<&str>, xdg_key: &str) -> Option<String> {
131 if let Some(v) = flag {
132 if !v.is_empty() {
133 return Some(v.to_string());
134 }
135 }
136 config::get_setting(xdg_key)
137 .ok()
138 .flatten()
139 .filter(|s| !s.is_empty())
140}
141
142pub fn resolve_usize(flag: Option<usize>, xdg_key: &str, default: usize) -> usize {
144 if let Some(v) = flag {
145 return v;
146 }
147 if let Ok(Some(v)) = config::get_setting(xdg_key) {
148 if let Ok(n) = v.parse::<usize>() {
149 return n;
150 }
151 }
152 default
153}
154
155pub fn resolve_u64(flag: Option<u64>, xdg_key: &str, default: u64) -> u64 {
157 if let Some(v) = flag {
158 return v;
159 }
160 if let Ok(Some(v)) = config::get_setting(xdg_key) {
161 if let Ok(n) = v.parse::<u64>() {
162 return n;
163 }
164 }
165 default
166}
167
168pub fn resolve_f64(flag: Option<f64>, xdg_key: &str, default: f64) -> f64 {
170 if let Some(v) = flag {
171 return v;
172 }
173 if let Ok(Some(v)) = config::get_setting(xdg_key) {
174 if let Ok(n) = v.parse::<f64>() {
175 return n;
176 }
177 }
178 default
179}
180
181pub fn resolve_bool(flag_set: bool, xdg_key: &str, default: bool) -> bool {
183 if flag_set {
184 return true;
185 }
186 if let Ok(Some(v)) = config::get_setting(xdg_key) {
187 let t = v.trim().to_ascii_lowercase();
188 return matches!(t.as_str(), "1" | "true" | "yes" | "on");
189 }
190 default
191}
192
193pub fn embedding_dim_override() -> Option<u32> {
199 let rt = get();
200 if let Some(d) = rt.embedding_dim {
201 return Some(d);
202 }
203 if let Ok(Some(v)) = config::get_setting("embedding.dim") {
204 if let Ok(n) = v.parse::<u32>() {
205 if crate::constants::EMBEDDING_DIM_RANGE.contains(&(n as usize)) {
206 return Some(n);
207 }
208 }
209 }
210 None
211}
212
213pub fn skip_embedding_on_failure() -> bool {
215 let rt = get();
216 resolve_bool(
217 rt.skip_embedding_on_failure,
218 "llm.skip_embedding_on_failure",
219 false,
220 )
221}
222
223pub fn llm_max_host_concurrency(default: usize) -> usize {
225 let rt = get();
226 resolve_usize(
227 rt.llm_max_host_concurrency,
228 "llm.max_host_concurrency",
229 default,
230 )
231}
232
233pub fn llm_slot_wait_secs(default: u64) -> u64 {
235 let rt = get();
236 if rt.llm_slot_no_wait {
237 return 0;
238 }
239 resolve_u64(rt.llm_slot_wait_secs, "llm.slot_wait_secs", default)
240}
241
242pub fn llm_slot_no_wait() -> bool {
244 let rt = get();
245 resolve_bool(rt.llm_slot_no_wait, "llm.slot_no_wait", false)
246}
247
248pub fn claude_binary() -> Option<String> {
250 let rt = get();
251 resolve_optional_string(rt.claude_binary.as_deref(), "llm.claude_binary")
252}
253
254pub fn codex_binary() -> Option<String> {
256 let rt = get();
257 resolve_optional_string(rt.codex_binary.as_deref(), "llm.codex_binary")
258}
259
260pub fn opencode_binary() -> Option<String> {
262 let rt = get();
263 resolve_optional_string(rt.opencode_binary.as_deref(), "llm.opencode_binary")
264}
265
266pub fn llm_model() -> Option<String> {
268 let rt = get();
269 resolve_optional_string(rt.llm_model.as_deref(), "llm.model")
270}
271
272pub fn llm_fallback(default: &str) -> String {
274 let rt = get();
275 resolve_string(rt.llm_fallback.as_deref(), "llm.fallback", default)
276}
277
278pub fn log_level(default: &str) -> String {
280 let rt = get();
281 resolve_string(rt.log_level.as_deref(), "log.level", default)
282}
283
284pub fn log_format(default: &str) -> String {
286 let rt = get();
287 resolve_string(rt.log_format.as_deref(), "log.format", default)
288}
289
290pub fn max_entities_per_memory(default: usize) -> usize {
292 resolve_usize(None, "limits.max_entities_per_memory", default)
293}
294
295pub fn max_relations_per_memory(default: usize) -> usize {
297 resolve_usize(None, "limits.max_relations_per_memory", default)
298}
299
300pub fn openrouter_chat_url(default: &str) -> String {
303 resolve_string_with_aliases(
304 None,
305 &["network.openrouter.chat_url", "network.chat_url"],
306 default,
307 )
308}
309
310pub fn openrouter_embeddings_url(default: &str) -> String {
313 resolve_string_with_aliases(
314 None,
315 &["network.openrouter.embeddings_url", "network.embed_url"],
316 default,
317 )
318}
319
320pub fn llm_probe_timeout_ms(default: u64) -> u64 {
322 resolve_u64(None, "llm.probe_timeout_ms", default)
323}
324
325pub fn rayon_threads(default: usize) -> usize {
337 let n = resolve_usize(None, "parallelism.rayon_threads", default);
338 if n == 0 {
339 default
340 } else {
341 n
342 }
343}
344
345pub fn db_busy_retries(default: u32) -> u32 {
347 resolve_u64(None, "db.busy_retries", u64::from(default)) as u32
348}
349
350pub fn db_busy_base_delay_ms(default: u64) -> u64 {
352 resolve_u64(None, "db.busy_base_delay_ms", default)
353}
354
355pub fn db_query_timeout_ms(default: u64) -> u64 {
357 resolve_u64(None, "db.query_timeout_ms", default)
358}
359
360pub fn embedding_batch_size(default: usize) -> usize {
365 resolve_usize(None, "embedding.batch_size", default).max(1)
366}
367
368fn resolve_string_with_aliases(flag: Option<&str>, keys: &[&str], default: &str) -> String {
370 if let Some(v) = flag {
371 if !v.is_empty() {
372 return v.to_string();
373 }
374 }
375 for key in keys {
376 if let Ok(Some(v)) = config::get_setting(key) {
377 if !v.is_empty() {
378 return v;
379 }
380 }
381 }
382 default.to_string()
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn resolve_string_prefers_flag() {
391 assert_eq!(
392 resolve_string(Some("from-flag"), "nonexistent.key.xyz", "def"),
393 "from-flag"
394 );
395 }
396
397 #[test]
398 fn resolve_string_falls_to_default() {
399 assert_eq!(
400 resolve_string(None, "nonexistent.key.xyz.zzz", "def"),
401 "def"
402 );
403 }
404}