1use crate::core::constants::{DEFAULT_BUFFER_SIZE, DEFAULT_POLL_RATE};
3use crate::core::prelude::*;
4use crate::ui::color::AppColor;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::path::Path;
8
9#[derive(Debug, Serialize, Deserialize)]
10struct ConfigFile {
11 general: GeneralConfig,
12 #[serde(default)]
13 theme: Option<HashMap<String, ThemeDefinitionConfig>>,
14 language: LanguageConfig,
15}
16
17#[derive(Debug, Serialize, Deserialize)]
18struct GeneralConfig {
19 max_messages: usize,
20 typewriter_delay: u64,
21 input_max_length: usize,
22 max_history: usize,
23 poll_rate: u64,
24 log_level: String,
25 #[serde(default = "default_theme")]
26 current_theme: String,
27}
28
29#[derive(Debug, Serialize, Deserialize)]
30struct LanguageConfig {
31 current: String,
32}
33
34#[derive(Debug, Serialize, Deserialize, Clone)]
35struct ThemeDefinitionConfig {
36 input_text: String,
37 input_bg: String,
38 output_text: String,
39 output_bg: String,
40 #[serde(default = "default_prefix")]
41 input_cursor_prefix: String,
42 #[serde(default = "default_input_color")]
43 input_cursor_color: String,
44 #[serde(default = "default_cursor")]
45 input_cursor: String,
46 #[serde(default = "default_cursor")]
47 output_cursor: String,
48 #[serde(default = "default_output_color")]
49 output_cursor_color: String,
50}
51
52fn default_theme() -> String {
54 "dark".into()
55}
56fn default_prefix() -> String {
57 "/// ".into()
58}
59fn default_input_color() -> String {
60 "LightBlue".into()
61}
62fn default_output_color() -> String {
63 "White".into()
64}
65fn default_cursor() -> String {
66 "PIPE".into()
67}
68
69#[derive(Clone)]
70pub struct Config {
71 config_path: Option<String>,
72 pub max_messages: usize,
73 pub typewriter_delay: Duration,
74 pub input_max_length: usize,
75 pub max_history: usize,
76 pub poll_rate: Duration,
77 pub log_level: String,
78 pub theme: Theme,
79 pub current_theme_name: String,
80 pub language: String,
81 pub debug_info: Option<String>,
82}
83
84#[derive(Clone)]
85pub struct Theme {
86 pub input_text: AppColor,
87 pub input_bg: AppColor,
88 pub output_text: AppColor,
89 pub output_bg: AppColor,
90 pub input_cursor_prefix: String,
91 pub input_cursor_color: AppColor,
92 pub input_cursor: String,
93 pub output_cursor: String,
94 pub output_cursor_color: AppColor,
95}
96
97impl Default for Theme {
98 fn default() -> Self {
99 Self {
100 input_text: AppColor::new(Color::White),
101 input_bg: AppColor::new(Color::Black),
102 output_text: AppColor::new(Color::White),
103 output_bg: AppColor::new(Color::Black),
104 input_cursor_prefix: "/// ".into(),
105 input_cursor_color: AppColor::new(Color::LightBlue),
106 input_cursor: "PIPE".into(),
107 output_cursor: "PIPE".into(),
108 output_cursor_color: AppColor::new(Color::White),
109 }
110 }
111}
112
113impl Config {
114 pub async fn load() -> Result<Self> {
115 Self::load_with_messages(true).await
116 }
117
118 pub async fn load_with_messages(show_messages: bool) -> Result<Self> {
119 for path in crate::setup::setup_toml::get_config_paths() {
121 if path.exists() {
122 if let Ok(config) = Self::from_file(&path).await {
123 if show_messages {
124 Self::log_startup(&config);
125 }
126 Self::apply_language(&config).await;
127 return Ok(config);
128 }
129 }
130 }
131
132 let path = crate::setup::setup_toml::ensure_config_exists().await?;
134 let mut config = Self::from_file(&path).await?;
135
136 if show_messages {
137 config.debug_info = Some(format!("New config: {}", path.display()));
138 Self::log_startup(&config);
139 }
140
141 Self::apply_language(&config).await;
142 Ok(config)
143 }
144
145 pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
146 let content = tokio::fs::read_to_string(&path)
147 .await
148 .map_err(AppError::Io)?;
149 let file: ConfigFile =
150 toml::from_str(&content).map_err(|e| AppError::Validation(format!("TOML: {}", e)))?;
151
152 let poll_rate = Self::clamp(file.general.poll_rate, 16, 1000, 16);
153 let typewriter = Self::clamp(file.general.typewriter_delay, 0, 2000, 50);
154 let theme = Self::load_theme(&file).unwrap_or_default();
155
156 let config = Self {
157 config_path: Some(path.as_ref().to_string_lossy().into_owned()),
158 max_messages: file.general.max_messages,
159 typewriter_delay: Duration::from_millis(typewriter),
160 input_max_length: file.general.input_max_length,
161 max_history: file.general.max_history,
162 poll_rate: Duration::from_millis(poll_rate),
163 log_level: file.general.log_level,
164 theme,
165 current_theme_name: file.general.current_theme,
166 language: file.language.current,
167 debug_info: None,
168 };
169
170 if poll_rate != file.general.poll_rate || typewriter != file.general.typewriter_delay {
172 let _ = config.save().await;
173 }
174
175 Ok(config)
176 }
177
178 fn clamp(value: u64, min: u64, max: u64, default: u64) -> u64 {
179 if value < min || value > max {
180 default
181 } else {
182 value
183 }
184 }
185
186 fn load_theme(file: &ConfigFile) -> Option<Theme> {
187 let themes = file.theme.as_ref()?;
188 let def = themes.get(&file.general.current_theme)?;
189 Theme::from_config(def).ok()
190 }
191
192 pub async fn save(&self) -> Result<()> {
194 let Some(path) = &self.config_path else {
195 return Ok(());
196 };
197
198 let themes = Self::load_existing_themes().await.unwrap_or_default();
199 let file = ConfigFile {
200 general: GeneralConfig {
201 max_messages: self.max_messages,
202 typewriter_delay: self.typewriter_delay.as_millis() as u64,
203 input_max_length: self.input_max_length,
204 max_history: self.max_history,
205 poll_rate: self.poll_rate.as_millis() as u64,
206 log_level: self.log_level.clone(),
207 current_theme: self.current_theme_name.clone(),
208 },
209 theme: if themes.is_empty() {
210 None
211 } else {
212 Some(themes)
213 },
214 language: LanguageConfig {
215 current: self.language.clone(),
216 },
217 };
218
219 let content = toml::to_string_pretty(&file)
220 .map_err(|e| AppError::Validation(format!("TOML: {}", e)))?;
221
222 if let Some(parent) = std::path::PathBuf::from(path).parent() {
224 tokio::fs::create_dir_all(parent)
225 .await
226 .map_err(AppError::Io)?;
227 }
228
229 tokio::fs::write(path, content).await.map_err(AppError::Io)
230 }
231
232 pub async fn change_theme(&mut self, name: &str) -> Result<()> {
234 let themes = Self::load_existing_themes().await?;
235 let def = themes
236 .get(name)
237 .ok_or_else(|| AppError::Validation(format!("Theme '{}' not found", name)))?;
238
239 self.theme = Theme::from_config(def)?;
240 self.current_theme_name = name.into();
241 self.save().await
242 }
243
244 async fn load_existing_themes() -> Result<HashMap<String, ThemeDefinitionConfig>> {
245 for path in crate::setup::setup_toml::get_config_paths() {
246 if path.exists() {
247 let content = tokio::fs::read_to_string(&path)
248 .await
249 .map_err(AppError::Io)?;
250 let file: ConfigFile = toml::from_str(&content)
251 .map_err(|e| AppError::Validation(format!("TOML: {}", e)))?;
252
253 if let Some(themes) = file.theme {
254 return Ok(themes);
255 }
256 }
257 }
258 Ok(HashMap::new())
259 }
260
261 pub fn get_performance_info(&self) -> String {
263 let fps = 1000.0 / self.poll_rate.as_millis() as f64;
264 let typewriter = if self.typewriter_delay.as_millis() > 0 {
265 1000.0 / self.typewriter_delay.as_millis() as f64
266 } else {
267 f64::INFINITY
268 };
269 format!(
270 "Performance: {:.1} FPS, Typewriter: {:.1} chars/sec",
271 fps, typewriter
272 )
273 }
274
275 async fn apply_language(config: &Config) {
277 let _ = crate::commands::lang::LanguageService::new()
278 .load_and_apply_from_config(config)
279 .await;
280 }
281
282 fn log_startup(config: &Config) {
283 if config.poll_rate.as_millis() < 16 {
284 log::warn!("⚡ PERFORMANCE: poll_rate sehr niedrig!");
285 }
286 log::info!("Rush Sync Server v{}", crate::core::constants::VERSION);
287 }
288}
289
290impl Theme {
291 fn from_config(def: &ThemeDefinitionConfig) -> Result<Self> {
292 Ok(Self {
293 input_text: AppColor::from_string(&def.input_text)?,
294 input_bg: AppColor::from_string(&def.input_bg)?,
295 output_text: AppColor::from_string(&def.output_text)?,
296 output_bg: AppColor::from_string(&def.output_bg)?,
297 input_cursor_prefix: def.input_cursor_prefix.clone(),
298 input_cursor_color: AppColor::from_string(&def.input_cursor_color)?,
299 input_cursor: def.input_cursor.clone(),
300 output_cursor: def.output_cursor.clone(),
301 output_cursor_color: AppColor::from_string(&def.output_cursor_color)?,
302 })
303 }
304}
305
306impl Default for Config {
307 fn default() -> Self {
308 Self {
309 config_path: None,
310 max_messages: DEFAULT_BUFFER_SIZE,
311 typewriter_delay: Duration::from_millis(50),
312 input_max_length: DEFAULT_BUFFER_SIZE,
313 max_history: 30,
314 poll_rate: Duration::from_millis(DEFAULT_POLL_RATE),
315 log_level: "info".into(),
316 theme: Theme::default(),
317 current_theme_name: "dark".into(),
318 language: crate::i18n::DEFAULT_LANGUAGE.into(),
319 debug_info: None,
320 }
321 }
322}