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}
68
69impl Default for AiConfig {
70 fn default() -> Self {
71 Self {
72 provider: "openrouter".to_string(),
73 model: "mistralai/devstral-2512:free".to_string(),
74 timeout_seconds: 30,
75 allow_paid_models: false,
76 }
77 }
78}
79
80#[derive(Debug, Deserialize)]
82#[serde(default)]
83pub struct GitHubConfig {
84 pub api_timeout_seconds: u64,
86}
87
88impl Default for GitHubConfig {
89 fn default() -> Self {
90 Self {
91 api_timeout_seconds: 10,
92 }
93 }
94}
95
96#[derive(Debug, Deserialize)]
98#[serde(default)]
99pub struct UiConfig {
100 pub color: bool,
102 pub progress_bars: bool,
104 pub confirm_before_post: bool,
106}
107
108impl Default for UiConfig {
109 fn default() -> Self {
110 Self {
111 color: true,
112 progress_bars: true,
113 confirm_before_post: true,
114 }
115 }
116}
117
118#[derive(Debug, Deserialize)]
120#[serde(default)]
121pub struct CacheConfig {
122 pub issue_ttl_minutes: u64,
124 pub repo_ttl_hours: u64,
126}
127
128impl Default for CacheConfig {
129 fn default() -> Self {
130 Self {
131 issue_ttl_minutes: 60,
132 repo_ttl_hours: 24,
133 }
134 }
135}
136
137#[must_use]
143pub fn config_dir() -> PathBuf {
144 dirs::config_dir()
145 .expect("Could not determine config directory - is HOME set?")
146 .join("aptu")
147}
148
149#[must_use]
155pub fn data_dir() -> PathBuf {
156 dirs::data_dir()
157 .expect("Could not determine data directory - is HOME set?")
158 .join("aptu")
159}
160
161#[must_use]
163pub fn config_file_path() -> PathBuf {
164 config_dir().join("config.toml")
165}
166
167pub fn load_config() -> Result<AppConfig, AptuError> {
177 let config_path = config_file_path();
178
179 let config = Config::builder()
180 .add_source(File::with_name(config_path.to_string_lossy().as_ref()).required(false))
182 .add_source(
184 Environment::with_prefix("APTU")
185 .separator("__")
186 .try_parsing(true),
187 )
188 .build()?;
189
190 let app_config: AppConfig = config.try_deserialize()?;
191
192 Ok(app_config)
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn test_load_config_defaults() {
201 let config = load_config().expect("should load with defaults");
203
204 assert_eq!(config.ai.provider, "openrouter");
205 assert_eq!(config.ai.model, "mistralai/devstral-2512:free");
206 assert_eq!(config.ai.timeout_seconds, 30);
207 assert_eq!(config.github.api_timeout_seconds, 10);
208 assert!(config.ui.color);
209 assert!(config.ui.confirm_before_post);
210 assert_eq!(config.cache.issue_ttl_minutes, 60);
211 }
212
213 #[test]
214 fn test_config_dir_exists() {
215 let dir = config_dir();
216 assert!(dir.ends_with("aptu"));
217 }
218
219 #[test]
220 fn test_data_dir_exists() {
221 let dir = data_dir();
222 assert!(dir.ends_with("aptu"));
223 }
224
225 #[test]
226 fn test_config_file_path() {
227 let path = config_file_path();
228 assert!(path.ends_with("config.toml"));
229 }
230}