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, Serialize)] pub struct ColumnConfig { pub name: String, pub label: String, #[serde(default)] pub align: ColumnAlignment, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum ColumnAlignment { #[default] Left, Right, } impl<'de> serde::Deserialize<'de> for ColumnConfig { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { #[derive(Deserialize)] struct Helper { name: String, label: Option, #[serde(default)] align: ColumnAlignment, } let helper = Helper::deserialize(deserializer)?; let label = helper.label.unwrap_or_else(|| helper.name.clone()); Ok(ColumnConfig { name: helper.name, label, align: helper.align, }) } } #[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 { 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 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: "id".to_string() }, ColumnConfig { name: "time".to_string(), label: "time".to_string() }, ColumnConfig { name: "size".to_string(), label: "size".to_string() }, ColumnConfig { name: "tags".to_string(), label: "tags".to_string() }, ColumnConfig { name: "meta:hostname".to_string(), label: "hostname".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 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() } }