feat: implement unified settings system

Co-authored-by: aider (openai/andrew/openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
This commit is contained in:
Andrew Phillips
2025-08-15 16:36:58 -03:00
parent 067cba703b
commit 56f4d8aad5
12 changed files with 283 additions and 140 deletions

View File

@@ -157,12 +157,6 @@ impl FromStr for OutputFormat {
}
}
pub fn get_output_format(args: &Args) -> OutputFormat {
args.options.output_format
.as_ref()
.and_then(|s| OutputFormat::from_str(s).ok())
.unwrap_or(OutputFormat::Table)
}
pub fn cmd_args_meta_plugin_types(cmd: &mut Command, args: &Args) -> Vec<MetaPluginType> {
let mut meta_plugin_types = Vec::new();
@@ -200,3 +194,83 @@ pub fn cmd_args_meta_plugin_types(cmd: &mut Command, args: &Args) -> Vec<MetaPlu
meta_plugin_types
}
pub fn settings_meta_plugin_types(cmd: &mut Command, settings: &crate::config::Settings) -> Vec<MetaPluginType> {
let mut meta_plugin_types = Vec::new();
// Handle comma-separated values in each meta_plugins argument
for meta_plugin_names_str in &settings.meta_plugins {
let meta_plugin_names: Vec<&str> = meta_plugin_names_str.split(',').collect();
for name in meta_plugin_names {
let trimmed_name = name.trim();
if trimmed_name.is_empty() {
continue;
}
// Try to find the MetaPluginType by meta name
let mut found = false;
for meta_plugin_type in MetaPluginType::iter() {
let mut meta_plugin = crate::meta_plugin::get_meta_plugin(meta_plugin_type.clone());
if meta_plugin.meta_name() == trimmed_name {
meta_plugin_types.push(meta_plugin_type);
found = true;
break;
}
}
if !found {
cmd.error(
ErrorKind::InvalidValue,
format!("Unknown meta plugin type: {}", trimmed_name),
)
.exit();
}
}
}
meta_plugin_types
}
pub fn settings_digest_type(cmd: &mut Command, settings: &crate::config::Settings) -> MetaPluginType {
let digest_name = settings
.digest
.clone()
.unwrap_or(MetaPluginType::DigestSha256.to_string());
let digest_type_opt = MetaPluginType::from_str(&digest_name);
if digest_type_opt.is_err() {
cmd.error(
ErrorKind::InvalidValue,
format!("Invalid digest algorithm '{}'. Use 'sha256' or 'md5'", digest_name),
)
.exit();
}
digest_type_opt.unwrap()
}
pub fn settings_compression_type(cmd: &mut Command, settings: &crate::config::Settings) -> CompressionType {
let compression_name = settings
.compression
.clone()
.unwrap_or(CompressionType::LZ4.to_string());
let compression_type_opt = CompressionType::from_str(&compression_name);
if compression_type_opt.is_err() {
cmd.error(
ErrorKind::InvalidValue,
format!("Invalid compression algorithm '{}'. Supported algorithms: lz4, gzip, xz, zstd", compression_name),
)
.exit();
}
compression_type_opt.unwrap()
}
pub fn settings_output_format(settings: &crate::config::Settings) -> OutputFormat {
settings.output_format
.as_ref()
.and_then(|s| OutputFormat::from_str(s).ok())
.unwrap_or(OutputFormat::Table)
}

View File

@@ -10,7 +10,8 @@ use rusqlite::Connection;
pub fn mode_delete(
cmd: &mut Command,
_args: &crate::Args,
_settings: &crate::config::Settings,
_config: &crate::config::Config,
ids: &mut Vec<i64>,
tags: &mut Vec<String>,
conn: &mut Connection,

View File

@@ -293,7 +293,8 @@ fn handle_diff_output(
pub fn mode_diff(
cmd: &mut Command,
_args: &crate::Args,
_settings: &crate::config::Settings,
_config: &crate::config::Config,
ids: &mut Vec<i64>,
tags: &mut Vec<String>,
conn: &mut rusqlite::Connection,

View File

@@ -10,7 +10,8 @@ use is_terminal::IsTerminal;
pub fn mode_get(
cmd: &mut Command,
args: &crate::Args,
settings: &crate::config::Settings,
_config: &crate::config::Config,
ids: &mut Vec<i64>,
tags: &mut Vec<String>,
conn: &mut rusqlite::Connection,
@@ -23,7 +24,7 @@ pub fn mode_get(
}
let mut meta: std::collections::HashMap<String, String> = std::collections::HashMap::new();
for item in args.item.meta.iter() {
for item in settings.meta.iter() {
let item = item.clone();
meta.insert(item.key, item.value);
}
@@ -47,7 +48,7 @@ pub fn mode_get(
item_path.push(item_id.to_string());
// Determine if we should detect binary data
let mut detect_binary = !args.options.force && std::io::stdout().is_terminal();
let mut detect_binary = !settings.force && std::io::stdout().is_terminal();
// If we're detecting binary and there's binary metadata, check it
if detect_binary {

View File

@@ -19,7 +19,8 @@ use prettytable::{Attr, Cell, Row, Table};
pub fn mode_info(
cmd: &mut Command,
args: &crate::Args,
settings: &crate::config::Settings,
_config: &crate::config::Config,
ids: &mut Vec<i64>,
tags: &mut Vec<String>,
conn: &mut rusqlite::Connection,
@@ -32,7 +33,7 @@ pub fn mode_info(
}
let mut meta: std::collections::HashMap<String, String> = std::collections::HashMap::new();
for item in args.item.meta.iter() {
for item in settings.meta.iter() {
let item = item.clone();
meta.insert(item.key, item.value);
}
@@ -46,7 +47,7 @@ pub fn mode_info(
};
match item_maybe {
Some(item) => show_item(item, args, conn, data_path),
Some(item) => show_item(item, settings, conn, data_path),
None => Err(anyhow!("Unable to find matching item in database")),
}
}
@@ -67,7 +68,7 @@ struct ItemInfo {
fn show_item(
item: Item, // Using the provided struct definition
args: &crate::Args,
settings: &crate::config::Settings,
conn: &mut rusqlite::Connection,
data_path: PathBuf,
) -> anyhow::Result<()> {
@@ -78,10 +79,10 @@ fn show_item(
.map(|x| x.name)
.collect();
let output_format = get_output_format(args);
let output_format = crate::modes::common::settings_output_format(settings);
if output_format != OutputFormat::Table {
return show_item_structured(item, args, conn, data_path, output_format);
return show_item_structured(item, settings, conn, data_path, output_format);
}
let mut table = Table::new();
@@ -111,7 +112,7 @@ fn show_item(
]));
let size_cell = match item.size {
Some(size) => Cell::new(format_size(size as u64, args.options.human_readable).as_str()),
Some(size) => Cell::new(format_size(size as u64, settings.human_readable).as_str()),
None => Cell::new("Missing")
.with_style(Attr::ForegroundColor(prettytable::color::RED))
.with_style(Attr::Bold),
@@ -132,7 +133,7 @@ fn show_item(
let file_size_cell = match item_path_buf.metadata() {
Ok(metadata) => {
Cell::new(format_size(metadata.len(), args.options.human_readable).as_str())
Cell::new(format_size(metadata.len(), settings.human_readable).as_str())
}
Err(_) => Cell::new("Missing")
.with_style(Attr::ForegroundColor(prettytable::color::RED))
@@ -162,7 +163,7 @@ fn show_item(
fn show_item_structured(
item: Item,
args: &crate::Args,
settings: &crate::config::Settings,
conn: &mut rusqlite::Connection,
data_path: PathBuf,
output_format: OutputFormat,
@@ -178,12 +179,12 @@ fn show_item_structured(
let file_size = item_path_buf.metadata().map(|m| m.len()).ok();
let file_size_formatted = match file_size {
Some(size) => format_size(size, args.options.human_readable),
Some(size) => format_size(size, settings.human_readable),
None => "Missing".to_string(),
};
let stream_size_formatted = match item.size {
Some(size) => format_size(size as u64, args.options.human_readable),
Some(size) => format_size(size as u64, settings.human_readable),
None => "Missing".to_string(),
};

View File

@@ -27,7 +27,8 @@ struct ListItem {
pub fn mode_list(
cmd: &mut clap::Command,
args: &crate::Args,
settings: &crate::config::Settings,
_config: &crate::config::Config,
ids: &mut Vec<i64>,
tags: &Vec<String>,
conn: &mut rusqlite::Connection,
@@ -42,7 +43,7 @@ pub fn mode_list(
}
let mut meta: std::collections::HashMap<String, String> = std::collections::HashMap::new();
for item in args.item.meta.iter() {
for item in settings.meta.iter() {
let item = item.clone();
meta.insert(item.key, item.value);
}
@@ -71,16 +72,16 @@ pub fn mode_list(
// Fetch all metadata for all items in a single query
let meta_by_item = crate::db::get_meta_for_items(conn, &item_ids)?;
let output_format = get_output_format(args);
let output_format = crate::modes::common::settings_output_format(settings);
if output_format != OutputFormat::Table {
return show_list_structured(items, tags_by_item, meta_by_item, data_path, args, output_format);
return show_list_structured(items, tags_by_item, meta_by_item, data_path, settings, output_format);
}
let mut table = Table::new();
table.set_format(*prettytable::format::consts::FORMAT_CLEAN);
let list_format = args.options.list_format.split(",");
let list_format = settings.list_format.split(",");
let mut title_row = row!();
@@ -141,7 +142,7 @@ pub fn mode_list(
)),
ColumnType::Size => match item.size {
Some(size) => Cell::new_align(
&size_column(size as u64, args.options.human_readable, column_width),
&size_column(size as u64, settings.human_readable, column_width),
Alignment::RIGHT,
),
None => match item_path.metadata() {
@@ -158,7 +159,7 @@ pub fn mode_list(
},
ColumnType::FileSize => match item_path.metadata() {
Ok(metadata) => Cell::new_align(
&size_column(metadata.len(), args.options.human_readable, column_width),
&size_column(metadata.len(), settings.human_readable, column_width),
Alignment::RIGHT,
),
Err(_) => Cell::new_align("Missing", Alignment::RIGHT)
@@ -195,7 +196,7 @@ fn show_list_structured(
tags_by_item: std::collections::HashMap<i64, Vec<String>>,
meta_by_item: std::collections::HashMap<i64, std::collections::HashMap<String, String>>,
data_path: std::path::PathBuf,
args: &crate::Args,
settings: &crate::config::Settings,
output_format: OutputFormat,
) -> anyhow::Result<()> {
let mut list_items = Vec::new();
@@ -210,12 +211,12 @@ fn show_list_structured(
let file_size = item_path.metadata().map(|m| m.len()).ok();
let file_size_formatted = match file_size {
Some(size) => crate::modes::common::format_size(size, args.options.human_readable),
Some(size) => crate::modes::common::format_size(size, settings.human_readable),
None => "Missing".to_string(),
};
let size_formatted = match item.size {
Some(size) => crate::modes::common::format_size(size as u64, args.options.human_readable),
Some(size) => crate::modes::common::format_size(size as u64, settings.human_readable),
None => "Unknown".to_string(),
};

View File

@@ -4,7 +4,7 @@ use log::debug;
use std::io::{Read, Write, IsTerminal};
// Import the missing functions from common module
use crate::modes::common::{cmd_args_digest_type, cmd_args_compression_type, cmd_args_meta_plugin_types};
use crate::modes::common::{settings_digest_type, settings_compression_type, settings_meta_plugin_types};
fn validate_save_args(cmd: &mut Command, ids: &Vec<i64>) {
if !ids.is_empty() {
@@ -24,18 +24,18 @@ fn initialize_tags(tags: &mut Vec<String>) {
fn setup_compression_and_plugins(
cmd: &mut Command,
args: &crate::Args,
settings: &crate::config::Settings,
) -> (crate::compression_engine::CompressionType, Box<dyn crate::compression_engine::CompressionEngine>, Vec<Box<dyn crate::meta_plugin::MetaPlugin>>) {
let digest_type = cmd_args_digest_type(cmd, &args);
let digest_type = settings_digest_type(cmd, settings);
debug!("MAIN: Digest type: {:?}", digest_type);
let compression_type = cmd_args_compression_type(cmd, &args);
let compression_type = settings_compression_type(cmd, settings);
debug!("MAIN: Compression type: {:?}", compression_type);
let compression_engine =
crate::compression_engine::get_compression_engine(compression_type.clone()).expect("Unable to get compression engine");
// Start with meta plugin types from command line
let mut meta_plugin_types: Vec<crate::meta_plugin::MetaPluginType> = cmd_args_meta_plugin_types(cmd, &args);
// Start with meta plugin types from settings
let mut meta_plugin_types: Vec<crate::meta_plugin::MetaPluginType> = settings_meta_plugin_types(cmd, settings);
debug!("MAIN: Meta plugin types: {:?}", meta_plugin_types);
// Convert digest type to meta plugin type and add to the list if needed
@@ -78,7 +78,7 @@ fn setup_compression_and_plugins(
fn create_and_log_item(
conn: &mut rusqlite::Connection,
args: &crate::Args,
settings: &crate::config::Settings,
tags: &Vec<String>,
compression_type: &crate::compression_engine::CompressionType,
) -> Result<crate::db::Item, anyhow::Error> {
@@ -93,7 +93,7 @@ fn create_and_log_item(
item.id = Some(id);
debug!("MAIN: Added item {:?}", item.clone());
if !args.options.quiet {
if !settings.quiet {
if std::io::stderr().is_terminal() {
let mut t = term::stderr().unwrap();
t.reset().unwrap_or(());
@@ -121,7 +121,7 @@ fn create_and_log_item(
fn setup_item_metadata(
conn: &mut rusqlite::Connection,
_args: &crate::Args,
_settings: &crate::config::Settings,
item: &crate::db::Item,
tags: &Vec<String>,
) -> Result<(), anyhow::Error> {
@@ -129,7 +129,7 @@ fn setup_item_metadata(
Ok(())
}
fn collect_item_meta(args: &crate::Args) -> std::collections::HashMap<String, String> {
fn collect_item_meta(settings: &crate::config::Settings) -> std::collections::HashMap<String, String> {
let mut item_meta: std::collections::HashMap<String, String> = crate::modes::common::get_meta_from_env();
if let Ok(hostname) = gethostname::gethostname().into_string() {
@@ -138,7 +138,7 @@ fn collect_item_meta(args: &crate::Args) -> std::collections::HashMap<String, St
}
}
for item in args.item.meta.iter() {
for item in settings.meta.iter() {
let item = item.clone();
item_meta.insert(item.key, item.value);
}
@@ -230,7 +230,8 @@ fn finalize_meta_plugins(
pub fn mode_save(
cmd: &mut Command,
args: &crate::Args,
settings: &crate::config::Settings,
_config: &crate::config::Config,
ids: &mut Vec<i64>,
tags: &mut Vec<String>,
conn: &mut rusqlite::Connection,
@@ -239,14 +240,14 @@ pub fn mode_save(
validate_save_args(cmd, ids);
initialize_tags(tags);
let (compression_type, compression_engine, mut meta_plugins) = setup_compression_and_plugins(cmd, args);
let (compression_type, compression_engine, mut meta_plugins) = setup_compression_and_plugins(cmd, settings);
let mut item = create_and_log_item(conn, args, tags, &compression_type)?;
setup_item_metadata(conn, args, &item, tags)?; // Pass mutable reference
let mut item = create_and_log_item(conn, settings, tags, &compression_type)?;
setup_item_metadata(conn, settings, &item, tags)?; // Pass mutable reference
// Save as much as possible in case something breaks - don't use transactions
// This allows partial saves to succeed even if some metadata operations fail
let item_meta = collect_item_meta(args);
let item_meta = collect_item_meta(settings);
let item_id = item.id.ok_or_else(|| anyhow!("Item missing ID"))?;
for kv in item_meta.iter() {

View File

@@ -20,26 +20,28 @@ pub use common::{ServerConfig, AppState, logging_middleware, create_auth_middlew
pub fn mode_server(
_cmd: &mut Command,
args: &crate::Args,
settings: &crate::config::Settings,
config: &crate::config::Config,
conn: &mut rusqlite::Connection,
data_path: PathBuf,
) -> Result<()> {
let server_address = args.mode.server.as_ref().unwrap();
let server_address = settings.get_server_address(&crate::args::Args::parse(), config)
.unwrap_or_else(|| "127.0.0.1:8080".to_string());
let config = ServerConfig {
address: server_address.clone(),
password: args.options.server_password.clone(),
let server_config = common::ServerConfig {
address: server_address,
password: settings.server_password.clone(),
};
// We need to move the connection into the async runtime
let rt = tokio::runtime::Runtime::new()?;
// Take ownership of the connection and move it into the async runtime
let owned_conn = std::mem::replace(conn, rusqlite::Connection::open_in_memory()?);
rt.block_on(run_server(config, owned_conn, data_path))
rt.block_on(run_server(server_config, owned_conn, data_path))
}
async fn run_server(
config: ServerConfig,
config: common::ServerConfig,
conn: rusqlite::Connection,
data_dir: PathBuf,
) -> Result<()> {

View File

@@ -119,15 +119,16 @@ fn build_meta_plugin_table(meta_plugin_info: &Vec<MetaPluginInfo>) -> Table {
pub fn mode_status(
_cmd: &mut Command,
args: &crate::Args,
settings: &crate::config::Settings,
_config: &crate::config::Config,
data_path: PathBuf,
db_path: PathBuf,
) -> Result<(), anyhow::Error> {
// Determine which meta plugins would be enabled for a save operation
let mut meta_plugin_types: Vec<MetaPluginType> = crate::modes::common::cmd_args_meta_plugin_types(_cmd, &args);
let mut meta_plugin_types: Vec<MetaPluginType> = crate::modes::common::settings_meta_plugin_types(_cmd, settings);
// Add digest type if specified
let digest_type = crate::modes::common::cmd_args_digest_type(_cmd, &args);
let digest_type = crate::modes::common::settings_digest_type(_cmd, settings);
let digest_meta_plugin_type = match digest_type {
crate::meta_plugin::MetaPluginType::DigestSha256 => Some(MetaPluginType::DigestSha256),
crate::meta_plugin::MetaPluginType::DigestMd5 => Some(MetaPluginType::DigestMd5),
@@ -140,7 +141,7 @@ pub fn mode_status(
}
}
let output_format = get_output_format(args);
let output_format = crate::modes::common::settings_output_format(settings);
let status_info = generate_status_info(data_path, db_path, &meta_plugin_types);
match output_format {

View File

@@ -13,7 +13,8 @@ use rusqlite::Connection;
pub fn mode_update(
cmd: &mut Command,
args: &crate::Args,
settings: &crate::config::Settings,
_config: &crate::config::Config,
ids: &mut Vec<i64>,
tags: &mut Vec<String>,
conn: &mut Connection,
@@ -71,7 +72,7 @@ pub fn mode_update(
}
}
let digest_type = cmd_args_digest_type(cmd, args);
let digest_type = crate::modes::common::settings_digest_type(cmd, settings);
let digest_meta = get_digest_type_meta(digest_type.clone());
let digest_value = db::get_item_meta_value(&tx, &item, digest_meta)?;
@@ -115,9 +116,9 @@ pub fn mode_update(
}
}
if !args.item.meta.is_empty() {
if !settings.meta.is_empty() {
debug!("MAIN: Updating item meta");
for kv in args.item.meta.iter() {
for kv in settings.meta.iter() {
let meta = db::Meta {
id: item_id,
name: kv.key.to_string(),