feat: add config system with --config argument and priority-based configuration

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:31:57 -03:00
parent 5689c3e5ef
commit 067cba703b
4 changed files with 180 additions and 3 deletions

View File

@@ -0,0 +1,96 @@
use std::path::PathBuf;
use std::fs;
use anyhow::{Result, Context};
use serde::{Deserialize, Serialize};
use log::debug;
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
pub struct Config {
pub dir: Option<PathBuf>,
pub list_format: Option<String>,
pub human_readable: Option<bool>,
pub server: Option<ServerConfig>,
pub compression_plugin: Option<CompressionPluginConfig>,
pub meta_plugins: Option<Vec<MetaPluginConfig>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerConfig {
pub address: Option<String>,
pub port: Option<u16>,
pub password_file: Option<PathBuf>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CompressionPluginConfig {
pub name: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MetaPluginConfig {
pub name: String,
}
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());
}
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))?;
debug!("CONFIG: Loaded config: {:?}", config);
Ok(config)
}
/// Get the default config file path
pub fn default_config_path() -> Result<PathBuf> {
let xdg_dirs = xdg::BaseDirectories::with_prefix("keep")?;
Ok(xdg_dirs.get_config_home().join("config.yml"))
}
/// Read password from password_file if configured
pub fn get_server_password(&self) -> Result<Option<String>> {
if let Some(server) = &self.server {
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));
}
}
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();
}
}
}