aimx/
config.rs

1//! Application configuration for AIMX.
2//!
3//! Provides a lazily loaded, process-wide [`Config`] singleton, including
4//! workspace paths and model provider settings, stored as TOML in a
5//! platform-specific directory.
6
7use 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
14/// Application name used for configuration paths.
15///
16/// Set once via [`AppName::set`] before accessing configuration. Defaults to
17/// "aimx" if not overridden.
18pub struct AppName;
19
20impl AppName {
21    /// Set the application name for configuration resolution.
22    ///
23    /// Must be called before any configuration access.
24    ///
25    /// # Panics
26    ///
27    /// Panics if called more than once.
28    #[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    /// Get the effective application name.
36    ///
37    /// Returns the runtime override if set, otherwise "aimx".
38    pub fn get() -> &'static str {
39        if let Some(name) = APP_NAME.get() {
40            return name;
41        }
42        "aimx"
43    }
44}
45
46/// Shared static storage for the application name.
47static APP_NAME: OnceLock<&'static str> = OnceLock::new();
48
49/// Persistent configuration for the process.
50///
51/// Stored as TOML at a platform-specific location derived from [`AppName`].
52#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
53pub struct Config {
54    /// Workspace root path.
55    pub workspace_path: String,
56    /// Last opened path.
57    pub last_path: String,
58    /// Theme
59    pub theme_preference: String,
60    /// Theme Mode (light|dark|auto)
61    pub theme_mode: String,
62
63    /// Provider for fast inference tasks.
64    pub fast: Provider,
65    /// Provider for standard inference tasks.
66    pub standard: Provider,
67    /// Provider for advanced reasoning tasks.
68    pub thinking: Provider,
69    /// Provider for extraction / large-context tasks.
70    pub extract: Provider,
71    /// Provider for instruction-following tasks.
72    pub instruct: Provider,
73    /// Provider for coding and AIM formula tasks.
74    pub coder: Provider,
75}
76
77impl Default for Config {
78    /// Default configuration used when no config file exists.
79    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    /// Returns a reference to the global configuration singleton instance.
163    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    /// Acquire a read guard for the global config.
173    #[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    /// Acquire a write guard for the global config.
182    #[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    /// Load existing configuration or create and persist defaults.
191    #[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    /// Load configuration from disk.
204    #[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    /// Persist configuration to disk.
213    #[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    /// Platform-specific configuration file path.
226    #[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    /// Provider configuration for the requested model class.
235    #[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
248/// Read access to the global config singleton.
249pub fn get_config() -> Result<RwLockReadGuard<'static, Config>, Box<dyn Error>> {
250    Config::read_lock()
251}
252
253/// Write access to the global config singleton.
254pub fn get_config_mut() -> Result<RwLockWriteGuard<'static, Config>, Box<dyn Error>> {
255    Config::write_lock()
256}