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 projects: HashMap<String, ProjectConfig>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(default)]
24pub struct GeneralConfig {
25    /// Default result limit
26    pub default_limit: usize,
27    /// Auto-start daemon
28    pub daemon_autostart: bool,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(default)]
33pub struct WatchConfig {
34    /// Directories to watch
35    pub paths: Vec<PathBuf>,
36    /// Recursively discover projects
37    pub recursive: bool,
38    /// Debounce time in milliseconds
39    pub debounce_ms: u64,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(default)]
44pub struct IgnoreConfig {
45    /// Global ignore patterns
46    pub patterns: Vec<String>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(default)]
51pub struct IndexConfig {
52    /// Maximum file size to index (bytes)
53    pub max_file_size: u64,
54    /// Maximum files per project
55    pub max_files: usize,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(default)]
60pub struct CacheConfig {
61    /// Query cache TTL (seconds)
62    pub query_ttl: u64,
63    /// Maximum cached queries
64    pub max_queries: usize,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, Default)]
68pub struct ProjectConfig {
69    /// Project-specific ignore patterns
70    pub ignore: Vec<String>,
71}
72
73impl Default for Config {
74    fn default() -> Self {
75        Self {
76            general: GeneralConfig::default(),
77            watch: WatchConfig::default(),
78            ignore: IgnoreConfig::default(),
79            index: IndexConfig::default(),
80            cache: CacheConfig::default(),
81            projects: HashMap::new(),
82        }
83    }
84}
85
86impl Default for GeneralConfig {
87    fn default() -> Self {
88        Self {
89            default_limit: 20,
90            daemon_autostart: false,
91        }
92    }
93}
94
95impl Default for WatchConfig {
96    fn default() -> Self {
97        Self {
98            paths: vec![],
99            recursive: true,
100            debounce_ms: 100,
101        }
102    }
103}
104
105impl Default for IgnoreConfig {
106    fn default() -> Self {
107        Self {
108            patterns: vec![
109                "node_modules".to_string(),
110                ".git".to_string(),
111                "dist".to_string(),
112                "build".to_string(),
113                "__pycache__".to_string(),
114                "*.min.js".to_string(),
115                "*.map".to_string(),
116            ],
117        }
118    }
119}
120
121impl Default for IndexConfig {
122    fn default() -> Self {
123        Self {
124            max_file_size: 1_048_576, // 1MB
125            max_files: 100_000,
126        }
127    }
128}
129
130impl Default for CacheConfig {
131    fn default() -> Self {
132        Self {
133            query_ttl: 60,
134            max_queries: 1000,
135        }
136    }
137}
138
139impl Config {
140    /// Load configuration from default location
141    pub fn load() -> Result<Self> {
142        let config_path = Self::config_path()?;
143
144        if config_path.exists() {
145            let content = std::fs::read_to_string(&config_path)?;
146            let config: Config = toml::from_str(&content)?;
147            Ok(config)
148        } else {
149            Ok(Config::default())
150        }
151    }
152
153    /// Get the configuration file path
154    pub fn config_path() -> Result<PathBuf> {
155        let home = Self::greppy_home()?;
156        Ok(home.join("config.toml"))
157    }
158
159    /// Get the greppy home directory
160    pub fn greppy_home() -> Result<PathBuf> {
161        // Check GREPPY_HOME env var first
162        if let Ok(home) = std::env::var("GREPPY_HOME") {
163            return Ok(PathBuf::from(home));
164        }
165
166        // Use XDG directories
167        ProjectDirs::from("dev", "greppy", "greppy")
168            .map(|dirs| dirs.data_dir().to_path_buf())
169            .ok_or_else(|| Error::ConfigError {
170                message: "Could not determine greppy home directory".to_string(),
171            })
172    }
173
174    /// Get the index directory for a project
175    pub fn index_dir(project_path: &std::path::Path) -> Result<PathBuf> {
176        let home = Self::greppy_home()?;
177        let hash = xxhash_rust::xxh3::xxh3_64(project_path.to_string_lossy().as_bytes());
178        Ok(home.join("indexes").join(format!("{:016x}", hash)))
179    }
180
181    /// Get registry file path (tracks indexed projects)
182    pub fn registry_path() -> Result<PathBuf> {
183        Ok(Self::greppy_home()?.join("registry.json"))
184    }
185
186    /// Ensure home directory exists
187    pub fn ensure_home() -> Result<()> {
188        let home = Self::greppy_home()?;
189        if !home.exists() {
190            std::fs::create_dir_all(&home)?;
191        }
192        Ok(())
193    }
194
195    /// Get the daemon socket path (Unix only)
196    pub fn socket_path() -> Result<PathBuf> {
197        if let Ok(socket) = std::env::var("GREPPY_DAEMON_SOCKET") {
198            return Ok(PathBuf::from(socket));
199        }
200        let home = Self::greppy_home()?;
201        Ok(home.join("daemon.sock"))
202    }
203
204    /// Get the daemon PID file path
205    pub fn pid_path() -> Result<PathBuf> {
206        Ok(Self::greppy_home()?.join("daemon.pid"))
207    }
208
209    /// Get the daemon port (Windows only - uses TCP instead of Unix sockets)
210    #[cfg(windows)]
211    pub fn daemon_port() -> u16 {
212        std::env::var("GREPPY_DAEMON_PORT")
213            .ok()
214            .and_then(|p| p.parse().ok())
215            .unwrap_or(DEFAULT_DAEMON_PORT)
216    }
217
218    /// Get the daemon port file path (Windows - stores which port daemon is using)
219    #[cfg(windows)]
220    pub fn port_path() -> Result<PathBuf> {
221        Ok(Self::greppy_home()?.join("daemon.port"))
222    }
223}
224
225/// Default daemon port for Windows TCP connection
226#[cfg(windows)]
227const DEFAULT_DAEMON_PORT: u16 = 19532;
228
229pub const MAX_FILE_SIZE: u64 = 1_048_576; // 1MB
230pub const CHUNK_MAX_LINES: usize = 50;
231pub const CHUNK_OVERLAP: usize = 5;