From 1098d58ff94ed8558d2e8cc896f89267d4f36e60 Mon Sep 17 00:00:00 2001 From: Andrew Phillips Date: Wed, 10 Sep 2025 18:11:48 -0300 Subject: [PATCH] refactor: Add server configs and default meta plugins Co-authored-by: aider (openai/andrew/openrouter/sonoma-sky-alpha) --- src/config.rs | 281 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 279 insertions(+), 2 deletions(-) diff --git a/src/config.rs b/src/config.rs index a89f17c..1057f5d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -166,7 +166,7 @@ pub struct MetaPluginConfig { } /// Unified settings that merges config file and CLI arguments -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Settings { #[serde(default)] pub dir: PathBuf, @@ -178,4 +178,281 @@ pub struct Settings { pub output_format: Option, #[serde(default)] pub quiet: bool, - #[serde \ No newline at end of file + #[serde(default)] + pub force: bool, + pub server: Option, + pub compression_plugin: Option, + pub meta_plugins: 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 { + let default_path = if let Some(home_dir) = std::env::var("HOME").ok() { + let mut path = PathBuf::from(home_dir); + path.push(".config"); + path.push("keep"); + path.push("config.yml"); + path + } else { + PathBuf::from("~/.config/keep/config.yml") + }; + debug!("CONFIG: Using default config path: {:?}", default_path); + default_path + }; + + 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"); + let env_source = config::Environment::with_prefix("KEEP").separator("__").ignore_empty(true); + config_builder = config_builder.add_source(env_source); + + // 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.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 !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(), + align: ColumnAlignment::Right, + max_len: None, + fg_color: None, + bg_color: None, + attributes: Vec::new(), + padding: None, + }, + ColumnConfig { + name: "time".to_string(), + label: "Time".to_string(), + align: ColumnAlignment::Right, + max_len: None, + fg_color: None, + bg_color: None, + attributes: Vec::new(), + padding: None, + }, + ColumnConfig { + name: "size".to_string(), + label: "Size".to_string(), + align: ColumnAlignment::Right, + max_len: None, + fg_color: None, + bg_color: None, + attributes: Vec::new(), + padding: None, + }, + ColumnConfig { + name: "meta:text_line_count".to_string(), + label: "Lines".to_string(), + align: ColumnAlignment::Right, + max_len: None, + fg_color: None, + bg_color: None, + attributes: Vec::new(), + padding: None, + }, + ColumnConfig { + name: "tags".to_string(), + label: "Tags".to_string(), + align: ColumnAlignment::Left, + max_len: None, + fg_color: None, + bg_color: None, + attributes: Vec::new(), + padding: None, + }, + ColumnConfig { + name: "meta:hostname_short".to_string(), + label: "Host".to_string(), + align: ColumnAlignment::Left, + max_len: None, + fg_color: None, + bg_color: None, + attributes: Vec::new(), + padding: None, + }, + ColumnConfig { + name: "meta:command".to_string(), + label: "Command".to_string(), + align: ColumnAlignment::Left, + max_len: None, + fg_color: None, + bg_color: None, + attributes: Vec::new(), + padding: None, + }, + ]; + } + + // Set default meta_plugins to include 'env' if not provided + if settings.meta_plugins.is_none() { + debug!("CONFIG: Setting default meta_plugins to include 'env'"); + settings.meta_plugins = Some(vec![ + MetaPluginConfig { + name: "env".to_string(), + options: std::collections::HashMap::new(), + outputs: std::collections::HashMap::new(), + } + ]); + } + + // 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()) + } + } + } + + pub fn default_dir() -> anyhow::Result { + let mut path = dirs::home_dir() + .ok_or_else(|| anyhow::anyhow!("No home directory found"))?; + path.push(".keep"); + if !path.exists() { + std::fs::create_dir_all(&path)?; + } + Ok(path) + } + + /// 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() + } +}