1use crate::error::{GitWarpError, Result};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4use figment::{Figment, providers::{Format, Toml, Env}};
5use dirs::config_dir;
6use std::fs;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Config {
10 #[serde(default = "default_terminal_mode")]
12 pub terminal_mode: String,
13
14 pub worktrees_path: Option<PathBuf>,
16
17 #[serde(default = "default_true")]
19 pub use_cow: bool,
20
21 #[serde(default)]
23 pub auto_confirm: bool,
24
25 #[serde(default)]
27 pub git: GitConfig,
28
29 #[serde(default)]
31 pub process: ProcessConfig,
32
33 #[serde(default)]
35 pub terminal: TerminalConfig,
36
37 #[serde(default)]
39 pub agent: AgentConfig,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct GitConfig {
44 #[serde(default = "default_main_branch")]
46 pub default_branch: String,
47
48 #[serde(default = "default_true")]
50 pub auto_fetch: bool,
51
52 #[serde(default = "default_true")]
54 pub auto_prune: bool,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct ProcessConfig {
59 #[serde(default = "default_true")]
61 pub check_processes: bool,
62
63 #[serde(default)]
65 pub auto_kill: bool,
66
67 #[serde(default = "default_kill_timeout")]
69 pub kill_timeout: u64,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct TerminalConfig {
74 #[serde(default = "default_terminal_app")]
76 pub app: String,
77
78 #[serde(default = "default_true")]
80 pub auto_activate: bool,
81
82 #[serde(default)]
84 pub init_commands: Vec<String>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct AgentConfig {
89 #[serde(default = "default_true")]
91 pub enabled: bool,
92
93 #[serde(default = "default_refresh_rate")]
95 pub refresh_rate: u64,
96
97 #[serde(default = "default_max_activities")]
99 pub max_activities: usize,
100
101 #[serde(default = "default_true")]
103 pub claude_hooks: bool,
104}
105
106fn default_terminal_mode() -> String {
108 "tab".to_string()
109}
110
111fn default_true() -> bool {
112 true
113}
114
115fn default_main_branch() -> String {
116 "main".to_string()
117}
118
119fn default_kill_timeout() -> u64 {
120 5
121}
122
123fn default_terminal_app() -> String {
124 "auto".to_string()
125}
126
127fn default_refresh_rate() -> u64 {
128 1000
129}
130
131fn default_max_activities() -> usize {
132 100
133}
134
135impl Default for Config {
137 fn default() -> Self {
138 Self {
139 terminal_mode: default_terminal_mode(),
140 worktrees_path: None,
141 use_cow: true,
142 auto_confirm: false,
143 git: GitConfig::default(),
144 process: ProcessConfig::default(),
145 terminal: TerminalConfig::default(),
146 agent: AgentConfig::default(),
147 }
148 }
149}
150
151impl Default for GitConfig {
152 fn default() -> Self {
153 Self {
154 default_branch: default_main_branch(),
155 auto_fetch: true,
156 auto_prune: true,
157 }
158 }
159}
160
161impl Default for ProcessConfig {
162 fn default() -> Self {
163 Self {
164 check_processes: true,
165 auto_kill: false,
166 kill_timeout: default_kill_timeout(),
167 }
168 }
169}
170
171impl Default for TerminalConfig {
172 fn default() -> Self {
173 Self {
174 app: default_terminal_app(),
175 auto_activate: true,
176 init_commands: Vec::new(),
177 }
178 }
179}
180
181impl Default for AgentConfig {
182 fn default() -> Self {
183 Self {
184 enabled: true,
185 refresh_rate: default_refresh_rate(),
186 max_activities: default_max_activities(),
187 claude_hooks: true,
188 }
189 }
190}
191
192impl Config {
193 pub fn with_defaults() -> Self {
195 Self::default()
196 }
197
198 pub fn apply_env_overrides(&mut self) {
200 if let Ok(mode) = std::env::var("GIT_WARP_TERMINAL_MODE") {
202 self.terminal_mode = mode;
203 }
204
205 if let Ok(confirm) = std::env::var("GIT_WARP_AUTO_CONFIRM") {
207 self.auto_confirm = confirm.parse().unwrap_or(false);
208 }
209
210 if let Ok(cow) = std::env::var("GIT_WARP_USE_COW") {
212 self.use_cow = cow.parse().unwrap_or(true);
213 }
214
215 if let Ok(path) = std::env::var("GIT_WARP_WORKTREES_PATH") {
217 self.worktrees_path = Some(PathBuf::from(path));
218 }
219 }
220
221 pub fn sample_config() -> String {
223 let config = Config::default();
224 format!(
225 r#"# Git-Warp Configuration
226# This file configures git-warp behavior
227# You can also set these values via environment variables with GIT_WARP_ prefix
228
229# Terminal mode: tab, window, inplace, echo
230terminal_mode = "{}"
231
232# Use Copy-on-Write when available
233use_cow = {}
234
235# Auto-confirm destructive operations
236auto_confirm = {}
237
238# Custom worktrees directory (optional)
239# worktrees_path = "/custom/path/to/worktrees"
240
241[git]
242# Default main branch name
243default_branch = "{}"
244
245# Auto-fetch before operations
246auto_fetch = {}
247
248# Auto-prune remote tracking branches
249auto_prune = {}
250
251[process]
252# Check for processes before cleanup
253check_processes = {}
254
255# Auto-kill processes during cleanup
256auto_kill = {}
257
258# Grace period before force killing (seconds)
259kill_timeout = {}
260
261[terminal]
262# Terminal app: auto, iterm2, terminal
263app = "{}"
264
265# Auto-activate new tabs/windows
266auto_activate = {}
267
268# Custom commands to run in new worktrees
269# init_commands = ["npm install", "source .env"]
270
271[agent]
272# Enable agent monitoring
273enabled = {}
274
275# Refresh rate for agent dashboard (milliseconds)
276refresh_rate = {}
277
278# Maximum activities to track
279max_activities = {}
280
281# Enable Claude Code hooks integration
282claude_hooks = {}
283"#,
284 config.terminal_mode,
285 config.use_cow,
286 config.auto_confirm,
287 config.git.default_branch,
288 config.git.auto_fetch,
289 config.git.auto_prune,
290 config.process.check_processes,
291 config.process.auto_kill,
292 config.process.kill_timeout,
293 config.terminal.app,
294 config.terminal.auto_activate,
295 config.agent.enabled,
296 config.agent.refresh_rate,
297 config.agent.max_activities,
298 config.agent.claude_hooks,
299 )
300 }
301}
302
303pub struct ConfigManager {
304 pub config: Config,
305 pub config_path: PathBuf,
306}
307
308
309impl ConfigManager {
310 pub fn new() -> Result<Self> {
312 let config_path = get_config_path()?;
313 let config = Self::load_config(&config_path)?;
314 Ok(Self { config, config_path })
315 }
316
317 fn load_config(config_path: &PathBuf) -> Result<Config> {
319 let figment = Figment::new()
320 .merge(Toml::file(config_path))
322 .merge(Env::prefixed("GIT_WARP_"));
324
325 figment.extract().map_err(|e| {
326 GitWarpError::ConfigError {
327 message: format!("Failed to load configuration: {}", e)
328 }.into()
329 })
330 }
331
332 pub fn get(&self) -> &Config {
334 &self.config
335 }
336
337 pub fn get_mut(&mut self) -> &mut Config {
339 &mut self.config
340 }
341
342 pub fn save(&self) -> Result<()> {
344 self.save_config(&self.config_path, &self.config)
345 }
346
347 fn save_config(&self, path: &PathBuf, config: &Config) -> Result<()> {
349 if let Some(parent) = path.parent() {
351 fs::create_dir_all(parent)?;
352 }
353
354 let toml_content = toml::to_string_pretty(config)
355 .map_err(|e| GitWarpError::ConfigError {
356 message: format!("Failed to serialize configuration: {}", e)
357 })?;
358
359 fs::write(path, toml_content)
360 .map_err(|e| GitWarpError::ConfigError {
361 message: format!("Failed to write config file: {}", e)
362 })?;
363
364 Ok(())
365 }
366
367 pub fn config_path(&self) -> &PathBuf {
369 &self.config_path
370 }
371
372 pub fn create_default_config(&self) -> Result<()> {
374 let default_config = Config::default();
375 self.save_config(&self.config_path, &default_config)
376 }
377
378 pub fn config_exists(&self) -> bool {
380 self.config_path.exists()
381 }
382
383 pub fn show_sample_config(&self) {
385 println!("{}", Config::sample_config());
386 }
387}
388
389fn get_config_path() -> Result<PathBuf> {
391 let config_dir = config_dir()
392 .ok_or_else(|| GitWarpError::ConfigError {
393 message: "Could not determine config directory".to_string()
394 })?;
395
396 Ok(config_dir.join("git-warp").join("config.toml"))
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use tempfile::tempdir;
403
404 #[test]
405 fn test_config_defaults() {
406 let config = Config::default();
407 assert_eq!(config.terminal_mode, "tab");
408 assert_eq!(config.use_cow, true);
409 assert_eq!(config.auto_confirm, false);
410 assert_eq!(config.git.default_branch, "main");
411 assert_eq!(config.process.kill_timeout, 5);
412 }
413
414 #[test]
415 fn test_config_serialization() {
416 let config = Config::default();
417 let toml_str = toml::to_string(&config).unwrap();
418 let parsed: Config = toml::from_str(&toml_str).unwrap();
419
420 assert_eq!(config.terminal_mode, parsed.terminal_mode);
421 assert_eq!(config.use_cow, parsed.use_cow);
422 assert_eq!(config.git.default_branch, parsed.git.default_branch);
423 }
424
425 #[test]
426 fn test_config_manager_creation() {
427 let temp_dir = tempdir().unwrap();
428
429 let manager = ConfigManager {
431 config: Config::default(),
432 config_path: temp_dir.path().join("config.toml"),
433 };
434
435 assert_eq!(manager.get().terminal_mode, "tab");
436 }
437
438 #[test]
439 fn test_config_environment_overrides() {
440 let mut config = Config::default();
441
442 unsafe {
444 std::env::set_var("GIT_WARP_TERMINAL_MODE", "window");
445 std::env::set_var("GIT_WARP_AUTO_CONFIRM", "true");
446 }
447
448 config.apply_env_overrides();
449
450 assert_eq!(config.terminal_mode, "window");
451 assert_eq!(config.auto_confirm, true);
452
453 unsafe {
455 std::env::remove_var("GIT_WARP_TERMINAL_MODE");
456 std::env::remove_var("GIT_WARP_AUTO_CONFIRM");
457 }
458 }
459
460 #[test]
461 fn test_sample_config_generation() {
462 let sample = Config::sample_config();
463 assert!(sample.contains("terminal_mode"));
464 assert!(sample.contains("[git]"));
465 assert!(sample.contains("[process]"));
466 assert!(sample.contains("[agent]"));
467 }
468}