1use crate::core::error::{Error, Result};
4use directories::ProjectDirs;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(default)]
12pub struct Config {
13 pub general: GeneralConfig,
14 pub watch: WatchConfig,
15 pub ignore: IgnoreConfig,
16 pub index: IndexConfig,
17 pub cache: CacheConfig,
18 #[serde(default)]
19 pub ai: AiConfig,
20 #[serde(default)]
21 pub projects: HashMap<String, ProjectConfig>,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(default)]
26pub struct GeneralConfig {
27 pub default_limit: usize,
29 pub daemon_autostart: bool,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(default)]
35pub struct WatchConfig {
36 pub paths: Vec<PathBuf>,
38 pub recursive: bool,
40 pub debounce_ms: u64,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(default)]
46pub struct IgnoreConfig {
47 pub patterns: Vec<String>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(default)]
53pub struct IndexConfig {
54 pub max_file_size: u64,
56 pub max_files: usize,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(default)]
62pub struct CacheConfig {
63 pub query_ttl: u64,
65 pub max_queries: usize,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(default)]
72pub struct AiConfig {
73 pub provider: AiProvider,
75 pub ollama_model: String,
77 pub ollama_url: String,
79 #[serde(skip_serializing_if = "Option::is_none")]
81 pub google_token: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
84 pub anthropic_token: Option<String>,
85 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
87 pub profiles: HashMap<String, AiProfile>,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct AiProfile {
93 pub provider: AiProvider,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub ollama_model: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub ollama_url: Option<String>,
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub google_token: Option<String>,
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub anthropic_token: Option<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
108#[serde(rename_all = "lowercase")]
109pub enum AiProvider {
110 #[default]
112 Claude,
113 Gemini,
115 Ollama,
117}
118
119impl Default for AiConfig {
120 fn default() -> Self {
121 Self {
122 provider: AiProvider::Claude,
123 ollama_model: "codellama".to_string(),
124 ollama_url: "http://localhost:11434".to_string(),
125 google_token: None,
126 anthropic_token: None,
127 profiles: HashMap::new(),
128 }
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, Default)]
133pub struct ProjectConfig {
134 pub ignore: Vec<String>,
136}
137
138impl Default for Config {
139 fn default() -> Self {
140 Self {
141 general: GeneralConfig::default(),
142 watch: WatchConfig::default(),
143 ignore: IgnoreConfig::default(),
144 index: IndexConfig::default(),
145 cache: CacheConfig::default(),
146 ai: AiConfig::default(),
147 projects: HashMap::new(),
148 }
149 }
150}
151
152impl Default for GeneralConfig {
153 fn default() -> Self {
154 Self {
155 default_limit: 20,
156 daemon_autostart: false,
157 }
158 }
159}
160
161impl Default for WatchConfig {
162 fn default() -> Self {
163 Self {
164 paths: vec![],
165 recursive: true,
166 debounce_ms: 100,
167 }
168 }
169}
170
171impl Default for IgnoreConfig {
172 fn default() -> Self {
173 Self {
174 patterns: vec![
175 "node_modules".to_string(),
176 ".git".to_string(),
177 "dist".to_string(),
178 "build".to_string(),
179 "__pycache__".to_string(),
180 "*.min.js".to_string(),
181 "*.map".to_string(),
182 ],
183 }
184 }
185}
186
187impl Default for IndexConfig {
188 fn default() -> Self {
189 Self {
190 max_file_size: 1_048_576, max_files: 100_000,
192 }
193 }
194}
195
196impl Default for CacheConfig {
197 fn default() -> Self {
198 Self {
199 query_ttl: 60,
200 max_queries: 1000,
201 }
202 }
203}
204
205impl Config {
206 pub fn load() -> Result<Self> {
208 let config_path = Self::config_path()?;
209
210 if config_path.exists() {
211 let content = std::fs::read_to_string(&config_path)?;
212 let config: Config = toml::from_str(&content)?;
213 Ok(config)
214 } else {
215 Ok(Config::default())
216 }
217 }
218
219 pub fn save(&self) -> Result<()> {
221 Self::ensure_home()?;
222 let config_path = Self::config_path()?;
223 let content = toml::to_string_pretty(self).map_err(|e| Error::ConfigError {
224 message: format!("Failed to serialize config: {}", e),
225 })?;
226 std::fs::write(&config_path, content)?;
227 Ok(())
228 }
229
230 pub fn config_path() -> Result<PathBuf> {
232 let home = Self::greppy_home()?;
233 Ok(home.join("config.toml"))
234 }
235
236 pub fn greppy_home() -> Result<PathBuf> {
238 if let Ok(home) = std::env::var("GREPPY_HOME") {
240 return Ok(PathBuf::from(home));
241 }
242
243 ProjectDirs::from("dev", "greppy", "greppy")
245 .map(|dirs| dirs.data_dir().to_path_buf())
246 .ok_or_else(|| Error::ConfigError {
247 message: "Could not determine greppy home directory".to_string(),
248 })
249 }
250
251 pub fn index_dir(project_path: &std::path::Path) -> Result<PathBuf> {
253 let home = Self::greppy_home()?;
254 let hash = xxhash_rust::xxh3::xxh3_64(project_path.to_string_lossy().as_bytes());
255 Ok(home.join("indexes").join(format!("{:016x}", hash)))
256 }
257
258 pub fn registry_path() -> Result<PathBuf> {
260 Ok(Self::greppy_home()?.join("registry.json"))
261 }
262
263 pub fn ensure_home() -> Result<()> {
265 let home = Self::greppy_home()?;
266 if !home.exists() {
267 std::fs::create_dir_all(&home)?;
268 }
269 Ok(())
270 }
271
272 pub fn socket_path() -> Result<PathBuf> {
274 if let Ok(socket) = std::env::var("GREPPY_DAEMON_SOCKET") {
275 return Ok(PathBuf::from(socket));
276 }
277 let home = Self::greppy_home()?;
278 Ok(home.join("daemon.sock"))
279 }
280
281 pub fn pid_path() -> Result<PathBuf> {
283 Ok(Self::greppy_home()?.join("daemon.pid"))
284 }
285
286 #[cfg(windows)]
288 pub fn daemon_port() -> u16 {
289 std::env::var("GREPPY_DAEMON_PORT")
290 .ok()
291 .and_then(|p| p.parse().ok())
292 .unwrap_or(DEFAULT_DAEMON_PORT)
293 }
294
295 #[cfg(windows)]
297 pub fn port_path() -> Result<PathBuf> {
298 Ok(Self::greppy_home()?.join("daemon.port"))
299 }
300}
301
302#[cfg(windows)]
304const DEFAULT_DAEMON_PORT: u16 = 19532;
305
306pub const MAX_FILE_SIZE: u64 = 1_048_576; pub const CHUNK_MAX_LINES: usize = 50;
308pub const CHUNK_OVERLAP: usize = 5;