1use crate::core::constants::{DEFAULT_BUFFER_SIZE, DEFAULT_POLL_RATE};
2use crate::core::prelude::*;
3use crate::ui::color::AppColor;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::Path;
7
8#[derive(Debug, Serialize, Deserialize)]
9struct ConfigFile {
10 general: GeneralConfig,
11 #[serde(default)]
12 theme: Option<HashMap<String, ThemeDefinitionConfig>>,
13 language: LanguageConfig,
14}
15
16#[derive(Debug, Serialize, Deserialize)]
17struct GeneralConfig {
18 max_messages: usize,
19 typewriter_delay: u64,
20 input_max_length: usize,
21 max_history: usize,
22 poll_rate: u64,
23 log_level: String,
24 #[serde(default = "default_theme_name")]
25 current_theme: String,
26}
27
28#[derive(Debug, Serialize, Deserialize)]
29struct LanguageConfig {
30 current: String,
31}
32
33#[derive(Debug, Serialize, Deserialize, Clone)]
34struct ThemeDefinitionConfig {
35 input_text: String,
36 input_bg: String,
37 cursor: String,
38 output_text: String,
39 output_bg: String,
40
41 #[serde(default = "default_input_cursor_prefix")]
43 input_cursor_prefix: String, #[serde(default = "default_input_cursor_color")]
46 input_cursor_color: String, #[serde(default = "default_input_cursor")]
49 input_cursor: String, #[serde(default = "default_output_cursor")]
52 output_cursor: String, #[serde(default = "default_output_cursor_color")]
55 output_cursor_color: String, #[serde(alias = "prompt_text", skip_serializing_if = "Option::is_none")]
59 _legacy_prompt_text: Option<String>,
60
61 #[serde(alias = "prompt_color", skip_serializing_if = "Option::is_none")]
62 _legacy_prompt_color: Option<String>,
63
64 #[serde(alias = "prompt_cursor", skip_serializing_if = "Option::is_none")]
65 _legacy_prompt_cursor: Option<String>,
66
67 #[serde(alias = "output_color", skip_serializing_if = "Option::is_none")]
68 _legacy_output_color: Option<String>,
69}
70
71fn default_theme_name() -> String {
72 "dark".to_string()
73}
74
75fn default_input_cursor_prefix() -> String {
77 "/// ".to_string()
78}
79fn default_input_cursor_color() -> String {
80 "LightBlue".to_string()
81}
82fn default_input_cursor() -> String {
83 "DEFAULT".to_string()
84}
85fn default_output_cursor() -> String {
86 "DEFAULT".to_string()
87}
88fn default_output_cursor_color() -> String {
89 "White".to_string()
90}
91
92#[derive(Clone)]
93pub struct Config {
94 config_path: Option<String>,
95 pub max_messages: usize,
96 pub typewriter_delay: Duration,
97 pub input_max_length: usize,
98 pub max_history: usize,
99 pub poll_rate: Duration,
100 pub log_level: String,
101 pub theme: Theme,
102 pub current_theme_name: String,
103 pub language: String,
104 pub debug_info: Option<String>,
105}
106
107#[derive(Clone)]
108pub struct Theme {
109 pub input_text: AppColor,
110 pub input_bg: AppColor,
111 pub cursor: AppColor,
112 pub output_text: AppColor,
113 pub output_bg: AppColor,
114
115 pub input_cursor_prefix: String, pub input_cursor_color: AppColor, pub input_cursor: String, pub output_cursor: String, pub output_cursor_color: AppColor, }
122
123impl Default for Theme {
124 fn default() -> Self {
125 Self {
126 input_text: AppColor::new(Color::White),
127 input_bg: AppColor::new(Color::Black),
128 cursor: AppColor::new(Color::White),
129 output_text: AppColor::new(Color::White),
130 output_bg: AppColor::new(Color::Black),
131
132 input_cursor_prefix: "/// ".to_string(),
134 input_cursor_color: AppColor::new(Color::LightBlue),
135 input_cursor: "DEFAULT".to_string(),
136 output_cursor: "DEFAULT".to_string(),
137 output_cursor_color: AppColor::new(Color::White),
138 }
139 }
140}
141
142impl Config {
143 pub async fn load() -> Result<Self> {
144 Self::load_with_messages(true).await
145 }
146
147 pub async fn load_with_messages(show_messages: bool) -> Result<Self> {
148 for path in crate::setup::setup_toml::get_config_paths() {
149 if path.exists() {
150 if let Ok(config) = Self::from_file(&path).await {
151 if show_messages && config.poll_rate.as_millis() < 16 {
152 log::warn!("⚡ PERFORMANCE: poll_rate sehr niedrig!");
153 }
154
155 let _ = crate::commands::lang::LanguageService::new()
156 .load_and_apply_from_config(&config)
157 .await;
158
159 if show_messages {
160 log::info!("Rush Sync Server v{}", crate::core::constants::VERSION);
161 }
162 return Ok(config);
163 }
164 }
165 }
166
167 if show_messages {
168 log::info!("Keine Config gefunden, erstelle neue");
169 }
170
171 let config_path = crate::setup::setup_toml::ensure_config_exists().await?;
172 let mut config = Self::from_file(&config_path).await?;
173
174 if show_messages {
175 config.debug_info = Some(format!("Neue Config erstellt: {}", config_path.display()));
176 log::info!("Rush Sync Server v{}", crate::core::constants::VERSION);
177 }
178
179 let _ = crate::commands::lang::LanguageService::new()
180 .load_and_apply_from_config(&config)
181 .await;
182
183 Ok(config)
184 }
185
186 pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
187 let content = tokio::fs::read_to_string(&path)
188 .await
189 .map_err(AppError::Io)?;
190 let config_file: ConfigFile = toml::from_str(&content)
191 .map_err(|e| AppError::Validation(format!("TOML Error: {}", e)))?;
192
193 let poll_rate = Self::validate_range(config_file.general.poll_rate, 16, 1000, 16);
194 let typewriter_delay =
195 Self::validate_range(config_file.general.typewriter_delay, 0, 2000, 50);
196
197 let theme = Self::load_theme_from_config(&config_file)?;
198
199 let config = Self {
200 config_path: Some(path.as_ref().to_string_lossy().into_owned()),
201 max_messages: config_file.general.max_messages,
202 typewriter_delay: Duration::from_millis(typewriter_delay),
203 input_max_length: config_file.general.input_max_length,
204 max_history: config_file.general.max_history,
205 poll_rate: Duration::from_millis(poll_rate),
206 log_level: config_file.general.log_level,
207 theme,
208 current_theme_name: config_file.general.current_theme,
209 language: config_file.language.current,
210 debug_info: None,
211 };
212
213 if poll_rate != config_file.general.poll_rate
214 || typewriter_delay != config_file.general.typewriter_delay
215 {
216 log::warn!("Config-Werte korrigiert und gespeichert");
217 let _ = config.save().await;
218 }
219
220 Ok(config)
221 }
222
223 fn validate_range(value: u64, min: u64, max: u64, default: u64) -> u64 {
224 if value < min || value > max {
225 log::warn!(
226 "Wert {} außerhalb Bereich {}-{}, verwende {}",
227 value,
228 min,
229 max,
230 default
231 );
232 default
233 } else {
234 value
235 }
236 }
237
238 fn load_theme_from_config(config_file: &ConfigFile) -> Result<Theme> {
239 let current_theme_name = &config_file.general.current_theme;
240
241 if let Some(ref themes) = config_file.theme {
242 if let Some(theme_def) = themes.get(current_theme_name) {
243 return Theme::from_config(theme_def);
244 }
245 }
246
247 log::warn!(
248 "Theme '{}' nicht gefunden, verwende Standard",
249 current_theme_name
250 );
251 Ok(Theme::default())
252 }
253
254 pub async fn save(&self) -> Result<()> {
255 if let Some(path) = &self.config_path {
256 let existing_themes = Self::load_themes_from_config().await.unwrap_or_default();
257
258 let config_file = ConfigFile {
259 general: GeneralConfig {
260 max_messages: self.max_messages,
261 typewriter_delay: self.typewriter_delay.as_millis() as u64,
262 input_max_length: self.input_max_length,
263 max_history: self.max_history,
264 poll_rate: self.poll_rate.as_millis() as u64,
265 log_level: self.log_level.clone(),
266 current_theme: self.current_theme_name.clone(),
267 },
268 theme: if existing_themes.is_empty() {
269 None
270 } else {
271 Some(existing_themes)
272 },
273 language: LanguageConfig {
274 current: self.language.clone(),
275 },
276 };
277
278 let content = toml::to_string_pretty(&config_file)
279 .map_err(|e| AppError::Validation(format!("TOML Error: {}", e)))?;
280
281 if let Some(parent) = std::path::PathBuf::from(path).parent() {
282 tokio::fs::create_dir_all(parent)
283 .await
284 .map_err(AppError::Io)?;
285 }
286
287 tokio::fs::write(path, content)
288 .await
289 .map_err(AppError::Io)?;
290 }
291 Ok(())
292 }
293
294 pub async fn change_theme(&mut self, theme_name: &str) -> Result<()> {
295 let available_themes = Self::load_themes_from_config().await?;
296
297 if let Some(theme_def) = available_themes.get(theme_name) {
298 self.theme = Theme::from_config(theme_def)?;
299 self.current_theme_name = theme_name.to_string();
300 self.save().await?;
301 log::info!("Theme gewechselt zu: {}", theme_name);
302 Ok(())
303 } else {
304 Err(AppError::Validation(format!(
305 "Theme '{}' nicht gefunden",
306 theme_name
307 )))
308 }
309 }
310
311 async fn load_themes_from_config() -> Result<HashMap<String, ThemeDefinitionConfig>> {
312 for path in crate::setup::setup_toml::get_config_paths() {
313 if path.exists() {
314 let content = tokio::fs::read_to_string(&path)
315 .await
316 .map_err(AppError::Io)?;
317 let config_file: ConfigFile = toml::from_str(&content)
318 .map_err(|e| AppError::Validation(format!("TOML Error: {}", e)))?;
319
320 if let Some(themes) = config_file.theme {
321 return Ok(themes);
322 }
323 }
324 }
325 Ok(HashMap::new())
326 }
327
328 pub fn get_performance_info(&self) -> String {
329 let fps = 1000.0 / self.poll_rate.as_millis() as f64;
330 let typewriter_chars_per_sec = if self.typewriter_delay.as_millis() > 0 {
331 1000.0 / self.typewriter_delay.as_millis() as f64
332 } else {
333 f64::INFINITY
334 };
335
336 format!(
337 "Performance: {:.1} FPS, Typewriter: {:.1} chars/sec",
338 fps, typewriter_chars_per_sec
339 )
340 }
341}
342
343impl Theme {
344 fn from_config(theme_def: &ThemeDefinitionConfig) -> Result<Self> {
345 let input_cursor_prefix = theme_def
347 ._legacy_prompt_text
348 .as_ref()
349 .or(Some(&theme_def.input_cursor_prefix))
350 .unwrap_or(&"/// ".to_string())
351 .clone();
352
353 let input_cursor_color = theme_def
354 ._legacy_prompt_color
355 .as_ref()
356 .or(Some(&theme_def.input_cursor_color))
357 .unwrap_or(&"LightBlue".to_string())
358 .clone();
359
360 let input_cursor = if let Some(ref legacy) = theme_def._legacy_prompt_cursor {
361 log::warn!(
362 "⚠️ Veraltetes 'prompt_cursor' gefunden, verwende als 'input_cursor': {}",
363 legacy
364 );
365 legacy.clone()
366 } else {
367 theme_def.input_cursor.clone()
368 };
369
370 let output_cursor_color = theme_def
371 ._legacy_output_color
372 .as_ref()
373 .or(Some(&theme_def.output_cursor_color))
374 .unwrap_or(&"White".to_string())
375 .clone();
376
377 Ok(Self {
378 input_text: AppColor::from_string(&theme_def.input_text)?,
379 input_bg: AppColor::from_string(&theme_def.input_bg)?,
380 cursor: AppColor::from_string(&theme_def.cursor)?,
381 output_text: AppColor::from_string(&theme_def.output_text)?,
382 output_bg: AppColor::from_string(&theme_def.output_bg)?,
383
384 input_cursor_prefix,
386 input_cursor_color: AppColor::from_string(&input_cursor_color)?,
387 input_cursor,
388 output_cursor: theme_def.output_cursor.clone(),
389 output_cursor_color: AppColor::from_string(&output_cursor_color)?,
390 })
391 }
392}
393
394impl Default for Config {
395 fn default() -> Self {
396 Self {
397 config_path: None,
398 max_messages: DEFAULT_BUFFER_SIZE,
399 typewriter_delay: Duration::from_millis(50),
400 input_max_length: DEFAULT_BUFFER_SIZE,
401 max_history: 30,
402 poll_rate: Duration::from_millis(DEFAULT_POLL_RATE),
403 log_level: "info".to_string(),
404 theme: Theme::default(),
405 current_theme_name: "dark".to_string(),
406 language: crate::i18n::DEFAULT_LANGUAGE.to_string(),
407 debug_info: None,
408 }
409 }
410}