use std::path::PathBuf; use std::fs; use anyhow::{Result, Context}; use serde::{Deserialize, Serialize}; use log::{debug, error}; use crate::args::{Args}; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ColumnConfig { pub name: String, pub label: String, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ServerConfig { pub address: Option, pub port: Option, pub password_file: Option, pub password: Option, pub password_hash: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct CompressionPluginConfig { pub name: String, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct MetaPluginConfig { pub name: String, } /// Unified settings that merges config file and CLI arguments #[derive(Debug, Clone, Deserialize)] pub struct Settings { #[serde(default)] pub dir: PathBuf, pub list_format: Vec, #[serde(default)] pub human_readable: bool, pub output_format: Option, #[serde(default)] pub quiet: bool, #[serde(default)] pub force: bool, pub server: Option, pub compression_plugin: Option, pub meta_plugins: Option>, pub digest: Option, } impl Settings { /// Create unified settings from config and args with proper priority pub fn new(args: &Args, default_dir: PathBuf) -> Result { debug!("CONFIG: Creating settings with default dir: {:?}", default_dir); let config_path = if let Some(config_path) = &args.options.config { config_path.clone() } else if let Ok(env_config) = std::env::var("KEEP_CONFIG") { PathBuf::from(env_config) } else { match Self::default_config_path() { Ok(path) => path, Err(e) => { debug!("CONFIG: Failed to get default config path: {}", e); PathBuf::from("~/.config/keep/config.yml") } } }; debug!("CONFIG: Using config path: {:?}", config_path); let mut config_builder = config::Config::builder(); // Load config file if it exists if config_path.exists() { debug!("CONFIG: Loading config file: {:?}", config_path); config_builder = config_builder.add_source(config::File::from(config_path.clone()).required(false)); } else { debug!("CONFIG: Config file does not exist: {:?}", config_path); } // Add environment variables debug!("CONFIG: Adding environment variables"); config_builder = config_builder.add_source(config::Environment::with_prefix("KEEP").separator("__")); // Override with CLI args if let Some(dir) = &args.options.dir { debug!("CONFIG: Overriding dir with CLI arg: {:?}", dir); config_builder = config_builder.set_override("dir", dir.to_str().unwrap())?; } if args.options.list_format != "id,time,size,tags,meta:hostname" { // Convert the string format to the new list format structure let columns: Vec> = args.options.list_format .split(',') .map(|col| { let parts: Vec<&str> = col.split(':').collect(); let mut map = std::collections::HashMap::new(); map.insert("name".to_string(), parts[0].to_string()); let label = if parts.len() > 1 { parts[1].to_string() } else { parts[0].to_string() }; map.insert("label".to_string(), label); map }) .collect(); config_builder = config_builder.set_override("list_format", columns)?; } if args.options.human_readable { config_builder = config_builder.set_override("human_readable", true)?; } if let Some(output_format) = &args.options.output_format { config_builder = config_builder.set_override("output_format", output_format.as_str())?; } if args.options.verbose > 0 { config_builder = config_builder.set_override("verbose", args.options.verbose)?; } if args.options.quiet { config_builder = config_builder.set_override("quiet", true)?; } if args.options.force { config_builder = config_builder.set_override("force", true)?; } if let Some(server_password) = &args.options.server_password { config_builder = config_builder.set_override("server.password", server_password.as_str())?; } if let Some(server_password_hash) = &args.options.server_password_hash { config_builder = config_builder.set_override("server.password_hash", server_password_hash.as_str())?; } if let Some(server_address) = &args.mode.server_address { config_builder = config_builder.set_override("server.address", server_address.as_str())?; } if let Some(server_port) = args.mode.server_port { config_builder = config_builder.set_override("server.port", server_port)?; } if let Some(compression) = &args.item.compression { config_builder = config_builder.set_override("compression_plugin.name", compression.as_str())?; } if let Some(digest) = &args.item.digest { config_builder = config_builder.set_override("digest", digest.as_str())?; } if !args.item.meta_plugins.is_empty() { let meta_plugins: Vec> = args.item.meta_plugins .iter() .map(|name| { let mut map = std::collections::HashMap::new(); map.insert("name".to_string(), name.clone()); map }) .collect(); config_builder = config_builder.set_override("meta_plugins", meta_plugins)?; } let config = config_builder.build()?; debug!("CONFIG: Built config, attempting to deserialize"); match config.try_deserialize::() { Ok(mut settings) => { debug!("CONFIG: Successfully deserialized settings: {:?}", settings); // Set defaults for list_format if not provided if settings.list_format.is_empty() { debug!("CONFIG: Setting default list_format"); settings.list_format = vec![ ColumnConfig { name: "id".to_string(), label: "Item".to_string() }, ColumnConfig { name: "time".to_string(), label: "Time".to_string() }, ColumnConfig { name: "size".to_string(), label: "Size".to_string() }, ColumnConfig { name: "meta:full_hostname".to_string(), label: "Host".to_string() }, ColumnConfig { name: "meta:command".to_string(), label: "Command".to_string() }, ]; } // Set dir to default if not provided or is empty if settings.dir == PathBuf::new() { debug!("CONFIG: Setting default dir: {:?}", default_dir); settings.dir = default_dir; } debug!("CONFIG: Final settings: {:?}", settings); Ok(settings) } Err(e) => { error!("CONFIG: Failed to deserialize settings: {}", e); Err(e.into()) } } } /// Get the default config file path pub fn default_config_path() -> Result { let xdg_dirs = xdg::BaseDirectories::with_prefix("keep")?; Ok(xdg_dirs.get_config_home().join("config.yml")) } /// Get server password from password_file or directly from config if configured pub fn get_server_password(&self) -> Result> { if let Some(server) = &self.server { // First check for password_file if let Some(password_file) = &server.password_file { debug!("CONFIG: Reading password from file: {:?}", password_file); let password = fs::read_to_string(password_file) .with_context(|| format!("Failed to read password file: {:?}", password_file))? .trim() .to_string(); return Ok(Some(password)); } // Fall back to direct password field if let Some(password) = &server.password { debug!("CONFIG: Using password from config"); return Ok(Some(password.clone())); } } Ok(None) } // Helper methods to access configuration values pub fn server_password(&self) -> Option { self.get_server_password().ok().flatten() } pub fn server_password_hash(&self) -> Option { self.server.as_ref().and_then(|s| s.password_hash.clone()) } pub fn server_address(&self) -> Option { self.server.as_ref().and_then(|s| s.address.clone()) } pub fn server_port(&self) -> Option { self.server.as_ref().and_then(|s| s.port) } pub fn compression(&self) -> Option { self.compression_plugin.as_ref().map(|c| c.name.clone()) } pub fn meta_plugins_names(&self) -> Vec { self.meta_plugins.as_ref() .map(|plugins| plugins.iter().map(|p| p.name.clone()).collect()) .unwrap_or_default() } }