diff --git a/src/app.rs b/src/app.rs index e695639..b9a499a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -17,10 +17,11 @@ impl App { pub fn new( repo_path: PathBuf, debug: bool, + config_override: Option, filter_path: Option, start_in_commits: bool, ) -> Result { - let config = AppConfig::load(debug)?; + let config = AppConfig::load(debug, config_override)?; // Validate git repo if !GitCommands::is_valid_repo(&repo_path) { diff --git a/src/config/mod.rs b/src/config/mod.rs index abfdd5b..ce97edd 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -46,12 +46,12 @@ impl Default for AppConfig { } impl AppConfig { - pub fn load(debug: bool) -> Result { + pub fn load(debug: bool, config_override: Option) -> Result { let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); let candidates = config_dir_candidates(); - let config_dir = candidates + let mut config_dir = candidates .iter() - .find(|dir| dir.join("config.yml").exists()) + .find(|dir| dir.join("config.yml").exists() || dir.join("config.yaml").exists()) .cloned() .unwrap_or_else(|| candidates[0].clone()); @@ -71,7 +71,19 @@ impl AppConfig { let _ = std::fs::copy(&legacy_state_path, &state_path); } - let user_config = UserConfig::load(&config_dir)?; + let user_config = UserConfig::load(&config_dir, config_override.as_ref())?; + if let Some(ref over) = config_override { + let over_path = user_config::expand_tilde(over); + if let Some(parent) = over_path.parent() { + if over_path.is_absolute() + || over.ends_with(".yml") + || over.ends_with(".yaml") + || over_path.exists() + { + config_dir = parent.to_path_buf(); + } + } + } let app_state = AppState::load(&state_path)?; Ok(Self { diff --git a/src/config/user_config.rs b/src/config/user_config.rs index e4170e7..d24d7cd 100644 --- a/src/config/user_config.rs +++ b/src/config/user_config.rs @@ -52,9 +52,58 @@ impl Default for RefresherConfig { } } +pub(crate) fn expand_tilde(path_str: &str) -> std::path::PathBuf { + if let Some(stripped) = path_str.strip_prefix("~/") { + if let Some(home) = dirs::home_dir() { + return home.join(stripped); + } + } else if path_str == "~" { + if let Some(home) = dirs::home_dir() { + return home; + } + } + std::path::PathBuf::from(path_str) +} + impl UserConfig { - pub fn load(config_dir: &Path) -> Result { - let config_path = config_dir.join("config.yml"); + pub fn load(config_dir: &Path, config_override: Option<&String>) -> Result { + let mut config_path = if config_dir.join("config.yml").exists() { + config_dir.join("config.yml") + } else if config_dir.join("config.yaml").exists() { + config_dir.join("config.yaml") + } else { + config_dir.join("config.yml") + }; + + if let Some(over) = config_override { + let expanded_path = expand_tilde(over); + if expanded_path.is_absolute() + || over.ends_with(".yml") + || over.ends_with(".yaml") + || expanded_path.exists() + { + config_path = expanded_path; + } else { + let preset_yaml = config_dir.join("presets").join(format!("{}.yaml", over)); + let preset_yml = config_dir.join("presets").join(format!("{}.yml", over)); + if preset_yaml.exists() { + config_path = preset_yaml; + } else if preset_yml.exists() { + config_path = preset_yml; + } else { + anyhow::bail!( + "Config file or preset not found: '{}' (checked relative, absolute, and presets in {})", + over, + config_dir.display() + ); + } + } + + if !config_path.exists() { + anyhow::bail!("Config file not found at: {}", config_path.display()); + } + } + if config_path.exists() { let contents = std::fs::read_to_string(&config_path)?; let config: UserConfig = serde_yaml::from_str(&contents)?; @@ -629,3 +678,26 @@ pub struct CustomCommandPrompt { pub command: Option, pub filter: Option, } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_expand_tilde() { + let expanded = expand_tilde("~/test/path"); + if let Some(home) = dirs::home_dir() { + assert_eq!(expanded, home.join("test/path")); + } + } + + #[test] + fn test_load_nonexistent_override_fails() { + let temp_dir = std::env::temp_dir(); + let result = UserConfig::load( + &temp_dir, + Some(&"/path/that/does/not/exist/definitely/config.yml".to_string()), + ); + assert!(result.is_err()); + } +} diff --git a/src/main.rs b/src/main.rs index 3415ca0..15fece4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,6 +39,14 @@ struct Cli { #[arg(short = 'f', long = "filter", value_name = "PATH")] filter_path: Option, + /// Print the default configuration YAML to stdout and exit + #[arg(long)] + print_default_config: bool, + + /// Configuration file or preset name to use (e.g. 'popup' or ~/.config/lazygitrs/config.yml) + #[arg(short = 'c', long = "config")] + config: Option, + /// Launch directly in the commits panel #[arg(long)] commits: bool, @@ -106,6 +114,13 @@ fn main() { return; } + if cli.print_default_config { + let config = config::user_config::UserConfig::default(); + let yaml = serde_yaml::to_string(&config).unwrap(); + println!("{}", yaml); + std::process::exit(0); + } + // Set up logging if debug mode if cli.debug { tracing_subscriber::fmt() @@ -119,7 +134,13 @@ fn main() { .or(cli.work_tree) .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))); - match app::App::new(repo_path, cli.debug, cli.filter_path, cli.commits) { + match app::App::new( + repo_path, + cli.debug, + cli.config, + cli.filter_path, + cli.commits, + ) { Ok(app) => { if let Err(e) = app.run() { eprintln!("Error: {:#}", e); @@ -145,4 +166,22 @@ mod tests { let cli_default = Cli::parse_from(["lazygitrs"]); assert!(!cli_default.commits); } + + #[test] + fn test_cli_config_flag() { + let cli = Cli::parse_from(["lazygitrs", "-c", "my-config.yml"]); + assert_eq!(cli.config.as_deref(), Some("my-config.yml")); + + let cli_long = Cli::parse_from(["lazygitrs", "--config", "popup"]); + assert_eq!(cli_long.config.as_deref(), Some("popup")); + + let cli_default = Cli::parse_from(["lazygitrs"]); + assert_eq!(cli_default.config, None); + } + + #[test] + fn test_cli_print_default_config_flag() { + let cli = Cli::parse_from(["lazygitrs", "--print-default-config"]); + assert!(cli.print_default_config); + } }