feat: update config system and list format structure
Co-authored-by: aider (openai/andrew/openrouter/qwen/qwen3-coder) <aider@aider.chat>
This commit is contained in:
@@ -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"
|
||||
|
||||
250
src/config.rs
250
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<PathBuf>,
|
||||
pub list_format: Option<String>,
|
||||
pub human_readable: Option<bool>,
|
||||
pub output_format: Option<String>,
|
||||
pub verbose: Option<u8>,
|
||||
pub quiet: Option<bool>,
|
||||
pub force: Option<bool>,
|
||||
pub server: Option<ServerConfig>,
|
||||
pub compression_plugin: Option<CompressionPluginConfig>,
|
||||
pub meta_plugins: Option<Vec<MetaPluginConfig>>,
|
||||
pub digest: Option<String>,
|
||||
#[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<ColumnConfig>,
|
||||
pub human_readable: bool,
|
||||
pub output_format: Option<String>,
|
||||
pub verbose: u8,
|
||||
pub quiet: bool,
|
||||
pub force: bool,
|
||||
pub server_password: Option<String>,
|
||||
pub server_password_hash: Option<String>,
|
||||
pub server_address: Option<String>,
|
||||
pub server_port: Option<u16>,
|
||||
pub compression: Option<String>,
|
||||
pub server: Option<ServerConfig>,
|
||||
pub compression_plugin: Option<CompressionPluginConfig>,
|
||||
pub meta_plugins: Option<Vec<MetaPluginConfig>>,
|
||||
pub digest: Option<String>,
|
||||
pub meta_plugins: Vec<String>,
|
||||
pub meta: Vec<KeyValue>,
|
||||
}
|
||||
|
||||
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<Self> {
|
||||
// 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<Self> {
|
||||
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());
|
||||
// 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 verbose = if args.options.verbose > 0 {
|
||||
args.options.verbose
|
||||
// Add 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 {
|
||||
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<ColumnConfig> = 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 {
|
||||
config.verbose.unwrap_or(0)
|
||||
name.clone()
|
||||
};
|
||||
|
||||
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(),
|
||||
ColumnConfig { name, label }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load configuration from a file
|
||||
pub fn from_file(path: &PathBuf) -> Result<Self> {
|
||||
debug!("CONFIG: Loading config from {:?}", path);
|
||||
|
||||
if !path.exists() {
|
||||
debug!("CONFIG: Config file does not exist, using defaults");
|
||||
return Ok(Config::default());
|
||||
.collect();
|
||||
config_builder = config_builder.set_override("list_format", columns)?;
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read config file: {:?}", path))?;
|
||||
if args.options.human_readable {
|
||||
config_builder = config_builder.set_override("human_readable", true)?;
|
||||
}
|
||||
|
||||
let config: Config = serde_yaml::from_str(&content)
|
||||
.with_context(|| format!("Failed to parse config file: {:?}", path))?;
|
||||
if let Some(output_format) = &args.options.output_format {
|
||||
config_builder = config_builder.set_override("output_format", output_format)?;
|
||||
}
|
||||
|
||||
debug!("CONFIG: Loaded config: {:?}", config);
|
||||
Ok(config)
|
||||
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<MetaPluginConfig> = 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<Option<String>> {
|
||||
if let Some(server) = &self.server {
|
||||
// First check for password_file
|
||||
@@ -183,25 +192,30 @@ 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();
|
||||
// Helper methods to access configuration values
|
||||
pub fn server_password(&self) -> Option<String> {
|
||||
self.get_server_password().ok().flatten()
|
||||
}
|
||||
if other.list_format.is_some() {
|
||||
self.list_format = other.list_format.clone();
|
||||
|
||||
pub fn server_password_hash(&self) -> Option<String> {
|
||||
self.server.as_ref().and_then(|s| s.password_hash.clone())
|
||||
}
|
||||
if other.human_readable.is_some() {
|
||||
self.human_readable = other.human_readable;
|
||||
|
||||
pub fn server_address(&self) -> Option<String> {
|
||||
self.server.as_ref().and_then(|s| s.address.clone())
|
||||
}
|
||||
if other.server.is_some() {
|
||||
self.server = other.server.clone();
|
||||
|
||||
pub fn server_port(&self) -> Option<u16> {
|
||||
self.server.as_ref().and_then(|s| s.port)
|
||||
}
|
||||
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();
|
||||
|
||||
pub fn compression(&self) -> Option<String> {
|
||||
self.compression_plugin.as_ref().map(|c| c.name.clone())
|
||||
}
|
||||
|
||||
pub fn meta_plugins_names(&self) -> Vec<String> {
|
||||
self.meta_plugins.as_ref()
|
||||
.map(|plugins| plugins.iter().map(|p| p.name.clone()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
22
src/main.rs
22
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();
|
||||
|
||||
@@ -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_type.to_string()).with_style(Attr::Bold));
|
||||
title_row.add_cell(Cell::new(&column.label).with_style(Attr::Bold));
|
||||
}
|
||||
} else {
|
||||
title_row.add_cell(Cell::new(&column.label).with_style(Attr::Bold));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,21 +111,19 @@ 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(
|
||||
|
||||
Reference in New Issue
Block a user