use std::fmt;
use directories::ProjectDirs;
use crate::EngineConfig;
const CONFIG_FILENAME: &str = "vtx-engine.toml";
#[derive(Debug)]
pub enum ConfigError {
Io(std::io::Error),
Parse(String),
NoProjectDir,
Serialize(String),
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConfigError::Io(e) => write!(f, "I/O error: {}", e),
ConfigError::Parse(s) => write!(f, "Parse error: {}", s),
ConfigError::NoProjectDir => write!(f, "Cannot determine config directory"),
ConfigError::Serialize(s) => write!(f, "Serialization error: {}", s),
}
}
}
impl std::error::Error for ConfigError {}
impl EngineConfig {
pub fn load(app_name: &str) -> Result<EngineConfig, ConfigError> {
let path = config_path(app_name)?;
if !path.exists() {
return Ok(EngineConfig::default());
}
let content = std::fs::read_to_string(&path).map_err(ConfigError::Io)?;
toml::from_str(&content).map_err(|e| ConfigError::Parse(e.to_string()))
}
pub fn save(&self, app_name: &str) -> Result<(), ConfigError> {
let path = config_path(app_name)?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(ConfigError::Io)?;
}
let content =
toml::to_string_pretty(self).map_err(|e| ConfigError::Serialize(e.to_string()))?;
std::fs::write(&path, content).map_err(ConfigError::Io)
}
}
fn config_path(app_name: &str) -> Result<std::path::PathBuf, ConfigError> {
if app_name.is_empty() {
return Err(ConfigError::NoProjectDir);
}
let dirs = ProjectDirs::from("", "", app_name).ok_or(ConfigError::NoProjectDir)?;
Ok(dirs.config_dir().join(CONFIG_FILENAME))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn partial_toml_without_mic_gain_gets_default() {
let toml = r#"recording_mode = "echo_cancel""#;
let config: EngineConfig = toml::from_str(toml).expect("should parse");
assert_eq!(config.mic_gain_db, 0.0);
}
#[test]
fn mic_gain_db_round_trips() {
let mut config = EngineConfig::default();
config.mic_gain_db = 6.0;
let toml_str = toml::to_string_pretty(&config).expect("should serialize");
let loaded: EngineConfig = toml::from_str(&toml_str).expect("should deserialize");
assert_eq!(loaded.mic_gain_db, 6.0);
}
#[test]
fn partial_toml_without_agc_gets_default_disabled() {
let toml = r#"recording_mode = "echo_cancel""#;
let config: EngineConfig = toml::from_str(toml).expect("should parse");
assert!(!config.agc.enabled, "agc.enabled should default to false");
assert_eq!(config.agc.target_level_db, -18.0);
assert_eq!(config.agc.attack_time_ms, 10.0);
assert_eq!(config.agc.release_time_ms, 200.0);
assert_eq!(config.agc.min_gain_db, -6.0);
assert_eq!(config.agc.max_gain_db, 30.0);
}
#[test]
fn agc_config_round_trips_through_toml() {
let mut config = EngineConfig::default();
config.agc = crate::AgcConfig {
enabled: true,
target_level_db: -20.0,
attack_time_ms: 15.0,
release_time_ms: 250.0,
min_gain_db: -3.0,
max_gain_db: 24.0,
gate_threshold_db: -45.0,
boost_threshold_db: -38.0,
gate_hold_time_ms: 75.0,
};
let toml_str = toml::to_string_pretty(&config).expect("should serialize");
let loaded: EngineConfig = toml::from_str(&toml_str).expect("should deserialize");
assert!(loaded.agc.enabled);
assert_eq!(loaded.agc.target_level_db, -20.0);
assert_eq!(loaded.agc.attack_time_ms, 15.0);
assert_eq!(loaded.agc.release_time_ms, 250.0);
assert_eq!(loaded.agc.min_gain_db, -3.0);
assert_eq!(loaded.agc.max_gain_db, 24.0);
}
}