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 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}