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).ok().flatten().filter(|s| !s.is_empty())
137}
138
139pub fn resolve_usize(flag: Option<usize>, xdg_key: &str, default: usize) -> usize {
141 if let Some(v) = flag {
142 return v;
143 }
144 if let Ok(Some(v)) = config::get_setting(xdg_key) {
145 if let Ok(n) = v.parse::<usize>() {
146 return n;
147 }
148 }
149 default
150}
151
152pub fn resolve_u64(flag: Option<u64>, xdg_key: &str, default: u64) -> u64 {
154 if let Some(v) = flag {
155 return v;
156 }
157 if let Ok(Some(v)) = config::get_setting(xdg_key) {
158 if let Ok(n) = v.parse::<u64>() {
159 return n;
160 }
161 }
162 default
163}
164
165pub fn resolve_f64(flag: Option<f64>, xdg_key: &str, default: f64) -> f64 {
167 if let Some(v) = flag {
168 return v;
169 }
170 if let Ok(Some(v)) = config::get_setting(xdg_key) {
171 if let Ok(n) = v.parse::<f64>() {
172 return n;
173 }
174 }
175 default
176}
177
178pub fn resolve_bool(flag_set: bool, xdg_key: &str, default: bool) -> bool {
180 if flag_set {
181 return true;
182 }
183 if let Ok(Some(v)) = config::get_setting(xdg_key) {
184 let t = v.trim().to_ascii_lowercase();
185 return matches!(t.as_str(), "1" | "true" | "yes" | "on");
186 }
187 default
188}
189
190pub fn embedding_dim_override() -> Option<u32> {
196 let rt = get();
197 if let Some(d) = rt.embedding_dim {
198 return Some(d);
199 }
200 if let Ok(Some(v)) = config::get_setting("embedding.dim") {
201 if let Ok(n) = v.parse::<u32>() {
202 if crate::constants::EMBEDDING_DIM_RANGE.contains(&(n as usize)) {
203 return Some(n);
204 }
205 }
206 }
207 None
208}
209
210pub fn skip_embedding_on_failure() -> bool {
212 let rt = get();
213 resolve_bool(
214 rt.skip_embedding_on_failure,
215 "llm.skip_embedding_on_failure",
216 false,
217 )
218}
219
220pub fn llm_max_host_concurrency(default: usize) -> usize {
222 let rt = get();
223 resolve_usize(
224 rt.llm_max_host_concurrency,
225 "llm.max_host_concurrency",
226 default,
227 )
228}
229
230pub fn llm_slot_wait_secs(default: u64) -> u64 {
232 let rt = get();
233 if rt.llm_slot_no_wait {
234 return 0;
235 }
236 resolve_u64(rt.llm_slot_wait_secs, "llm.slot_wait_secs", default)
237}
238
239pub fn llm_slot_no_wait() -> bool {
241 let rt = get();
242 resolve_bool(rt.llm_slot_no_wait, "llm.slot_no_wait", false)
243}
244
245pub fn claude_binary() -> Option<String> {
247 let rt = get();
248 resolve_optional_string(rt.claude_binary.as_deref(), "llm.claude_binary")
249}
250
251pub fn codex_binary() -> Option<String> {
253 let rt = get();
254 resolve_optional_string(rt.codex_binary.as_deref(), "llm.codex_binary")
255}
256
257pub fn opencode_binary() -> Option<String> {
259 let rt = get();
260 resolve_optional_string(rt.opencode_binary.as_deref(), "llm.opencode_binary")
261}
262
263pub fn llm_model() -> Option<String> {
265 let rt = get();
266 resolve_optional_string(rt.llm_model.as_deref(), "llm.model")
267}
268
269pub fn llm_fallback(default: &str) -> String {
271 let rt = get();
272 resolve_string(rt.llm_fallback.as_deref(), "llm.fallback", default)
273}
274
275pub fn log_level(default: &str) -> String {
277 let rt = get();
278 resolve_string(rt.log_level.as_deref(), "log.level", default)
279}
280
281pub fn log_format(default: &str) -> String {
283 let rt = get();
284 resolve_string(rt.log_format.as_deref(), "log.format", default)
285}
286
287pub fn max_entities_per_memory(default: usize) -> usize {
289 resolve_usize(None, "limits.max_entities_per_memory", default)
290}
291
292pub fn max_relations_per_memory(default: usize) -> usize {
294 resolve_usize(None, "limits.max_relations_per_memory", default)
295}
296
297pub fn openrouter_chat_url(default: &str) -> String {
300 resolve_string_with_aliases(
301 None,
302 &["network.openrouter.chat_url", "network.chat_url"],
303 default,
304 )
305}
306
307pub fn openrouter_embeddings_url(default: &str) -> String {
310 resolve_string_with_aliases(
311 None,
312 &[
313 "network.openrouter.embeddings_url",
314 "network.embed_url",
315 ],
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 { default } else { n }
339}
340
341pub fn db_busy_retries(default: u32) -> u32 {
343 resolve_u64(None, "db.busy_retries", u64::from(default)) as u32
344}
345
346pub fn db_busy_base_delay_ms(default: u64) -> u64 {
348 resolve_u64(None, "db.busy_base_delay_ms", default)
349}
350
351pub fn db_query_timeout_ms(default: u64) -> u64 {
353 resolve_u64(None, "db.query_timeout_ms", default)
354}
355
356pub fn embedding_batch_size(default: usize) -> usize {
361 resolve_usize(None, "embedding.batch_size", default).max(1)
362}
363
364fn resolve_string_with_aliases(flag: Option<&str>, keys: &[&str], default: &str) -> String {
366 if let Some(v) = flag {
367 if !v.is_empty() {
368 return v.to_string();
369 }
370 }
371 for key in keys {
372 if let Ok(Some(v)) = config::get_setting(key) {
373 if !v.is_empty() {
374 return v;
375 }
376 }
377 }
378 default.to_string()
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn resolve_string_prefers_flag() {
387 assert_eq!(
388 resolve_string(Some("from-flag"), "nonexistent.key.xyz", "def"),
389 "from-flag"
390 );
391 }
392
393 #[test]
394 fn resolve_string_falls_to_default() {
395 assert_eq!(
396 resolve_string(None, "nonexistent.key.xyz.zzz", "def"),
397 "def"
398 );
399 }
400}