Skip to main content

dot/
config.rs

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)]
95pub struct TuiConfig {
96    #[serde(default = "default_true")]
97    pub vim_mode: bool,
98    #[serde(default)]
99    pub favorite_models: Vec<String>,
100}
101
102impl Default for TuiConfig {
103    fn default() -> Self {
104        Self {
105            vim_mode: true,
106            favorite_models: Vec::new(),
107        }
108    }
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ProviderDefinition {
113    pub api: String,
114    pub base_url: Option<String>,
115    #[serde(default)]
116    pub api_key_env: Option<String>,
117    #[serde(default)]
118    pub models: Vec<String>,
119    pub default_model: Option<String>,
120    #[serde(default = "default_true")]
121    pub enabled: bool,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct CustomToolConfig {
126    pub description: String,
127    pub command: String,
128    #[serde(default = "default_schema")]
129    pub schema: serde_json::Value,
130    #[serde(default = "default_timeout")]
131    pub timeout: u64,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct CommandConfig {
136    pub description: String,
137    pub command: String,
138    #[serde(default = "default_timeout")]
139    pub timeout: u64,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct HookConfig {
144    pub command: String,
145    #[serde(default = "default_timeout")]
146    pub timeout: u64,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct SubagentSettings {
151    #[serde(default = "default_true")]
152    pub enabled: bool,
153    #[serde(default = "default_max_subagent_turns")]
154    pub max_turns: usize,
155}
156
157impl Default for SubagentSettings {
158    fn default() -> Self {
159        Self {
160            enabled: true,
161            max_turns: 20,
162        }
163    }
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct MemoryConfig {
168    #[serde(default = "default_true")]
169    pub enabled: bool,
170    #[serde(default = "default_true")]
171    pub auto_extract: bool,
172    #[serde(default = "default_inject_count")]
173    pub inject_count: usize,
174    #[serde(default = "default_max_memories")]
175    pub max_memories: usize,
176}
177
178impl Default for MemoryConfig {
179    fn default() -> Self {
180        Self {
181            enabled: true,
182            auto_extract: true,
183            inject_count: 15,
184            max_memories: 2000,
185        }
186    }
187}
188
189fn default_inject_count() -> usize {
190    15
191}
192
193fn default_max_memories() -> usize {
194    2000
195}
196
197fn default_max_subagent_turns() -> usize {
198    20
199}
200
201fn default_true() -> bool {
202    true
203}
204
205fn default_timeout() -> u64 {
206    30
207}
208
209fn default_schema() -> serde_json::Value {
210    serde_json::json!({
211        "type": "object",
212        "properties": {},
213        "required": []
214    })
215}
216
217impl Default for Config {
218    fn default() -> Self {
219        Self {
220            default_provider: "anthropic".to_string(),
221            default_model: "claude-sonnet-4-20250514".to_string(),
222            theme: ThemeConfig {
223                name: "terminal".to_string(),
224            },
225            context: ContextConfig::default(),
226            acp_agents: HashMap::new(),
227            mcp: HashMap::new(),
228            agents: HashMap::new(),
229            tui: TuiConfig::default(),
230            permissions: HashMap::new(),
231            providers: HashMap::new(),
232            custom_tools: HashMap::new(),
233            commands: HashMap::new(),
234            hooks: HashMap::new(),
235            subagents: SubagentSettings::default(),
236            memory: MemoryConfig::default(),
237        }
238    }
239}
240
241impl Config {
242    pub fn config_dir() -> PathBuf {
243        if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME")
244            && !xdg.is_empty()
245        {
246            return PathBuf::from(xdg).join("dot");
247        }
248        #[cfg(unix)]
249        return dirs::home_dir()
250            .unwrap_or_else(|| PathBuf::from("."))
251            .join(".config")
252            .join("dot");
253        #[cfg(not(unix))]
254        dirs::config_dir()
255            .unwrap_or_else(|| PathBuf::from("."))
256            .join("dot")
257    }
258
259    pub fn config_path() -> PathBuf {
260        Self::config_dir().join("config.toml")
261    }
262
263    pub fn data_dir() -> PathBuf {
264        if let Ok(xdg) = std::env::var("XDG_DATA_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(".local")
273            .join("share")
274            .join("dot");
275        #[cfg(not(unix))]
276        dirs::data_local_dir()
277            .unwrap_or_else(|| PathBuf::from("."))
278            .join("dot")
279    }
280
281    pub fn db_path() -> PathBuf {
282        Self::data_dir().join("dot.db")
283    }
284
285    pub fn load() -> Result<Self> {
286        let path = Self::config_path();
287        if path.exists() {
288            let content = std::fs::read_to_string(&path)
289                .with_context(|| format!("reading config from {}", path.display()))?;
290            toml::from_str(&content).context("parsing config.toml")
291        } else {
292            let config = Self::default();
293            config.save()?;
294            Ok(config)
295        }
296    }
297
298    pub fn save(&self) -> Result<()> {
299        let dir = Self::config_dir();
300        std::fs::create_dir_all(&dir)
301            .with_context(|| format!("creating config dir {}", dir.display()))?;
302        let content = toml::to_string_pretty(self).context("serializing config")?;
303        std::fs::write(Self::config_path(), content).context("writing config.toml")
304    }
305
306    pub fn ensure_dirs() -> Result<()> {
307        std::fs::create_dir_all(Self::config_dir()).context("creating config directory")?;
308        std::fs::create_dir_all(Self::data_dir()).context("creating data directory")?;
309        Ok(())
310    }
311
312    pub fn enabled_mcp_servers(&self) -> Vec<(&str, &McpServerConfig)> {
313        self.mcp
314            .iter()
315            .filter(|(_, cfg)| cfg.enabled && !cfg.command.is_empty())
316            .map(|(name, cfg)| (name.as_str(), cfg))
317            .collect()
318    }
319
320    pub fn enabled_agents(&self) -> Vec<(&str, &AgentConfig)> {
321        self.agents
322            .iter()
323            .filter(|(_, cfg)| cfg.enabled)
324            .map(|(name, cfg)| (name.as_str(), cfg))
325            .collect()
326    }
327
328    /// Parse a `provider/model` spec. Returns `(provider, model)` if `/` present,
329    /// otherwise `(None, spec)`.
330    pub fn parse_model_spec(spec: &str) -> (Option<&str>, &str) {
331        if let Some((provider, model)) = spec.split_once('/') {
332            (Some(provider), model)
333        } else {
334            (None, spec)
335        }
336    }
337}