rush_sync_server/core/
config.rs

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