use std::path::Path;
use serde::Deserialize;
#[derive(Debug, Deserialize, Default)]
#[serde(default)]
pub struct McpConfig {
pub logging: LoggingConfig,
}
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct LoggingConfig {
pub level: String,
pub coloured: bool,
pub output: String,
pub report_caller: bool,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: "info".into(),
coloured: true,
output: "stderr".into(),
report_caller: false,
}
}
}
impl LoggingConfig {
pub fn to_twyg_opts(&self) -> twyg::Opts {
let output = match self.output.as_str() {
"stdout" => twyg::Output::Stdout,
_ => twyg::Output::Stderr,
};
let level = match self.level.as_str() {
"trace" => twyg::LogLevel::Trace,
"debug" => twyg::LogLevel::Debug,
"warn" => twyg::LogLevel::Warn,
"error" => twyg::LogLevel::Error,
_ => twyg::LogLevel::Info,
};
let colors = twyg::Colors {
timestamp: Some(twyg::Color::hi_black()),
..Default::default()
};
twyg::OptsBuilder::new()
.coloured(self.coloured)
.output(output)
.level(level)
.report_caller(self.report_caller)
.timestamp_format(twyg::TSFormat::Simple)
.colors(colors)
.build()
.unwrap_or_default()
}
}
impl McpConfig {
pub fn load() -> Self {
let path = dirs::home_dir()
.map(|h| h.join(".nms-copilot/config.toml"))
.unwrap_or_default();
Self::load_from(&path)
}
pub fn load_from(path: &Path) -> Self {
if !path.exists() {
return Self::default();
}
match std::fs::read_to_string(path) {
Ok(content) => toml::from_str(&content).unwrap_or_default(),
Err(_) => Self::default(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = McpConfig::default();
assert_eq!(config.logging.level, "info");
assert!(config.logging.coloured);
assert_eq!(config.logging.output, "stderr");
assert!(!config.logging.report_caller);
}
#[test]
fn test_parse_logging_config() {
let toml = r#"
[logging]
level = "debug"
coloured = false
output = "stdout"
report_caller = true
"#;
let config: McpConfig = toml::from_str(toml).unwrap();
assert_eq!(config.logging.level, "debug");
assert!(!config.logging.coloured);
assert_eq!(config.logging.output, "stdout");
assert!(config.logging.report_caller);
}
#[test]
fn test_parse_empty_config() {
let config: McpConfig = toml::from_str("").unwrap();
assert_eq!(config.logging.level, "info");
}
#[test]
fn test_to_twyg_opts() {
let config = LoggingConfig::default();
let opts = config.to_twyg_opts();
assert!(!format!("{:?}", opts).is_empty());
}
#[test]
fn test_load_nonexistent_returns_default() {
let config = McpConfig::load_from(std::path::Path::new("/nonexistent/config.toml"));
assert_eq!(config.logging.level, "info");
}
#[test]
fn test_unknown_sections_ignored() {
let toml = r#"
[save]
path = "/tmp/save"
[display]
emoji_glyphs = false
[logging]
level = "warn"
"#;
let config: McpConfig = toml::from_str(toml).unwrap();
assert_eq!(config.logging.level, "warn");
}
}