1#![allow(dead_code)]
7use std::path::PathBuf;
25
26use config::{Config, Environment, File};
27use serde::Deserialize;
28
29use crate::error::AptuError;
30
31#[derive(Debug, Default, Deserialize)]
33#[serde(default)]
34pub struct AppConfig {
35 pub user: UserConfig,
37 pub ai: AiConfig,
39 pub github: GitHubConfig,
41 pub ui: UiConfig,
43 pub cache: CacheConfig,
45}
46
47#[derive(Debug, Deserialize, Default)]
49#[serde(default)]
50pub struct UserConfig {
51 pub default_repo: Option<String>,
53}
54
55#[derive(Debug, Deserialize)]
57#[serde(default)]
58pub struct AiConfig {
59 pub provider: String,
61 pub model: String,
63 pub timeout_seconds: u64,
65 pub allow_paid_models: bool,
67 pub max_tokens: u32,
69 pub temperature: f32,
71}
72
73impl Default for AiConfig {
74 fn default() -> Self {
75 Self {
76 provider: "gemini".to_string(),
77 model: "gemini-3-flash-preview".to_string(),
78 timeout_seconds: 30,
79 allow_paid_models: false,
80 max_tokens: 2048,
81 temperature: 0.3,
82 }
83 }
84}
85
86#[derive(Debug, Deserialize)]
88#[serde(default)]
89pub struct GitHubConfig {
90 pub api_timeout_seconds: u64,
92}
93
94impl Default for GitHubConfig {
95 fn default() -> Self {
96 Self {
97 api_timeout_seconds: 10,
98 }
99 }
100}
101
102#[derive(Debug, Deserialize)]
104#[serde(default)]
105pub struct UiConfig {
106 pub color: bool,
108 pub progress_bars: bool,
110 pub confirm_before_post: bool,
112}
113
114impl Default for UiConfig {
115 fn default() -> Self {
116 Self {
117 color: true,
118 progress_bars: true,
119 confirm_before_post: true,
120 }
121 }
122}
123
124#[derive(Debug, Deserialize)]
126#[serde(default)]
127pub struct CacheConfig {
128 pub issue_ttl_minutes: u64,
130 pub repo_ttl_hours: u64,
132 pub curated_repos_url: String,
134}
135
136impl Default for CacheConfig {
137 fn default() -> Self {
138 Self {
139 issue_ttl_minutes: 60,
140 repo_ttl_hours: 24,
141 curated_repos_url:
142 "https://raw.githubusercontent.com/clouatre-labs/aptu/main/data/curated-repos.json"
143 .to_string(),
144 }
145 }
146}
147
148#[must_use]
154pub fn config_dir() -> PathBuf {
155 dirs::config_dir()
156 .expect("Could not determine config directory - is HOME set?")
157 .join("aptu")
158}
159
160#[must_use]
166pub fn data_dir() -> PathBuf {
167 dirs::data_dir()
168 .expect("Could not determine data directory - is HOME set?")
169 .join("aptu")
170}
171
172#[must_use]
174pub fn config_file_path() -> PathBuf {
175 config_dir().join("config.toml")
176}
177
178pub fn load_config() -> Result<AppConfig, AptuError> {
188 let config_path = config_file_path();
189
190 let config = Config::builder()
191 .add_source(File::with_name(config_path.to_string_lossy().as_ref()).required(false))
193 .add_source(
195 Environment::with_prefix("APTU")
196 .separator("__")
197 .try_parsing(true),
198 )
199 .build()?;
200
201 let app_config: AppConfig = config.try_deserialize()?;
202
203 Ok(app_config)
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_load_config_defaults() {
212 let config = load_config().expect("should load with defaults");
214
215 assert_eq!(config.ai.provider, "gemini");
216 assert_eq!(config.ai.model, "gemini-3-flash-preview");
217 assert_eq!(config.ai.timeout_seconds, 30);
218 assert_eq!(config.ai.max_tokens, 2048);
219 assert_eq!(config.ai.temperature, 0.3);
220 assert_eq!(config.github.api_timeout_seconds, 10);
221 assert!(config.ui.color);
222 assert!(config.ui.confirm_before_post);
223 assert_eq!(config.cache.issue_ttl_minutes, 60);
224 }
225
226 #[test]
227 fn test_config_dir_exists() {
228 let dir = config_dir();
229 assert!(dir.ends_with("aptu"));
230 }
231
232 #[test]
233 fn test_data_dir_exists() {
234 let dir = data_dir();
235 assert!(dir.ends_with("aptu"));
236 }
237
238 #[test]
239 fn test_config_file_path() {
240 let path = config_file_path();
241 assert!(path.ends_with("config.toml"));
242 }
243}