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 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}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ContextConfig {
25    #[serde(default = "default_true")]
26    pub auto_load_global: bool,
27    #[serde(default = "default_true")]
28    pub auto_load_project: bool,
29}
30impl Default for ContextConfig {
31    fn default() -> Self {
32        Self {
33            auto_load_global: true,
34            auto_load_project: true,
35        }
36    }
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct ThemeConfig {
41    pub name: String,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct McpServerConfig {
46    #[serde(default)]
47    pub command: Vec<String>,
48    pub url: Option<String>,
49    #[serde(default = "default_true")]
50    pub enabled: bool,
51    #[serde(default)]
52    pub env: HashMap<String, String>,
53    #[serde(default = "default_timeout")]
54    pub timeout: u64,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct AgentConfig {
59    pub description: String,
60    pub model: Option<String>,
61    pub system_prompt: Option<String>,
62    #[serde(default)]
63    pub tools: HashMap<String, bool>,
64    #[serde(default = "default_true")]
65    pub enabled: bool,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct TuiConfig {
70    #[serde(default = "default_true")]
71    pub vim_mode: bool,
72}
73
74impl Default for TuiConfig {
75    fn default() -> Self {
76        Self { vim_mode: true }
77    }
78}
79
80fn default_true() -> bool {
81    true
82}
83
84fn default_timeout() -> u64 {
85    30
86}
87
88impl Default for Config {
89    fn default() -> Self {
90        Self {
91            default_provider: "anthropic".to_string(),
92            default_model: "claude-sonnet-4-20250514".to_string(),
93            theme: ThemeConfig {
94                name: "terminal".to_string(),
95            },
96            context: ContextConfig::default(),
97
98            mcp: HashMap::new(),
99            agents: HashMap::new(),
100            tui: TuiConfig::default(),
101            permissions: HashMap::new(),
102        }
103    }
104}
105
106impl Config {
107    pub fn config_dir() -> PathBuf {
108        if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME")
109            && !xdg.is_empty()
110        {
111            return PathBuf::from(xdg).join("dot");
112        }
113        #[cfg(unix)]
114        return dirs::home_dir()
115            .unwrap_or_else(|| PathBuf::from("."))
116            .join(".config")
117            .join("dot");
118        #[cfg(not(unix))]
119        dirs::config_dir()
120            .unwrap_or_else(|| PathBuf::from("."))
121            .join("dot")
122    }
123
124    pub fn config_path() -> PathBuf {
125        Self::config_dir().join("config.toml")
126    }
127
128    pub fn data_dir() -> PathBuf {
129        if let Ok(xdg) = std::env::var("XDG_DATA_HOME")
130            && !xdg.is_empty()
131        {
132            return PathBuf::from(xdg).join("dot");
133        }
134        #[cfg(unix)]
135        return dirs::home_dir()
136            .unwrap_or_else(|| PathBuf::from("."))
137            .join(".local")
138            .join("share")
139            .join("dot");
140        #[cfg(not(unix))]
141        dirs::data_local_dir()
142            .unwrap_or_else(|| PathBuf::from("."))
143            .join("dot")
144    }
145
146    pub fn db_path() -> PathBuf {
147        Self::data_dir().join("dot.db")
148    }
149
150    pub fn load() -> Result<Self> {
151        let path = Self::config_path();
152        if path.exists() {
153            let content = std::fs::read_to_string(&path)
154                .with_context(|| format!("reading config from {}", path.display()))?;
155            toml::from_str(&content).context("parsing config.toml")
156        } else {
157            let config = Self::default();
158            config.save()?;
159            Ok(config)
160        }
161    }
162
163    pub fn save(&self) -> Result<()> {
164        let dir = Self::config_dir();
165        std::fs::create_dir_all(&dir)
166            .with_context(|| format!("creating config dir {}", dir.display()))?;
167        let content = toml::to_string_pretty(self).context("serializing config")?;
168        std::fs::write(Self::config_path(), content).context("writing config.toml")
169    }
170
171    pub fn ensure_dirs() -> Result<()> {
172        std::fs::create_dir_all(Self::config_dir()).context("creating config directory")?;
173        std::fs::create_dir_all(Self::data_dir()).context("creating data directory")?;
174        Ok(())
175    }
176
177    pub fn enabled_mcp_servers(&self) -> Vec<(&str, &McpServerConfig)> {
178        self.mcp
179            .iter()
180            .filter(|(_, cfg)| cfg.enabled && !cfg.command.is_empty())
181            .map(|(name, cfg)| (name.as_str(), cfg))
182            .collect()
183    }
184
185    pub fn enabled_agents(&self) -> Vec<(&str, &AgentConfig)> {
186        self.agents
187            .iter()
188            .filter(|(_, cfg)| cfg.enabled)
189            .map(|(name, cfg)| (name.as_str(), cfg))
190            .collect()
191    }
192
193    /// Parse a `provider/model` spec. Returns `(provider, model)` if `/` present,
194    /// otherwise `(None, spec)`.
195    pub fn parse_model_spec(spec: &str) -> (Option<&str>, &str) {
196        if let Some((provider, model)) = spec.split_once('/') {
197            (Some(provider), model)
198        } else {
199            (None, spec)
200        }
201    }
202}