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 acp_agents: HashMap<String, AcpAgentConfig>,
15 #[serde(default)]
16 pub mcp: HashMap<String, McpServerConfig>,
17 #[serde(default)]
18 pub agents: HashMap<String, AgentConfig>,
19 #[serde(default)]
20 pub tui: TuiConfig,
21 #[serde(default)]
22 pub permissions: HashMap<String, String>,
23 #[serde(default)]
24 pub providers: HashMap<String, ProviderDefinition>,
25 #[serde(default)]
26 pub custom_tools: HashMap<String, CustomToolConfig>,
27 #[serde(default)]
28 pub commands: HashMap<String, CommandConfig>,
29 #[serde(default)]
30 pub hooks: HashMap<String, HookConfig>,
31 #[serde(default)]
32 pub subagents: SubagentSettings,
33 #[serde(default)]
34 pub memory: MemoryConfig,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ContextConfig {
39 #[serde(default = "default_true")]
40 pub auto_load_global: bool,
41 #[serde(default = "default_true")]
42 pub auto_load_project: bool,
43}
44impl Default for ContextConfig {
45 fn default() -> Self {
46 Self {
47 auto_load_global: true,
48 auto_load_project: true,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ThemeConfig {
55 pub name: String,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, Default)]
59pub struct AcpAgentConfig {
60 #[serde(default)]
61 pub command: Vec<String>,
62 #[serde(default)]
63 pub env: HashMap<String, String>,
64 #[serde(default = "default_true")]
65 pub enabled: bool,
66 #[serde(default)]
67 pub description: String,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct McpServerConfig {
72 #[serde(default)]
73 pub command: Vec<String>,
74 pub url: Option<String>,
75 #[serde(default = "default_true")]
76 pub enabled: bool,
77 #[serde(default)]
78 pub env: HashMap<String, String>,
79 #[serde(default = "default_timeout")]
80 pub timeout: u64,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct AgentConfig {
85 pub description: String,
86 pub model: Option<String>,
87 pub system_prompt: Option<String>,
88 #[serde(default)]
89 pub tools: HashMap<String, bool>,
90 #[serde(default = "default_true")]
91 pub enabled: bool,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, Default)]
95#[serde(rename_all = "snake_case")]
96pub enum CursorShape {
97 #[default]
98 Block,
99 Underline,
100 Line,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct TuiConfig {
105 #[serde(default = "default_true")]
106 pub vim_mode: bool,
107 #[serde(default)]
108 pub favorite_models: Vec<String>,
109 #[serde(default)]
110 pub cursor_shape: CursorShape,
111 #[serde(default)]
112 pub cursor_shape_normal: Option<CursorShape>,
113 #[serde(default = "default_true")]
114 pub cursor_blink: bool,
115 #[serde(default)]
116 pub cursor_blink_normal: Option<bool>,
117}
118
119impl Default for TuiConfig {
120 fn default() -> Self {
121 Self {
122 vim_mode: true,
123 favorite_models: Vec::new(),
124 cursor_shape: CursorShape::default(),
125 cursor_shape_normal: None,
126 cursor_blink: true,
127 cursor_blink_normal: None,
128 }
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct ProviderDefinition {
134 pub api: String,
135 pub base_url: Option<String>,
136 #[serde(default)]
137 pub api_key_env: Option<String>,
138 #[serde(default)]
139 pub models: Vec<String>,
140 pub default_model: Option<String>,
141 #[serde(default = "default_true")]
142 pub enabled: bool,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct CustomToolConfig {
147 pub description: String,
148 pub command: String,
149 #[serde(default = "default_schema")]
150 pub schema: serde_json::Value,
151 #[serde(default = "default_timeout")]
152 pub timeout: u64,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct CommandConfig {
157 pub description: String,
158 pub command: String,
159 #[serde(default = "default_timeout")]
160 pub timeout: u64,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct HookConfig {
165 pub command: String,
166 #[serde(default = "default_timeout")]
167 pub timeout: u64,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct SubagentSettings {
172 #[serde(default = "default_true")]
173 pub enabled: bool,
174 #[serde(default = "default_max_subagent_turns")]
175 pub max_turns: usize,
176}
177
178impl Default for SubagentSettings {
179 fn default() -> Self {
180 Self {
181 enabled: true,
182 max_turns: 20,
183 }
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct MemoryConfig {
189 #[serde(default = "default_true")]
190 pub enabled: bool,
191 #[serde(default = "default_true")]
192 pub auto_extract: bool,
193 #[serde(default = "default_inject_count")]
194 pub inject_count: usize,
195 #[serde(default = "default_max_memories")]
196 pub max_memories: usize,
197}
198
199impl Default for MemoryConfig {
200 fn default() -> Self {
201 Self {
202 enabled: true,
203 auto_extract: true,
204 inject_count: 15,
205 max_memories: 2000,
206 }
207 }
208}
209
210fn default_inject_count() -> usize {
211 15
212}
213
214fn default_max_memories() -> usize {
215 2000
216}
217
218fn default_max_subagent_turns() -> usize {
219 20
220}
221
222fn default_true() -> bool {
223 true
224}
225
226fn default_timeout() -> u64 {
227 30
228}
229
230fn default_schema() -> serde_json::Value {
231 serde_json::json!({
232 "type": "object",
233 "properties": {},
234 "required": []
235 })
236}
237
238impl Default for Config {
239 fn default() -> Self {
240 Self {
241 default_provider: "anthropic".to_string(),
242 default_model: "claude-sonnet-4-20250514".to_string(),
243 theme: ThemeConfig {
244 name: "terminal".to_string(),
245 },
246 context: ContextConfig::default(),
247 acp_agents: HashMap::new(),
248 mcp: HashMap::new(),
249 agents: HashMap::new(),
250 tui: TuiConfig::default(),
251 permissions: HashMap::new(),
252 providers: HashMap::new(),
253 custom_tools: HashMap::new(),
254 commands: HashMap::new(),
255 hooks: HashMap::new(),
256 subagents: SubagentSettings::default(),
257 memory: MemoryConfig::default(),
258 }
259 }
260}
261
262impl Config {
263 pub fn config_dir() -> PathBuf {
264 if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME")
265 && !xdg.is_empty()
266 {
267 return PathBuf::from(xdg).join("dot");
268 }
269 #[cfg(unix)]
270 return dirs::home_dir()
271 .unwrap_or_else(|| PathBuf::from("."))
272 .join(".config")
273 .join("dot");
274 #[cfg(not(unix))]
275 dirs::config_dir()
276 .unwrap_or_else(|| PathBuf::from("."))
277 .join("dot")
278 }
279
280 pub fn config_path() -> PathBuf {
281 Self::config_dir().join("config.toml")
282 }
283
284 pub fn data_dir() -> PathBuf {
285 if let Ok(xdg) = std::env::var("XDG_DATA_HOME")
286 && !xdg.is_empty()
287 {
288 return PathBuf::from(xdg).join("dot");
289 }
290 #[cfg(unix)]
291 return dirs::home_dir()
292 .unwrap_or_else(|| PathBuf::from("."))
293 .join(".local")
294 .join("share")
295 .join("dot");
296 #[cfg(not(unix))]
297 dirs::data_local_dir()
298 .unwrap_or_else(|| PathBuf::from("."))
299 .join("dot")
300 }
301
302 pub fn db_path() -> PathBuf {
303 Self::data_dir().join("dot.db")
304 }
305
306 pub fn load() -> Result<Self> {
307 let path = Self::config_path();
308 if path.exists() {
309 let content = std::fs::read_to_string(&path)
310 .with_context(|| format!("reading config from {}", path.display()))?;
311 toml::from_str(&content).context("parsing config.toml")
312 } else {
313 let config = Self::default();
314 config.save()?;
315 Ok(config)
316 }
317 }
318
319 pub fn save(&self) -> Result<()> {
320 let dir = Self::config_dir();
321 std::fs::create_dir_all(&dir)
322 .with_context(|| format!("creating config dir {}", dir.display()))?;
323 let content = toml::to_string_pretty(self).context("serializing config")?;
324 std::fs::write(Self::config_path(), content).context("writing config.toml")
325 }
326
327 pub fn ensure_dirs() -> Result<()> {
328 std::fs::create_dir_all(Self::config_dir()).context("creating config directory")?;
329 std::fs::create_dir_all(Self::data_dir()).context("creating data directory")?;
330 Ok(())
331 }
332
333 pub fn enabled_mcp_servers(&self) -> Vec<(&str, &McpServerConfig)> {
334 self.mcp
335 .iter()
336 .filter(|(_, cfg)| cfg.enabled && !cfg.command.is_empty())
337 .map(|(name, cfg)| (name.as_str(), cfg))
338 .collect()
339 }
340
341 pub fn enabled_agents(&self) -> Vec<(&str, &AgentConfig)> {
342 self.agents
343 .iter()
344 .filter(|(_, cfg)| cfg.enabled)
345 .map(|(name, cfg)| (name.as_str(), cfg))
346 .collect()
347 }
348
349 pub fn parse_model_spec(spec: &str) -> (Option<&str>, &str) {
352 if let Some((provider, model)) = spec.split_once('/') {
353 (Some(provider), model)
354 } else {
355 (None, spec)
356 }
357 }
358}