use anyhow::Result; use directories::ProjectDirs; use ratatui::style::Color; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AppConfig { #[serde(default)] pub network: NetworkConfig, #[serde(default)] pub media: MediaConfig, #[serde(default)] pub files: FileConfig, #[serde(default)] pub ui: UiConfig, } impl Default for AppConfig { fn default() -> Self { Self { network: NetworkConfig::default(), media: MediaConfig::default(), files: FileConfig::default(), ui: UiConfig::default(), } } } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct FileConfig { #[serde(default = "default_timeout")] pub default_timeout_seconds: u64, } fn default_timeout() -> u64 { 60 } impl Default for FileConfig { fn default() -> Self { Self { default_timeout_seconds: 60, } } } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct NetworkConfig { pub topic: Option, } impl Default for NetworkConfig { fn default() -> Self { Self { topic: None } } } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct MediaConfig { #[serde(default = "default_bitrate")] pub mic_bitrate: u32, #[serde(default)] pub input_device: Option, #[serde(default)] pub output_device: Option, #[serde(default = "default_volume")] pub master_volume: f32, #[serde(default = "default_true")] pub noise_suppression: bool, } fn default_bitrate() -> u32 { 128000 } fn default_volume() -> f32 { 1.0 } fn default_true() -> bool { true } impl Default for MediaConfig { fn default() -> Self { Self { mic_bitrate: 128000, input_device: None, output_device: None, master_volume: 1.0, noise_suppression: true, } } } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UiConfig { pub text: String, #[serde(default = "default_cyan")] pub chat_border: String, #[serde(default = "default_cyan")] pub peer_border: String, #[serde(default = "default_yellow")] pub transfer_border: String, pub self_name: String, pub system_msg: String, pub time: String, #[serde(default)] pub success: String, #[serde(default)] pub error: String, #[serde(default)] pub warning: String, } impl Default for UiConfig { fn default() -> Self { Self { text: "white".to_string(), self_name: "green".to_string(), system_msg: "yellow".to_string(), time: "dark_gray".to_string(), chat_border: "cyan".to_string(), peer_border: "cyan".to_string(), transfer_border: "yellow".to_string(), success: "green".to_string(), error: "red".to_string(), warning: "yellow".to_string(), } } } impl AppConfig { pub fn load() -> Result { let config_path = Self::get_config_path(); if !config_path.exists() { // Create default config if it doesn't exist let default_config = Self::default(); if let Some(parent) = config_path.parent() { fs::create_dir_all(parent)?; } let toml = toml::to_string_pretty(&default_config)?; fs::write(&config_path, toml)?; return Ok(default_config); } let content = fs::read_to_string(&config_path)?; let config: AppConfig = toml::from_str(&content)?; Ok(config) } fn get_config_path() -> PathBuf { if let Some(proj_dirs) = ProjectDirs::from("com", "p2p-chat", "p2p-chat") { proj_dirs.config_dir().join("config.toml") } else { PathBuf::from("config.toml") } } /// Save the current configuration to disk. pub fn save(&self) -> Result<()> { let config_path = Self::get_config_path(); if let Some(parent) = config_path.parent() { fs::create_dir_all(parent)?; } let toml = toml::to_string_pretty(self)?; fs::write(&config_path, toml)?; Ok(()) } } // Helper to parse color strings pub fn parse_color(color_str: &str) -> Color { if color_str.starts_with('#') { let hex = color_str.trim_start_matches('#'); if let Ok(val) = u32::from_str_radix(hex, 16) { let (r, g, b) = if hex.len() == 3 { // #RGB -> #RRGGBB let r = ((val >> 8) & 0xF) * 17; let g = ((val >> 4) & 0xF) * 17; let b = (val & 0xF) * 17; (r as u8, g as u8, b as u8) } else { // #RRGGBB let r = (val >> 16) & 0xFF; let g = (val >> 8) & 0xFF; let b = val & 0xFF; (r as u8, g as u8, b as u8) }; return Color::Rgb(r, g, b); } } match color_str.to_lowercase().as_str() { "black" => Color::Black, "red" => Color::Red, "green" => Color::Green, "yellow" => Color::Yellow, "blue" => Color::Blue, "magenta" => Color::Magenta, "cyan" => Color::Cyan, "gray" => Color::Gray, "dark_gray" | "darkgray" => Color::DarkGray, "light_red" | "lightred" => Color::LightRed, "light_green" | "lightgreen" => Color::LightGreen, "light_yellow" | "lightyellow" => Color::LightYellow, "light_blue" | "lightblue" => Color::LightBlue, "light_magenta" | "lightmagenta" => Color::LightMagenta, "light_cyan" | "lightcyan" => Color::LightCyan, "white" => Color::White, _ => { // Try hex parsing if needed, but for now fallback to White Color::White } } } // Runtime Theme struct derived from config #[derive(Debug, Clone)] pub struct Theme { pub chat_border: Color, pub peer_border: Color, pub transfer_border: Color, pub text: Color, pub self_name: Color, pub system_msg: Color, pub time: Color, pub success: Color, pub error: Color, pub warning: Color, } impl From for Theme { fn from(cfg: UiConfig) -> Self { Self { chat_border: parse_color(&cfg.chat_border), peer_border: parse_color(&cfg.peer_border), transfer_border: parse_color(&cfg.transfer_border), text: parse_color(&cfg.text), self_name: parse_color(&cfg.self_name), system_msg: parse_color(&cfg.system_msg), time: parse_color(&cfg.time), success: parse_color(&cfg.success), error: parse_color(&cfg.error), warning: parse_color(&cfg.warning), } } } fn default_yellow() -> String { "yellow".to_string() } fn default_cyan() -> String { "cyan".to_string() }