1use crate::inference::{Api, Capability, Model, Provider};
8use directories::ProjectDirs;
9use serde::{Deserialize, Serialize};
10use std::{
11 error::Error, fs, path::PathBuf, sync::{OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard}
12};
13
14pub struct AppName;
19
20impl AppName {
21 #[doc = include_str!("../docs/config/set.md")]
29 pub fn set(name: &'static str) {
30 APP_NAME.set(name).unwrap_or_else(|_| {
31 panic!("App name can only be set once");
32 });
33 }
34
35 pub fn get() -> &'static str {
39 if let Some(name) = APP_NAME.get() {
40 return name;
41 }
42 "aimx"
43 }
44}
45
46static APP_NAME: OnceLock<&'static str> = OnceLock::new();
48
49#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
53pub struct Config {
54 pub workspace_path: String,
56 pub last_path: String,
58 pub theme_preference: String,
60 pub theme_mode: String,
62
63 pub fast: Provider,
65 pub standard: Provider,
67 pub thinking: Provider,
69 pub extract: Provider,
71 pub instruct: Provider,
73 pub coder: Provider,
75}
76
77impl Default for Config {
78 fn default() -> Self {
80 Self {
81 workspace_path: String::new(),
82 last_path: String::new(),
83 theme_preference: "default".to_owned(),
84 theme_mode: "auto".to_owned(),
85 fast: Provider {
86 api: Api::Ollama,
87 url: "http://localhost:11434".to_owned(),
88 key: String::new(),
89 model: "granite4:latest".to_owned(),
90 capability: Capability::Standard,
91 temperature: 0.7,
92 max_tokens: 2048,
93 context_length: 4096,
94 connection_timeout_ms: 30000,
95 request_timeout_ms: 120000,
96 },
97 standard: Provider {
98 api: Api::Ollama,
99 url: "http://localhost:11434".to_owned(),
100 key: String::new(),
101 model: "mistral:latest".to_owned(),
102 capability: Capability::Standard,
103 temperature: 0.7,
104 max_tokens: 2048,
105 context_length: 4096,
106 connection_timeout_ms: 30000,
107 request_timeout_ms: 120000,
108 },
109 thinking: Provider {
110 api: Api::Ollama,
111 url: "http://localhost:11434".to_owned(),
112 key: String::new(),
113 model: "mistral:latest".to_owned(),
114 capability: Capability::Standard,
115 temperature: 0.7,
116 max_tokens: 2048,
117 context_length: 4096,
118 connection_timeout_ms: 30000,
119 request_timeout_ms: 120000,
120 },
121 extract: Provider {
122 api: Api::Ollama,
123 url: "http://localhost:11434".to_owned(),
124 key: String::new(),
125 model: "mistral:latest".to_owned(),
126 capability: Capability::Standard,
127 temperature: 0.7,
128 max_tokens: 2048,
129 context_length: 4096,
130 connection_timeout_ms: 30000,
131 request_timeout_ms: 120000,
132 },
133 instruct: Provider {
134 api: Api::Ollama,
135 url: "http://localhost:11434".to_owned(),
136 key: String::new(),
137 model: "mistral:latest".to_owned(),
138 capability: Capability::Standard,
139 temperature: 0.7,
140 max_tokens: 2048,
141 context_length: 4096,
142 connection_timeout_ms: 30000,
143 request_timeout_ms: 120000,
144 },
145 coder: Provider {
146 api: Api::Ollama,
147 url: "http://localhost:11434".to_owned(),
148 key: String::new(),
149 model: "mistral:latest".to_owned(),
150 capability: Capability::Standard,
151 temperature: 0.7,
152 max_tokens: 2048,
153 context_length: 4096,
154 connection_timeout_ms: 30000,
155 request_timeout_ms: 120000,
156 },
157 }
158 }
159}
160
161impl Config {
162 fn get_instance() -> &'static RwLock<Self> {
164 static INSTANCE: OnceLock<RwLock<Config>> = OnceLock::new();
165
166 INSTANCE.get_or_init(|| {
167 let config = Self::new().unwrap_or_else(|_| Self::default());
168 RwLock::new(config)
169 })
170 }
171
172 #[doc = include_str!("../docs/config/read_lock.md")]
174 pub fn read_lock() -> Result<RwLockReadGuard<'static, Self>, Box<dyn Error>> {
175 Self::get_instance().read().map_err(|_| {
176 Box::<dyn Error>::from("Config lock is poisoned")
177 as Box<dyn Error>
178 })
179 }
180
181 #[doc = include_str!("../docs/config/write_lock.md")]
183 pub fn write_lock() -> Result<RwLockWriteGuard<'static, Self>, Box<dyn Error>> {
184 Self::get_instance().write().map_err(|_| {
185 Box::<dyn Error>::from("Config lock is poisoned")
186 as Box<dyn Error>
187 })
188 }
189
190 #[doc = include_str!("../docs/config/new.md")]
192 pub fn new() -> Result<Self, Box<dyn Error>> {
193 match Self::load() {
194 Ok(config) => Ok(config),
195 Err(_) => {
196 let default_config = Self::default();
197 default_config.save()?;
198 Ok(default_config)
199 }
200 }
201 }
202
203 #[doc = include_str!("../docs/config/load.md")]
205 pub fn load() -> Result<Self, Box<dyn Error>> {
206 let path = Self::path()?;
207 let content = fs::read_to_string(path)?;
208 let config = toml::from_str(&content)?;
209 Ok(config)
210 }
211
212 #[doc = include_str!("../docs/config/save.md")]
214 pub fn save(&self) -> Result<(), Box<dyn Error>> {
215 let path = Self::path()?;
216 if let Some(parent) = path.parent() {
217 fs::create_dir_all(parent)?;
218 }
219
220 let content = toml::to_string_pretty(self)?;
221 fs::write(path, content)?;
222 Ok(())
223 }
224
225 #[doc = include_str!("../docs/config/path.md")]
227 pub fn path() -> Result<PathBuf, Box<dyn Error>> {
228 let app_name: &str = AppName::get();
229 let proj_dirs = ProjectDirs::from("net", "imogen", "aimx")
230 .ok_or("Failed to determine project directories")?;
231 Ok(proj_dirs.config_dir().join(format!("{}.toml", app_name)))
232 }
233
234 #[doc = include_str!("../docs/config/get_provider.md")]
236 pub fn get_provider(&self, model: &Model) -> &Provider {
237 match model {
238 Model::Fast => &self.fast,
239 Model::Standard => &self.standard,
240 Model::Thinking => &self.thinking,
241 Model::Extract => &self.extract,
242 Model::Instruct => &self.instruct,
243 Model::Coder => &self.coder,
244 }
245 }
246}
247
248pub fn get_config() -> Result<RwLockReadGuard<'static, Config>, Box<dyn Error>> {
250 Config::read_lock()
251}
252
253pub fn get_config_mut() -> Result<RwLockWriteGuard<'static, Config>, Box<dyn Error>> {
255 Config::write_lock()
256}