1use std::path::{Path, PathBuf};
14
15use anyhow::{Context, Result};
16
17fn data_dir() -> Option<PathBuf> {
21 if let Ok(d) = std::env::var("KINTSUGI_DATA_DIR") {
22 return Some(PathBuf::from(d));
23 }
24 directories::ProjectDirs::from("", "", "kintsugi").map(|p| p.data_dir().to_path_buf())
25}
26
27pub fn model_config_path() -> Option<PathBuf> {
29 data_dir().map(|d| d.join("model.path"))
30}
31
32pub fn configured_model() -> Option<PathBuf> {
36 let cfg = model_config_path()?;
37 let raw = std::fs::read_to_string(&cfg).ok()?;
38 let trimmed = raw.trim();
39 if trimmed.is_empty() {
40 return None;
41 }
42 Some(PathBuf::from(trimmed))
43}
44
45pub fn set_configured_model(path: &Path) -> Result<()> {
47 let cfg = model_config_path().context("could not resolve the Kintsugi data dir")?;
48 if let Some(parent) = cfg.parent() {
49 std::fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
50 }
51 std::fs::write(&cfg, format!("{}\n", path.display()))
52 .with_context(|| format!("write {}", cfg.display()))?;
53 Ok(())
54}
55
56pub fn clear_configured_model() -> Result<()> {
58 if let Some(cfg) = model_config_path() {
59 if cfg.exists() {
60 std::fs::remove_file(&cfg).with_context(|| format!("remove {}", cfg.display()))?;
61 }
62 }
63 Ok(())
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 fn with_data_dir<T>(f: impl FnOnce(&Path) -> T) -> T {
74 use std::sync::{Mutex, OnceLock};
75 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
76 let _g = LOCK.get_or_init(|| Mutex::new(())).lock().unwrap();
77 let tmp = tempfile::tempdir().unwrap();
78 std::env::set_var("KINTSUGI_DATA_DIR", tmp.path());
79 let out = f(tmp.path());
80 std::env::remove_var("KINTSUGI_DATA_DIR");
81 out
82 }
83
84 #[test]
85 fn round_trips_a_selection() {
86 with_data_dir(|_| {
87 assert!(configured_model().is_none(), "starts empty");
88 let model = PathBuf::from("/models/qwen.gguf");
89 set_configured_model(&model).unwrap();
90 assert_eq!(configured_model(), Some(model));
91 });
92 }
93
94 #[test]
95 fn clear_removes_the_selection() {
96 with_data_dir(|_| {
97 set_configured_model(Path::new("/models/a.gguf")).unwrap();
98 clear_configured_model().unwrap();
99 assert!(configured_model().is_none());
100 clear_configured_model().unwrap();
102 });
103 }
104
105 #[test]
106 fn blank_or_whitespace_file_reads_as_unset() {
107 with_data_dir(|_| {
108 let cfg = model_config_path().unwrap();
109 std::fs::create_dir_all(cfg.parent().unwrap()).unwrap();
110 std::fs::write(&cfg, " \n").unwrap();
111 assert!(configured_model().is_none());
112 });
113 }
114
115 #[test]
116 fn set_creates_the_data_dir() {
117 with_data_dir(|root| {
118 let nested = root.join("nested/deeper");
120 std::env::set_var("KINTSUGI_DATA_DIR", &nested);
121 set_configured_model(Path::new("/m/x.gguf")).unwrap();
122 assert!(nested.join("model.path").is_file());
123 });
124 }
125}