rush_sync_server/core/
config.rs

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    // ✅ PERFEKTE CURSOR-KONFIGURATION (5 Parameter)
42    #[serde(default = "default_input_cursor_prefix")]
43    input_cursor_prefix: String, // NEU: Prompt-Text (/// )
44
45    #[serde(default = "default_input_cursor_color")]
46    input_cursor_color: String, // NEU: Prompt-Farbe
47
48    #[serde(default = "default_input_cursor")]
49    input_cursor: String, // NEU: Input-Cursor-Typ
50
51    #[serde(default = "default_output_cursor")]
52    output_cursor: String, // Output-Cursor-Typ
53
54    #[serde(default = "default_output_cursor_color")]
55    output_cursor_color: String, // NEU: Output-Cursor-Farbe
56
57    // ✅ BACKWARD-KOMPATIBILITÄT für alte Felder
58    #[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
75// ✅ PERFEKTE CURSOR-DEFAULTS
76fn 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    // ✅ PERFEKTE CURSOR-KONFIGURATION (5 Felder)
116    pub input_cursor_prefix: String,   // NEU: Prompt-Text
117    pub input_cursor_color: AppColor,  // NEU: Prompt-Farbe
118    pub input_cursor: String,          // NEU: Input-Cursor-Typ
119    pub output_cursor: String,         // Output-Cursor-Typ
120    pub output_cursor_color: AppColor, // NEU: Output-Cursor-Farbe
121}
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            // ✅ PERFEKTE CURSOR-DEFAULTS
133            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        // ✅ BACKWARD-KOMPATIBILITÄT: Legacy-Felder → neue Felder
346        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            // ✅ PERFEKTE CURSOR-KONFIGURATION
385            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}