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