rush_sync_server/core/
config.rs

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