1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Config {
8 pub default_provider: String,
9 pub default_model: String,
10 pub theme: ThemeConfig,
11 #[serde(default)]
12 pub context: ContextConfig,
13 #[serde(default)]
14 pub mcp: HashMap<String, McpServerConfig>,
15 #[serde(default)]
16 pub agents: HashMap<String, AgentConfig>,
17 #[serde(default)]
18 pub tui: TuiConfig,
19 #[serde(default)]
20 pub permissions: HashMap<String, String>,
21 #[serde(default)]
22 pub providers: HashMap<String, ProviderDefinition>,
23 #[serde(default)]
24 pub custom_tools: HashMap<String, CustomToolConfig>,
25 #[serde(default)]
26 pub commands: HashMap<String, CommandConfig>,
27 #[serde(default)]
28 pub hooks: HashMap<String, HookConfig>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct ContextConfig {
33 #[serde(default = "default_true")]
34 pub auto_load_global: bool,
35 #[serde(default = "default_true")]
36 pub auto_load_project: bool,
37}
38impl Default for ContextConfig {
39 fn default() -> Self {
40 Self {
41 auto_load_global: true,
42 auto_load_project: true,
43 }
44 }
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct ThemeConfig {
49 pub name: String,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct McpServerConfig {
54 #[serde(default)]
55 pub command: Vec<String>,
56 pub url: Option<String>,
57 #[serde(default = "default_true")]
58 pub enabled: bool,
59 #[serde(default)]
60 pub env: HashMap<String, String>,
61 #[serde(default = "default_timeout")]
62 pub timeout: u64,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct AgentConfig {
67 pub description: String,
68 pub model: Option<String>,
69 pub system_prompt: Option<String>,
70 #[serde(default)]
71 pub tools: HashMap<String, bool>,
72 #[serde(default = "default_true")]
73 pub enabled: bool,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct TuiConfig {
78 #[serde(default = "default_true")]
79 pub vim_mode: bool,
80 #[serde(default)]
81 pub favorite_models: Vec<String>,
82}
83
84impl Default for TuiConfig {
85 fn default() -> Self {
86 Self {
87 vim_mode: true,
88 favorite_models: Vec::new(),
89 }
90 }
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ProviderDefinition {
95 pub api: String,
96 pub base_url: Option<String>,
97 #[serde(default)]
98 pub api_key_env: Option<String>,
99 #[serde(default)]
100 pub models: Vec<String>,
101 pub default_model: Option<String>,
102 #[serde(default = "default_true")]
103 pub enabled: bool,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct CustomToolConfig {
108 pub description: String,
109 pub command: String,
110 #[serde(default = "default_schema")]
111 pub schema: serde_json::Value,
112 #[serde(default = "default_timeout")]
113 pub timeout: u64,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct CommandConfig {
118 pub description: String,
119 pub command: String,
120 #[serde(default = "default_timeout")]
121 pub timeout: u64,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct HookConfig {
126 pub command: String,
127 #[serde(default = "default_timeout")]
128 pub timeout: u64,
129}
130
131fn default_true() -> bool {
132 true
133}
134
135fn default_timeout() -> u64 {
136 30
137}
138
139fn default_schema() -> serde_json::Value {
140 serde_json::json!({
141 "type": "object",
142 "properties": {},
143 "required": []
144 })
145}
146
147impl Default for Config {
148 fn default() -> Self {
149 Self {
150 default_provider: "anthropic".to_string(),
151 default_model: "claude-sonnet-4-20250514".to_string(),
152 theme: ThemeConfig {
153 name: "terminal".to_string(),
154 },
155 context: ContextConfig::default(),
156 mcp: HashMap::new(),
157 agents: HashMap::new(),
158 tui: TuiConfig::default(),
159 permissions: HashMap::new(),
160 providers: HashMap::new(),
161 custom_tools: HashMap::new(),
162 commands: HashMap::new(),
163 hooks: HashMap::new(),
164 }
165 }
166}
167
168impl Config {
169 pub fn config_dir() -> PathBuf {
170 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME")
171 && !xdg.is_empty()
172 {
173 return PathBuf::from(xdg).join("dot");
174 }
175 #[cfg(unix)]
176 return dirs::home_dir()
177 .unwrap_or_else(|| PathBuf::from("."))
178 .join(".config")
179 .join("dot");
180 #[cfg(not(unix))]
181 dirs::config_dir()
182 .unwrap_or_else(|| PathBuf::from("."))
183 .join("dot")
184 }
185
186 pub fn config_path() -> PathBuf {
187 Self::config_dir().join("config.toml")
188 }
189
190 pub fn data_dir() -> PathBuf {
191 if let Ok(xdg) = std::env::var("XDG_DATA_HOME")
192 && !xdg.is_empty()
193 {
194 return PathBuf::from(xdg).join("dot");
195 }
196 #[cfg(unix)]
197 return dirs::home_dir()
198 .unwrap_or_else(|| PathBuf::from("."))
199 .join(".local")
200 .join("share")
201 .join("dot");
202 #[cfg(not(unix))]
203 dirs::data_local_dir()
204 .unwrap_or_else(|| PathBuf::from("."))
205 .join("dot")
206 }
207
208 pub fn db_path() -> PathBuf {
209 Self::data_dir().join("dot.db")
210 }
211
212 pub fn load() -> Result<Self> {
213 let path = Self::config_path();
214 if path.exists() {
215 let content = std::fs::read_to_string(&path)
216 .with_context(|| format!("reading config from {}", path.display()))?;
217 toml::from_str(&content).context("parsing config.toml")
218 } else {
219 let config = Self::default();
220 config.save()?;
221 Ok(config)
222 }
223 }
224
225 pub fn save(&self) -> Result<()> {
226 let dir = Self::config_dir();
227 std::fs::create_dir_all(&dir)
228 .with_context(|| format!("creating config dir {}", dir.display()))?;
229 let content = toml::to_string_pretty(self).context("serializing config")?;
230 std::fs::write(Self::config_path(), content).context("writing config.toml")
231 }
232
233 pub fn ensure_dirs() -> Result<()> {
234 std::fs::create_dir_all(Self::config_dir()).context("creating config directory")?;
235 std::fs::create_dir_all(Self::data_dir()).context("creating data directory")?;
236 Ok(())
237 }
238
239 pub fn enabled_mcp_servers(&self) -> Vec<(&str, &McpServerConfig)> {
240 self.mcp
241 .iter()
242 .filter(|(_, cfg)| cfg.enabled && !cfg.command.is_empty())
243 .map(|(name, cfg)| (name.as_str(), cfg))
244 .collect()
245 }
246
247 pub fn enabled_agents(&self) -> Vec<(&str, &AgentConfig)> {
248 self.agents
249 .iter()
250 .filter(|(_, cfg)| cfg.enabled)
251 .map(|(name, cfg)| (name.as_str(), cfg))
252 .collect()
253 }
254
255 pub fn parse_model_spec(spec: &str) -> (Option<&str>, &str) {
258 if let Some((provider, model)) = spec.split_once('/') {
259 (Some(provider), model)
260 } else {
261 (None, spec)
262 }
263 }
264}