greppy/core/
config.rs

1//! Configuration management
2
3use crate::core::error::{Error, Result};
4use directories::ProjectDirs;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::PathBuf;
8
9/// Global configuration
10#[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    /// Default result limit
28    pub default_limit: usize,
29    /// Auto-start daemon
30    pub daemon_autostart: bool,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(default)]
35pub struct WatchConfig {
36    /// Directories to watch
37    pub paths: Vec<PathBuf>,
38    /// Recursively discover projects
39    pub recursive: bool,
40    /// Debounce time in milliseconds
41    pub debounce_ms: u64,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(default)]
46pub struct IgnoreConfig {
47    /// Global ignore patterns
48    pub patterns: Vec<String>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(default)]
53pub struct IndexConfig {
54    /// Maximum file size to index (bytes)
55    pub max_file_size: u64,
56    /// Maximum files per project
57    pub max_files: usize,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(default)]
62pub struct CacheConfig {
63    /// Query cache TTL (seconds)
64    pub query_ttl: u64,
65    /// Maximum cached queries
66    pub max_queries: usize,
67}
68
69/// AI provider configuration
70#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(default)]
72pub struct AiConfig {
73    /// AI provider to use: "claude", "gemini", or "ollama"
74    pub provider: AiProvider,
75    /// Ollama model name (e.g., "codellama", "deepseek-coder", "llama3")
76    pub ollama_model: String,
77    /// Ollama server URL
78    pub ollama_url: String,
79    /// Google OAuth token (for Gemini)
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub google_token: Option<String>,
82    /// Anthropic OAuth token (for Claude)
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub anthropic_token: Option<String>,
85    /// Saved AI profiles for quick switching
86    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
87    pub profiles: HashMap<String, AiProfile>,
88}
89
90/// A saved AI profile (model configuration with optional tokens)
91#[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    /// Profile-specific Google token (for multiple Gemini accounts)
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub google_token: Option<String>,
101    /// Profile-specific Anthropic token (for multiple Claude accounts)
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub anthropic_token: Option<String>,
104}
105
106/// Supported AI providers
107#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
108#[serde(rename_all = "lowercase")]
109pub enum AiProvider {
110    /// Anthropic Claude (requires OAuth login)
111    #[default]
112    Claude,
113    /// Google Gemini (requires OAuth login)
114    Gemini,
115    /// Local Ollama instance (no auth required)
116    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    /// Project-specific ignore patterns
135    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, // 1MB
191            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    /// Load configuration from default location
207    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    /// Save configuration to default location
220    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    /// Get the configuration file path
231    pub fn config_path() -> Result<PathBuf> {
232        let home = Self::greppy_home()?;
233        Ok(home.join("config.toml"))
234    }
235
236    /// Get the greppy home directory
237    pub fn greppy_home() -> Result<PathBuf> {
238        // Check GREPPY_HOME env var first
239        if let Ok(home) = std::env::var("GREPPY_HOME") {
240            return Ok(PathBuf::from(home));
241        }
242
243        // Use XDG directories
244        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    /// Get the index directory for a project
252    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    /// Get registry file path (tracks indexed projects)
259    pub fn registry_path() -> Result<PathBuf> {
260        Ok(Self::greppy_home()?.join("registry.json"))
261    }
262
263    /// Ensure home directory exists
264    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    /// Get the daemon socket path (Unix only)
273    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    /// Get the daemon PID file path
282    pub fn pid_path() -> Result<PathBuf> {
283        Ok(Self::greppy_home()?.join("daemon.pid"))
284    }
285
286    /// Get the daemon port (Windows only - uses TCP instead of Unix sockets)
287    #[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    /// Get the daemon port file path (Windows - stores which port daemon is using)
296    #[cfg(windows)]
297    pub fn port_path() -> Result<PathBuf> {
298        Ok(Self::greppy_home()?.join("daemon.port"))
299    }
300}
301
302/// Default daemon port for Windows TCP connection
303#[cfg(windows)]
304const DEFAULT_DAEMON_PORT: u16 = 19532;
305
306pub const MAX_FILE_SIZE: u64 = 1_048_576; // 1MB
307pub const CHUNK_MAX_LINES: usize = 50;
308pub const CHUNK_OVERLAP: usize = 5;