diff --git a/Cargo.toml b/Cargo.toml index f2550d0..a563b89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ axum = "0.8.4" base64 = "0.22.1" chrono = "0.4.26" clap = { version = "4.3.10", features = ["derive", "env"] } +config = "0.14.0" directories = "6.0.0" dns-lookup = "2.0.2" enum-map = "2.6.1" diff --git a/src/config.rs b/src/config.rs index 3f7184c..55e9110 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,19 +5,10 @@ use serde::{Deserialize, Serialize}; use log::debug; use crate::args::{Args, KeyValue}; -#[derive(Debug, Clone, Deserialize, Serialize, Default)] -pub struct Config { - pub dir: Option, - pub list_format: Option, - pub human_readable: Option, - pub output_format: Option, - pub verbose: Option, - pub quiet: Option, - pub force: Option, - pub server: Option, - pub compression_plugin: Option, - pub meta_plugins: Option>, - pub digest: Option, +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ColumnConfig { + pub name: String, + pub label: String, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -40,119 +31,137 @@ pub struct MetaPluginConfig { } /// Unified settings that merges config file and CLI arguments -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize)] pub struct Settings { pub dir: PathBuf, - pub list_format: String, + pub list_format: Vec, pub human_readable: bool, pub output_format: Option, pub verbose: u8, pub quiet: bool, pub force: bool, - pub server_password: Option, - pub server_password_hash: Option, - pub server_address: Option, - pub server_port: Option, - pub compression: Option, + pub server: Option, + pub compression_plugin: Option, + pub meta_plugins: Option>, pub digest: Option, - pub meta_plugins: Vec, - pub meta: Vec, } impl Settings { /// Create unified settings from config and args with proper priority - pub fn from_config_and_args(config: &Config, args: &Args, default_dir: PathBuf) -> Result { - // Apply priority: CLI args > env vars > config file > defaults - - let dir = args.options.dir.clone() - .or_else(|| config.dir.clone()) - .unwrap_or(default_dir); - - let list_format = if args.options.list_format != "id,time,size,tags,meta:hostname" { - args.options.list_format.clone() + pub fn new(args: &Args, default_dir: PathBuf) -> Result { + 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 { - config.list_format.clone() - .unwrap_or_else(|| "id,time,size,tags,meta:hostname".to_string()) + Self::default_config_path().unwrap_or_else(|_| PathBuf::from("~/.config/keep/config.yml")) }; - let human_readable = args.options.human_readable || config.human_readable.unwrap_or(false); + let mut config_builder = config::Config::builder(); - let output_format = args.options.output_format.clone() - .or_else(|| config.output_format.clone()); - - let verbose = if args.options.verbose > 0 { - args.options.verbose - } else { - config.verbose.unwrap_or(0) - }; - - let quiet = args.options.quiet || config.quiet.unwrap_or(false); - let force = args.options.force || config.force.unwrap_or(false); - - let server_password = args.options.server_password.clone() - .or_else(|| config.get_server_password().ok().flatten()); - - let server_password_hash = args.options.server_password_hash.clone() - .or_else(|| config.server.as_ref().and_then(|s| s.password_hash.clone())); - - let server_address = args.mode.server_address.clone() - .or_else(|| config.server.as_ref().and_then(|s| s.address.clone())); - - let server_port = args.mode.server_port - .or_else(|| config.server.as_ref().and_then(|s| s.port)); - - let compression = args.item.compression.clone() - .or_else(|| config.compression_plugin.as_ref().map(|c| c.name.clone())); - - let digest = args.item.digest.clone() - .or_else(|| config.digest.clone()); - - let meta_plugins = if !args.item.meta_plugins.is_empty() { - args.item.meta_plugins.clone() - } else { - config.meta_plugins.as_ref() - .map(|plugins| plugins.iter().map(|p| p.name.clone()).collect()) - .unwrap_or_default() - }; - - Ok(Settings { - dir, - list_format, - human_readable, - output_format, - verbose, - quiet, - force, - server_password, - server_password_hash, - server_address, - server_port, - compression, - digest, - meta_plugins, - meta: args.item.meta.clone(), - }) - } -} - -impl Config { - /// Load configuration from a file - pub fn from_file(path: &PathBuf) -> Result { - debug!("CONFIG: Loading config from {:?}", path); - - if !path.exists() { - debug!("CONFIG: Config file does not exist, using defaults"); - return Ok(Config::default()); + // Load config file if it exists + if config_path.exists() { + config_builder = config_builder.add_source(config::File::from(config_path.clone()).required(false)); } - - let content = fs::read_to_string(path) - .with_context(|| format!("Failed to read config file: {:?}", path))?; - let config: Config = serde_yaml::from_str(&content) - .with_context(|| format!("Failed to parse config file: {:?}", path))?; + // Add environment variables + config_builder = config_builder.add_source(config::Environment::with_prefix("KEEP").separator("__")); - debug!("CONFIG: Loaded config: {:?}", config); - Ok(config) + // Override with CLI args + if let Some(dir) = &args.options.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 name = parts[0].to_string(); + let label = if parts.len() > 1 { + parts[1].to_string() + } else { + name.clone() + }; + ColumnConfig { name, label } + }) + .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)?; + } + + 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)?; + } + + if let Some(server_password_hash) = &args.options.server_password_hash { + config_builder = config_builder.set_override("server.password_hash", server_password_hash)?; + } + + if let Some(server_address) = &args.mode.server_address { + config_builder = config_builder.set_override("server.address", server_address)?; + } + + 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)?; + } + + if let Some(digest) = &args.item.digest { + config_builder = config_builder.set_override("digest", digest)?; + } + + if !args.item.meta_plugins.is_empty() { + let meta_plugins: Vec = args.item.meta_plugins + .iter() + .map(|name| MetaPluginConfig { name: name.clone() }) + .collect(); + config_builder = config_builder.set_override("meta_plugins", meta_plugins)?; + } + + let config = config_builder.build()?; + let mut settings: Settings = config.try_deserialize()?; + + // Set defaults for list_format if not provided + if settings.list_format.is_empty() { + 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 + if settings.dir == PathBuf::new() { + settings.dir = default_dir; + } + + Ok(settings) } /// Get the default config file path @@ -161,7 +170,7 @@ impl Config { Ok(xdg_dirs.get_config_home().join("config.yml")) } - /// Read password from password_file or directly from config if configured + /// 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 @@ -182,26 +191,31 @@ impl Config { } Ok(None) } - - /// Merge this config with another, giving priority to the other config - pub fn merge_with(&mut self, other: &Config) { - if other.dir.is_some() { - self.dir = other.dir.clone(); - } - if other.list_format.is_some() { - self.list_format = other.list_format.clone(); - } - if other.human_readable.is_some() { - self.human_readable = other.human_readable; - } - if other.server.is_some() { - self.server = other.server.clone(); - } - if other.compression_plugin.is_some() { - self.compression_plugin = other.compression_plugin.clone(); - } - if other.meta_plugins.is_some() { - self.meta_plugins = other.meta_plugins.clone(); - } + + // 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() } } diff --git a/src/main.rs b/src/main.rs index 870417a..c12efe9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,30 +51,16 @@ fn main() -> Result<(), Error> { debug!("MAIN: Start"); - // Load configuration with priority: CLI args > env vars > config file > defaults - 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 { - Config::default_config_path().unwrap_or_else(|_| PathBuf::from("~/.config/keep/config.yml")) - }; - - let mut config = Config::from_file(&config_path).unwrap_or_else(|e| { - debug!("CONFIG: Failed to load config: {}, using defaults", e); - Config::default() - }); - - debug!("MAIN: Loaded config: {:?}", config); - // Determine default data directory let default_dir = match proj_dirs { Some(ref proj_dirs) => proj_dirs.data_dir().to_path_buf(), None => return Err(anyhow!("Unable to determine data directory")), }; - // Create unified settings - let settings = Settings::from_config_and_args(&config, &args, default_dir)?; + // Create unified settings using the new config system + let settings = Settings::new(&args, default_dir)?; + + debug!("MAIN: Loaded settings: {:?}", settings); let ids = &mut Vec::new(); let tags = &mut Vec::new(); diff --git a/src/modes/list.rs b/src/modes/list.rs index 356437d..b70f741 100644 --- a/src/modes/list.rs +++ b/src/modes/list.rs @@ -82,23 +82,21 @@ pub fn mode_list( let mut table = Table::new(); table.set_format(*prettytable::format::consts::FORMAT_CLEAN); - let list_format = settings.list_format.split(","); - let mut title_row = row!(); - for column in list_format.clone() { - let mut column_format = column.split(":"); - let column_name = column_format.next().expect("Unable to parse column name"); - let column_type = ColumnType::from_str(column_name) - .map_err(|_| anyhow!("Unknown column {:?}", column_name))?; + for column in &settings.list_format { + let column_type = ColumnType::from_str(&column.name) + .map_err(|_| anyhow!("Unknown column {:?}", column.name))?; if column_type == ColumnType::Meta { - let meta_name = column_format - .next() - .expect("Unable to parse metadata name for meta column"); - title_row.add_cell(Cell::new(meta_name).with_style(Attr::Bold)); + let parts: Vec<&str> = column.name.split(':').collect(); + if parts.len() > 1 { + title_row.add_cell(Cell::new(parts[1]).with_style(Attr::Bold)); + } else { + title_row.add_cell(Cell::new(&column.label).with_style(Attr::Bold)); + } } else { - title_row.add_cell(Cell::new(&column_type.to_string()).with_style(Attr::Bold)); + title_row.add_cell(Cell::new(&column.label).with_style(Attr::Bold)); } } @@ -113,22 +111,20 @@ pub fn mode_list( let mut table_row = Row::new(vec![]); - for column in list_format.clone() { - let mut column_format = column.split(":"); - let column_name = column_format.next().expect("Unable to parse column name"); - let column_type = ColumnType::from_str(column_name) - .unwrap_or_else(|_| panic!("Unknown column {:?}", column_name)); + for column in &settings.list_format { + let column_type = ColumnType::from_str(&column.name) + .unwrap_or_else(|_| panic!("Unknown column {:?}", column.name)); + let mut meta_name: Option<&str> = None; + let column_width = 0; // We're not supporting width in the new format if column_type == ColumnType::Meta { - meta_name = column_format.next(); + let parts: Vec<&str> = column.name.split(':').collect(); + if parts.len() > 1 { + meta_name = Some(parts[1]); + } } - let column_width: usize = match column_format.next() { - Some(len) => len.parse().unwrap_or(0), - None => 0, - }; - let cell = match column_type { ColumnType::Id => Cell::new_align( &string_column(item.id.unwrap_or(0).to_string(), column_width),