rush_sync_server/core/
config.rs

1// =====================================================
2// FILE: src/core/config.rs - KORRIGIERTE VERSION
3// =====================================================
4
5use crate::core::constants::{DEFAULT_BUFFER_SIZE, DEFAULT_POLL_RATE};
6use crate::core::prelude::*;
7use crate::ui::color::AppColor;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::Path;
11use toml_edit::{value, Document};
12
13// ✅ SICHERE BOUNDS für Performance
14const MIN_POLL_RATE: u64 = 16; // 60 FPS maximum
15const MAX_POLL_RATE: u64 = 1000; // 1 FPS minimum
16const MAX_TYPEWRITER_DELAY: u64 = 2000; // Maximum 2 Sekunden
17
18// ✅ KORRIGIERTE STRUCT DEFINITIONEN
19#[derive(Debug, Serialize, Deserialize)]
20struct ConfigFile {
21    general: GeneralConfig,
22    #[serde(default)]
23    theme: Option<HashMap<String, ThemeDefinitionConfig>>,
24    language: LanguageConfig,
25    // ✅ ENTFERNT: prompt section (jetzt Teil von theme)
26}
27
28#[derive(Debug, Serialize, Deserialize)]
29struct GeneralConfig {
30    max_messages: usize,
31    typewriter_delay: u64,
32    input_max_length: usize,
33    max_history: usize,
34    poll_rate: u64,
35    log_level: String,
36    #[serde(default = "default_theme_name")]
37    current_theme: String,
38}
39
40#[derive(Debug, Serialize, Deserialize)]
41struct LanguageConfig {
42    current: String,
43}
44
45#[derive(Debug, Serialize, Deserialize, Clone)]
46struct ThemeDefinitionConfig {
47    input_text: String,
48    input_bg: String,
49    cursor: String,
50    output_text: String,
51    output_bg: String,
52    prompt_text: String,  // ✅ HINZUGEFÜGT
53    prompt_color: String, // ✅ HINZUGEFÜGT
54}
55
56fn default_theme_name() -> String {
57    "dark".to_string()
58}
59
60// ✅ KORRIGIERTE HAUPTSTRUCTS
61#[derive(Clone)]
62pub struct Config {
63    config_path: Option<String>,
64    pub max_messages: usize,
65    pub typewriter_delay: Duration,
66    pub input_max_length: usize,
67    pub max_history: usize,
68    pub poll_rate: Duration,
69    pub log_level: String,
70    pub theme: Theme, // ✅ Theme enthält jetzt prompt
71    pub current_theme_name: String,
72    pub language: String,
73    pub debug_info: Option<String>,
74    // ✅ ENTFERNT: pub prompt: Prompt, (jetzt Teil von theme)
75}
76
77#[derive(Clone)]
78pub struct Theme {
79    pub input_text: AppColor,
80    pub input_bg: AppColor,
81    pub cursor: AppColor,
82    pub output_text: AppColor,
83    pub output_bg: AppColor,
84    pub prompt_text: String,    // ✅ HINZUGEFÜGT
85    pub prompt_color: AppColor, // ✅ HINZUGEFÜGT
86}
87
88// ✅ ENTFERNT: Prompt struct (jetzt Teil von Theme)
89
90impl Config {
91    pub async fn load() -> Result<Self> {
92        Self::load_with_messages(true).await
93    }
94
95    pub async fn load_with_messages(show_messages: bool) -> Result<Self> {
96        // ✅ 1. PRÜFE ob Config bereits existiert
97        for path in crate::setup::setup_toml::get_config_paths() {
98            if path.exists() {
99                match Self::from_file(&path).await {
100                    Ok(config) => {
101                        // ✅ PERFORMANCE WARNING nur bei problematischen Werten
102                        if config.poll_rate.as_millis() < 16 && show_messages {
103                            log::warn!(
104                                "⚡ PERFORMANCE: poll_rate sehr niedrig! {}",
105                                config.get_performance_info()
106                            );
107                        }
108
109                        // Sprache setzen (ohne Log-Spam)
110                        if let Err(e) = crate::commands::lang::LanguageService::new()
111                            .load_and_apply_from_config(&config)
112                            .await
113                        {
114                            if show_messages {
115                                log::warn!(
116                                    "{}",
117                                    get_translation(
118                                        "system.config.language_set_failed",
119                                        &[&e.to_string()]
120                                    )
121                                );
122                            }
123                        }
124
125                        // ✅ VERSION nur einmal beim echten Start
126                        if show_messages {
127                            crate::output::logging::AppLogger::log_plain(
128                                crate::i18n::get_command_translation(
129                                    "system.startup.version",
130                                    &[crate::core::constants::VERSION],
131                                ),
132                            );
133                        }
134
135                        return Ok(config);
136                    }
137                    Err(_e) => {
138                        continue;
139                    }
140                }
141            }
142        }
143
144        // ✅ 2. KEINE CONFIGS GEFUNDEN - Neue erstellen
145        if show_messages {
146            log::info!("{}", get_translation("system.config.no_existing", &[]));
147        }
148
149        match crate::setup::setup_toml::ensure_config_exists().await {
150            Ok(config_path) => {
151                match Self::from_file(&config_path).await {
152                    Ok(mut config) => {
153                        // ✅ NUR BEI FIRST-RUN zeigen
154                        if show_messages {
155                            let plain_msg = get_translation(
156                                "system.config.new_default",
157                                &[&config_path.display().to_string()],
158                            );
159                            log::info!("{}", plain_msg);
160                            config.debug_info = Some(plain_msg);
161
162                            crate::output::logging::AppLogger::log_plain(
163                                crate::i18n::get_command_translation(
164                                    "system.startup.version",
165                                    &[crate::core::constants::VERSION],
166                                ),
167                            );
168                        }
169
170                        let _ = crate::commands::lang::LanguageService::new()
171                            .load_and_apply_from_config(&config)
172                            .await;
173
174                        Ok(config)
175                    }
176                    Err(e) => {
177                        if show_messages {
178                            log::error!(
179                                "{}",
180                                get_translation("system.config.load_error", &[&format!("{:?}", e)])
181                            );
182                        }
183                        Err(e)
184                    }
185                }
186            }
187            Err(e) => {
188                if show_messages {
189                    log::error!(
190                        "{}",
191                        get_translation("system.config.setup_failed", &[&format!("{:?}", e)])
192                    );
193                }
194                Err(e)
195            }
196        }
197    }
198
199    pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
200        let content = tokio::fs::read_to_string(&path)
201            .await
202            .map_err(AppError::Io)?;
203
204        let config_file: ConfigFile = toml::from_str(&content)
205            .map_err(|e| AppError::Validation(format!("Ungültiges TOML-Format: {}", e)))?;
206
207        // ✅ BOUNDS CHECKING mit Warnungen
208        let original_poll_rate = config_file.general.poll_rate;
209        let original_typewriter_delay = config_file.general.typewriter_delay;
210
211        let poll_rate = Self::validate_poll_rate(original_poll_rate);
212        let typewriter_delay = Self::validate_typewriter_delay(original_typewriter_delay);
213
214        // ✅ THEME LOADING (korrigiert)
215        let theme = Self::load_theme_from_config(&config_file)?;
216        let current_theme_name = config_file.general.current_theme.clone();
217
218        let config = Self {
219            config_path: Some(path.as_ref().to_string_lossy().into_owned()),
220            max_messages: config_file.general.max_messages,
221            typewriter_delay: Duration::from_millis(typewriter_delay),
222            input_max_length: config_file.general.input_max_length,
223            max_history: config_file.general.max_history,
224            poll_rate: Duration::from_millis(poll_rate),
225            log_level: config_file.general.log_level,
226            theme,
227            current_theme_name,
228            // ✅ ENTFERNT: prompt field
229            language: config_file.language.current,
230            debug_info: None,
231        };
232
233        // ✅ KORRIGIERTE WERTE ZURÜCKSCHREIBEN (falls geändert)
234        let values_changed =
235            original_poll_rate != poll_rate || original_typewriter_delay != typewriter_delay;
236
237        if values_changed {
238            log::warn!("🔧 Ungültige Config-Werte korrigiert und gespeichert:");
239            if original_poll_rate != poll_rate {
240                log::warn!("   poll_rate: {}ms → {}ms", original_poll_rate, poll_rate);
241            }
242            if original_typewriter_delay != typewriter_delay {
243                log::warn!(
244                    "   typewriter_delay: {}ms → {}ms",
245                    original_typewriter_delay,
246                    typewriter_delay
247                );
248            }
249
250            if let Err(e) = config.save().await {
251                log::warn!("Konnte korrigierte Config nicht speichern: {}", e);
252            } else {
253                log::info!("✅ Korrigierte Werte in Config-Datei gespeichert");
254            }
255        }
256
257        Ok(config)
258    }
259
260    /// ✅ KORRIGIERTE Theme-Loading Methode
261    fn load_theme_from_config(config_file: &ConfigFile) -> Result<Theme> {
262        let current_theme_name = &config_file.general.current_theme;
263
264        // ✅ NUR NOCH TOML-THEMES
265        if let Some(ref themes) = config_file.theme {
266            if let Some(theme_def) = themes.get(current_theme_name) {
267                return Theme::from_theme_definition_config(theme_def);
268            }
269        }
270
271        // ✅ FALLBACK: Default Theme (minimal)
272        log::warn!(
273            "Theme '{}' nicht in TOML gefunden, verwende minimales Standard-Theme",
274            current_theme_name
275        );
276        Ok(Theme::default())
277    }
278
279    // ✅ POLL_RATE Validierung (unverändert)
280    fn validate_poll_rate(value: u64) -> u64 {
281        match value {
282            0 => {
283                log::warn!(
284                    "poll_rate = 0 nicht erlaubt, verwende Minimum: {}ms",
285                    MIN_POLL_RATE
286                );
287                MIN_POLL_RATE
288            }
289            v if v < MIN_POLL_RATE => {
290                log::warn!(
291                    "poll_rate = {}ms zu schnell (Performance!), verwende Minimum: {}ms",
292                    v,
293                    MIN_POLL_RATE
294                );
295                MIN_POLL_RATE
296            }
297            v if v > MAX_POLL_RATE => {
298                log::warn!(
299                    "poll_rate = {}ms zu langsam, verwende Maximum: {}ms",
300                    v,
301                    MAX_POLL_RATE
302                );
303                MAX_POLL_RATE
304            }
305            v => {
306                if v < 33 {
307                    log::trace!("poll_rate = {}ms (sehr schnell, aber OK)", v);
308                }
309                v
310            }
311        }
312    }
313
314    // ✅ TYPEWRITER_DELAY Validierung (unverändert)
315    fn validate_typewriter_delay(value: u64) -> u64 {
316        match value {
317            0 => {
318                log::info!("typewriter_delay = 0 → Typewriter-Effekt deaktiviert");
319                0
320            }
321            v if v > MAX_TYPEWRITER_DELAY => {
322                log::warn!(
323                    "typewriter_delay = {}ms zu langsam, verwende Maximum: {}ms",
324                    v,
325                    MAX_TYPEWRITER_DELAY
326                );
327                MAX_TYPEWRITER_DELAY
328            }
329            v => v,
330        }
331    }
332
333    pub fn get_performance_info(&self) -> String {
334        let fps = 1000.0 / self.poll_rate.as_millis() as f64;
335        let typewriter_chars_per_sec = if self.typewriter_delay.as_millis() > 0 {
336            1000.0 / self.typewriter_delay.as_millis() as f64
337        } else {
338            f64::INFINITY
339        };
340
341        format!(
342            "Performance: {:.1} FPS, Typewriter: {:.1} chars/sec",
343            fps, typewriter_chars_per_sec
344        )
345    }
346
347    /// ✅ KORRIGIERTE SAVE METHODE
348    pub async fn save(&self) -> Result<()> {
349        if let Some(path) = &self.config_path {
350            self.save_with_retry(path).await
351        } else {
352            Err(AppError::Validation("No config path available".to_string()))
353        }
354    }
355
356    /// ✅ ATOMIC SAVE mit Retry-Logic (unverändert)
357    async fn save_with_retry(&self, path: &str) -> Result<()> {
358        const MAX_RETRIES: u32 = 3;
359        let mut last_error = None;
360
361        for attempt in 1..=MAX_RETRIES {
362            match self.save_to_file(path).await {
363                Ok(_) => {
364                    if attempt > 1 {
365                        log::debug!("Config save succeeded on attempt {}", attempt);
366                    }
367                    return Ok(());
368                }
369                Err(e) => {
370                    log::warn!("Config save attempt {} failed: {}", attempt, e);
371                    last_error = Some(e);
372
373                    if attempt < MAX_RETRIES {
374                        tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
375                    }
376                }
377            }
378        }
379
380        Err(last_error.unwrap_or_else(|| AppError::Validation("Unknown save error".to_string())))
381    }
382
383    /// ✅ KORRIGIERTE SAVE_TO_FILE Methode
384    async fn save_to_file(&self, path: &str) -> Result<()> {
385        log::debug!("Saving config to: {}", path);
386
387        // ✅ BACKUP-ERSTELLUNG
388        if std::path::Path::new(path).exists() {
389            let backup_path = format!("{}.backup", path);
390            if let Err(e) = tokio::fs::copy(path, &backup_path).await {
391                log::warn!("Could not create backup: {}", e);
392            } else {
393                log::debug!("Config backup created: {}", backup_path);
394            }
395        }
396
397        // ✅ LADE BESTEHENDE THEMES AUS TOML (falls vorhanden)
398        let existing_themes = Self::load_available_themes_from_current_config()
399            .await
400            .unwrap_or_else(|_| std::collections::HashMap::new());
401
402        let config_file = ConfigFile {
403            general: GeneralConfig {
404                max_messages: self.max_messages,
405                typewriter_delay: self.typewriter_delay.as_millis() as u64,
406                input_max_length: self.input_max_length,
407                max_history: self.max_history,
408                poll_rate: self.poll_rate.as_millis() as u64,
409                log_level: self.log_level.clone(),
410                current_theme: self.current_theme_name.clone(),
411            },
412            theme: if existing_themes.is_empty() {
413                None
414            } else {
415                Some(existing_themes)
416            },
417            language: LanguageConfig {
418                current: self.language.clone(),
419            },
420        };
421
422        // ✅ SERIALIZE zu TOML
423        let content = toml::to_string_pretty(&config_file).map_err(|e| {
424            log::error!("TOML serialization failed: {}", e);
425            AppError::Validation(format!("Serialisierungsfehler: {}", e))
426        })?;
427
428        // ✅ ENSURE DIRECTORY EXISTS
429        if let Some(parent) = std::path::PathBuf::from(path).parent() {
430            if !parent.exists() {
431                log::debug!("Creating config directory: {}", parent.display());
432                tokio::fs::create_dir_all(parent).await.map_err(|e| {
433                    log::error!("Failed to create config directory: {}", e);
434                    AppError::Io(e)
435                })?;
436            }
437        }
438
439        // ✅ ATOMIC WRITE
440        let temp_path = format!("{}.tmp", path);
441        match tokio::fs::write(&temp_path, &content).await {
442            Ok(_) => match tokio::fs::rename(&temp_path, path).await {
443                Ok(_) => {
444                    log::debug!("✅ Config successfully written to: {}", path);
445                    log::debug!("   current_theme = {}", self.current_theme_name);
446                    Ok(())
447                }
448                Err(e) => {
449                    log::error!("Failed to rename temp file: {}", e);
450                    let _ = tokio::fs::remove_file(&temp_path).await;
451                    Err(AppError::Io(e))
452                }
453            },
454            Err(e) => {
455                log::error!("Failed to write temp config file: {}", e);
456                Err(AppError::Io(e))
457            }
458        }
459    }
460
461    /// ✅ THEME WECHSELN (für ThemeManager)
462    async fn update_current_theme_in_file(&self) -> Result<()> {
463        let path = self
464            .config_path
465            .as_ref()
466            .ok_or_else(|| AppError::Validation("Kein config-Pfad gesetzt".to_string()))?;
467        let text = tokio::fs::read_to_string(path)
468            .await
469            .map_err(AppError::Io)?;
470        let mut doc = text
471            .parse::<Document>()
472            .map_err(|e| AppError::Validation(format!("Failed to parse TOML: {}", e)))?;
473        doc["general"]["current_theme"] = value(self.current_theme_name.clone());
474        // atomar schreiben
475        if let Some(parent) = Path::new(path).parent() {
476            tokio::fs::create_dir_all(parent)
477                .await
478                .map_err(AppError::Io)?;
479        }
480        tokio::fs::write(path, doc.to_string())
481            .await
482            .map_err(AppError::Io)?;
483        Ok(())
484    }
485
486    /// Change theme in-memory and persist only the current_theme setting
487    pub async fn change_theme(&mut self, theme_name: &str) -> Result<()> {
488        log::debug!("Switch theme to {}", theme_name);
489
490        // ✅ 1. LADE AKTUELL VERFÜGBARE THEMES AUS TOML
491        let available_themes = Self::load_available_themes_from_current_config().await?;
492
493        if let Some(theme_def) = available_themes.get(theme_name) {
494            self.theme = Theme {
495                input_text: AppColor::from_string(&theme_def.input_text)?,
496                input_bg: AppColor::from_string(&theme_def.input_bg)?,
497                cursor: AppColor::from_string(&theme_def.cursor)?,
498                output_text: AppColor::from_string(&theme_def.output_text)?,
499                output_bg: AppColor::from_string(&theme_def.output_bg)?,
500                prompt_text: theme_def.prompt_text.clone(),
501                prompt_color: AppColor::from_string(&theme_def.prompt_color)?,
502            };
503            self.current_theme_name = theme_name.to_string();
504
505            // ✅ 2. PERSISTIEREN
506            self.update_current_theme_in_file().await?;
507            log::info!("Saved current_theme to config: {}", theme_name);
508            Ok(())
509        } else {
510            let available: Vec<String> = available_themes.keys().cloned().collect();
511            Err(AppError::Validation(format!(
512                "Theme '{}' nicht in TOML gefunden. Verfügbar: {}",
513                theme_name,
514                available.join(", ")
515            )))
516        }
517    }
518
519    /// ✅ NEU: Lädt alle verfügbaren Themes aus aktueller Config
520    async fn load_available_themes_from_current_config(
521    ) -> Result<std::collections::HashMap<String, ThemeDefinitionConfig>> {
522        for path in crate::setup::setup_toml::get_config_paths() {
523            if path.exists() {
524                let content = tokio::fs::read_to_string(&path)
525                    .await
526                    .map_err(AppError::Io)?;
527                let config_file: ConfigFile = toml::from_str(&content)
528                    .map_err(|e| AppError::Validation(format!("TOML parse error: {}", e)))?;
529
530                if let Some(themes) = config_file.theme {
531                    log::debug!(
532                        "Loaded {} themes from TOML: {}",
533                        themes.len(),
534                        themes.keys().cloned().collect::<Vec<String>>().join(", ") // ✅ FIX: .cloned()
535                    );
536                    return Ok(themes);
537                }
538            }
539        }
540
541        // ✅ FALLBACK: Leere Map (keine Themes)
542        log::warn!("Keine Themes in TOML gefunden");
543        Ok(std::collections::HashMap::new())
544    }
545}
546
547impl Theme {
548    fn from_theme_definition_config(theme_def: &ThemeDefinitionConfig) -> Result<Self> {
549        Ok(Self {
550            input_text: AppColor::from_string(&theme_def.input_text)?,
551            input_bg: AppColor::from_string(&theme_def.input_bg)?,
552            cursor: AppColor::from_string(&theme_def.cursor)?,
553            output_text: AppColor::from_string(&theme_def.output_text)?,
554            output_bg: AppColor::from_string(&theme_def.output_bg)?,
555            prompt_text: theme_def.prompt_text.clone(),
556            prompt_color: AppColor::from_string(&theme_def.prompt_color)?,
557        })
558    }
559}
560
561// ✅ DEFAULT IMPLEMENTATIONS
562crate::impl_default!(
563    Config,
564    Self {
565        config_path: None,
566        max_messages: DEFAULT_BUFFER_SIZE,
567        typewriter_delay: Duration::from_millis(50),
568        input_max_length: DEFAULT_BUFFER_SIZE,
569        max_history: 30,
570        poll_rate: Duration::from_millis(DEFAULT_POLL_RATE),
571        log_level: "info".to_string(),
572        theme: Theme::default(),
573        current_theme_name: "dark".to_string(),
574        // ✅ ENTFERNT: prompt field
575        language: crate::i18n::DEFAULT_LANGUAGE.to_string(),
576        debug_info: None,
577    }
578);
579
580crate::impl_default!(
581    Theme,
582    Self {
583        input_text: AppColor::new(Color::White),
584        input_bg: AppColor::new(Color::Black),
585        cursor: AppColor::new(Color::White),
586        output_text: AppColor::new(Color::White),
587        output_bg: AppColor::new(Color::Black),
588        prompt_text: "/// ".to_string(), // ✅ HINZUGEFÜGT
589        prompt_color: AppColor::new(Color::LightBlue), // ✅ HINZUGEFÜGT
590    }
591);
592
593#[cfg(debug_assertions)]
594impl Config {
595    pub fn debug_performance_warning(&self) {
596        if self.poll_rate.as_millis() < 16 {
597            log::warn!(
598                "🔥 PERFORMANCE WARNING: poll_rate = {}ms verursacht hohe CPU-Last!",
599                self.poll_rate.as_millis()
600            );
601            log::warn!("💡 EMPFEHLUNG: Setze poll_rate auf 16-33ms für bessere Performance");
602        }
603
604        if self.typewriter_delay.as_millis() < 10 {
605            log::warn!(
606                "⚡ PERFORMANCE INFO: typewriter_delay = {}ms (sehr schnell)",
607                self.typewriter_delay.as_millis()
608            );
609        }
610
611        log::info!("📊 AKTUELLE WERTE: {}", self.get_performance_info());
612    }
613}