260 lines
6.9 KiB
Rust
260 lines
6.9 KiB
Rust
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<String>,
|
|
}
|
|
|
|
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<String>,
|
|
#[serde(default)]
|
|
pub output_device: Option<String>,
|
|
#[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<Self> {
|
|
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<UiConfig> 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()
|
|
}
|