rush_sync_server/core/
config.rs

1// src/core/config.rs - Cleaned and simplified
2use crate::core::constants::{DEFAULT_BUFFER_SIZE, DEFAULT_POLL_RATE};
3use crate::core::prelude::*;
4use crate::proxy::types::{ProxyConfig, ProxyConfigToml};
5use crate::ui::color::AppColor;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::Path;
9
10// TOML Configuration Structure
11#[derive(Debug, Serialize, Deserialize)]
12struct ConfigFile {
13    general: GeneralConfig,
14    #[serde(default)]
15    server: Option<ServerConfigToml>,
16    #[serde(default)]
17    logging: Option<LoggingConfigToml>,
18    #[serde(default)]
19    theme: Option<HashMap<String, ThemeDefinitionConfig>>,
20    language: LanguageConfig,
21    proxy: Option<ProxyConfigToml>,
22}
23
24#[derive(Debug, Serialize, Deserialize)]
25struct GeneralConfig {
26    max_messages: usize,
27    typewriter_delay: u64,
28    input_max_length: usize,
29    max_history: usize,
30    poll_rate: u64,
31    log_level: String,
32    #[serde(default = "default_theme")]
33    current_theme: String,
34}
35
36#[derive(Debug, Serialize, Deserialize)]
37struct LanguageConfig {
38    current: String,
39}
40
41#[derive(Debug, Serialize, Deserialize, Clone)]
42struct ServerConfigToml {
43    #[serde(default = "default_port_start")]
44    port_range_start: u16,
45    #[serde(default = "default_port_end")]
46    port_range_end: u16,
47    #[serde(default = "default_max_concurrent")]
48    max_concurrent: usize,
49    #[serde(default = "default_shutdown_timeout")]
50    shutdown_timeout: u64,
51    #[serde(default = "default_startup_delay")]
52    startup_delay_ms: u64,
53    #[serde(default = "default_workers")]
54    workers: usize,
55    #[serde(default = "default_auto_open_browser")]
56    auto_open_browser: bool,
57
58    // TLS Configuration
59    #[serde(default = "default_enable_https")]
60    enable_https: bool,
61    #[serde(default = "default_https_port_offset")]
62    https_port_offset: u16,
63    #[serde(default = "default_cert_dir")]
64    cert_dir: String,
65    #[serde(default = "default_auto_cert")]
66    auto_cert: bool,
67    #[serde(default = "default_cert_validity_days")]
68    cert_validity_days: u32,
69
70    // Production Settings
71    #[serde(default = "default_use_lets_encrypt")]
72    use_lets_encrypt: bool,
73    #[serde(default = "default_production_domain")]
74    production_domain: String,
75}
76
77#[derive(Debug, Serialize, Deserialize, Clone)]
78struct LoggingConfigToml {
79    #[serde(default = "default_max_file_size")]
80    max_file_size_mb: u64,
81    #[serde(default = "default_max_archive_files")]
82    max_archive_files: u8,
83    #[serde(default = "default_compress_archives")]
84    compress_archives: bool,
85    #[serde(default = "default_log_requests")]
86    log_requests: bool,
87    #[serde(default = "default_log_security")]
88    log_security_alerts: bool,
89    #[serde(default = "default_log_performance")]
90    log_performance: bool,
91}
92
93#[derive(Debug, Serialize, Deserialize, Clone)]
94struct ThemeDefinitionConfig {
95    input_text: String,
96    input_bg: String,
97    output_text: String,
98    output_bg: String,
99    #[serde(default = "default_prefix")]
100    input_cursor_prefix: String,
101    #[serde(default = "default_input_color")]
102    input_cursor_color: String,
103    #[serde(default = "default_cursor")]
104    input_cursor: String,
105    #[serde(default = "default_cursor")]
106    output_cursor: String,
107    #[serde(default = "default_output_color")]
108    output_cursor_color: String,
109}
110
111// Default Functions
112fn default_theme() -> String {
113    "dark".into()
114}
115fn default_prefix() -> String {
116    "/// ".into()
117}
118fn default_input_color() -> String {
119    "LightBlue".into()
120}
121fn default_output_color() -> String {
122    "White".into()
123}
124fn default_cursor() -> String {
125    "PIPE".into()
126}
127
128// Server Defaults
129fn default_port_start() -> u16 {
130    8080
131}
132fn default_port_end() -> u16 {
133    8180
134}
135fn default_max_concurrent() -> usize {
136    10
137}
138fn default_shutdown_timeout() -> u64 {
139    5
140}
141fn default_startup_delay() -> u64 {
142    500
143}
144fn default_workers() -> usize {
145    1
146}
147fn default_auto_open_browser() -> bool {
148    true
149}
150
151// TLS Defaults
152fn default_enable_https() -> bool {
153    true
154}
155fn default_https_port_offset() -> u16 {
156    1000
157}
158fn default_cert_dir() -> String {
159    ".rss/certs".to_string()
160}
161fn default_auto_cert() -> bool {
162    true
163}
164fn default_cert_validity_days() -> u32 {
165    365
166}
167fn default_use_lets_encrypt() -> bool {
168    false
169}
170fn default_production_domain() -> String {
171    "localhost".to_string()
172}
173
174// Logging Defaults
175fn default_max_file_size() -> u64 {
176    100
177}
178fn default_max_archive_files() -> u8 {
179    9
180}
181fn default_compress_archives() -> bool {
182    true
183}
184fn default_log_requests() -> bool {
185    true
186}
187fn default_log_security() -> bool {
188    true
189}
190fn default_log_performance() -> bool {
191    true
192}
193
194// Main Configuration Structures
195#[derive(Clone)]
196pub struct Config {
197    config_path: Option<String>,
198    pub max_messages: usize,
199    pub typewriter_delay: Duration,
200    pub input_max_length: usize,
201    pub max_history: usize,
202    pub poll_rate: Duration,
203    pub log_level: String,
204    pub theme: Theme,
205    pub current_theme_name: String,
206    pub language: String,
207    pub debug_info: Option<String>,
208    pub server: ServerConfig,
209    pub logging: LoggingConfig,
210    pub proxy: ProxyConfig,
211}
212
213#[derive(Clone)]
214pub struct ServerConfig {
215    pub port_range_start: u16,
216    pub port_range_end: u16,
217    pub max_concurrent: usize,
218    pub shutdown_timeout: u64,
219    pub startup_delay_ms: u64,
220    pub workers: usize,
221    pub auto_open_browser: bool,
222
223    // TLS Configuration
224    pub enable_https: bool,
225    pub https_port_offset: u16,
226    pub cert_dir: String,
227    pub auto_cert: bool,
228    pub cert_validity_days: u32,
229    pub use_lets_encrypt: bool,
230    pub production_domain: String,
231}
232
233#[derive(Clone)]
234pub struct LoggingConfig {
235    pub max_file_size_mb: u64,
236    pub max_archive_files: u8,
237    pub compress_archives: bool,
238    pub log_requests: bool,
239    pub log_security_alerts: bool,
240    pub log_performance: bool,
241}
242
243#[derive(Clone)]
244pub struct Theme {
245    pub input_text: AppColor,
246    pub input_bg: AppColor,
247    pub output_text: AppColor,
248    pub output_bg: AppColor,
249    pub input_cursor_prefix: String,
250    pub input_cursor_color: AppColor,
251    pub input_cursor: String,
252    pub output_cursor: String,
253    pub output_cursor_color: AppColor,
254}
255
256impl Default for Theme {
257    fn default() -> Self {
258        Self {
259            input_text: AppColor::new(Color::White),
260            input_bg: AppColor::new(Color::Black),
261            output_text: AppColor::new(Color::White),
262            output_bg: AppColor::new(Color::Black),
263            input_cursor_prefix: "/// ".into(),
264            input_cursor_color: AppColor::new(Color::LightBlue),
265            input_cursor: "PIPE".into(),
266            output_cursor: "PIPE".into(),
267            output_cursor_color: AppColor::new(Color::White),
268        }
269    }
270}
271
272impl Default for ServerConfig {
273    fn default() -> Self {
274        Self {
275            port_range_start: 8080,
276            port_range_end: 8180,
277            max_concurrent: 10,
278            shutdown_timeout: 5,
279            startup_delay_ms: 500,
280            workers: 1,
281            auto_open_browser: true,
282            enable_https: true,
283            https_port_offset: 1000,
284            cert_dir: ".rss/certs".to_string(),
285            auto_cert: true,
286            cert_validity_days: 365,
287            use_lets_encrypt: false,
288            production_domain: "localhost".to_string(),
289        }
290    }
291}
292
293impl Default for LoggingConfig {
294    fn default() -> Self {
295        Self {
296            max_file_size_mb: 100,
297            max_archive_files: 9,
298            compress_archives: true,
299            log_requests: true,
300            log_security_alerts: true,
301            log_performance: true,
302        }
303    }
304}
305
306impl Config {
307    pub async fn load() -> Result<Self> {
308        Self::load_with_messages(true).await
309    }
310
311    pub async fn load_with_messages(show_messages: bool) -> Result<Self> {
312        // Try existing configs
313        for path in crate::setup::setup_toml::get_config_paths() {
314            if path.exists() {
315                if let Ok(config) = Self::from_file(&path).await {
316                    if show_messages {
317                        Self::log_startup(&config);
318                    }
319                    Self::apply_language(&config).await;
320                    return Ok(config);
321                }
322            }
323        }
324
325        // Create new config
326        let path = crate::setup::setup_toml::ensure_config_exists().await?;
327        let mut config = Self::from_file(&path).await?;
328
329        if show_messages {
330            config.debug_info = Some(format!("New config: {}", path.display()));
331            Self::log_startup(&config);
332        }
333
334        Self::apply_language(&config).await;
335        Ok(config)
336    }
337
338    pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
339        let content = tokio::fs::read_to_string(&path)
340            .await
341            .map_err(AppError::Io)?;
342        let file: ConfigFile =
343            toml::from_str(&content).map_err(|e| AppError::Validation(format!("TOML: {}", e)))?;
344
345        let poll_rate = Self::clamp(file.general.poll_rate, 16, 1000, 16);
346        let typewriter = Self::clamp(file.general.typewriter_delay, 0, 2000, 50);
347        let theme = Self::load_theme(&file).unwrap_or_default();
348
349        // Load server config
350        let server = file
351            .server
352            .map_or_else(ServerConfig::default, |s| ServerConfig {
353                port_range_start: s.port_range_start,
354                port_range_end: s.port_range_end,
355                max_concurrent: s.max_concurrent,
356                shutdown_timeout: s.shutdown_timeout,
357                startup_delay_ms: s.startup_delay_ms,
358                workers: s.workers,
359                auto_open_browser: s.auto_open_browser,
360                enable_https: s.enable_https,
361                https_port_offset: s.https_port_offset,
362                cert_dir: s.cert_dir,
363                auto_cert: s.auto_cert,
364                cert_validity_days: s.cert_validity_days,
365                use_lets_encrypt: s.use_lets_encrypt,
366                production_domain: s.production_domain,
367            });
368
369        // Load logging config
370        let logging = file
371            .logging
372            .map_or_else(LoggingConfig::default, |l| LoggingConfig {
373                max_file_size_mb: l.max_file_size_mb,
374                max_archive_files: l.max_archive_files,
375                compress_archives: l.compress_archives,
376                log_requests: l.log_requests,
377                log_security_alerts: l.log_security_alerts,
378                log_performance: l.log_performance,
379            });
380
381        let config = Self {
382            config_path: Some(path.as_ref().to_string_lossy().into_owned()),
383            max_messages: file.general.max_messages,
384            typewriter_delay: Duration::from_millis(typewriter),
385            input_max_length: file.general.input_max_length,
386            max_history: file.general.max_history,
387            poll_rate: Duration::from_millis(poll_rate),
388            log_level: file.general.log_level,
389            theme,
390            current_theme_name: file.general.current_theme,
391            language: file.language.current,
392            debug_info: None,
393            server,
394            logging,
395            proxy: file.proxy.map(ProxyConfig::from).unwrap_or_default(),
396        };
397
398        // Auto-save corrected values
399        if poll_rate != file.general.poll_rate || typewriter != file.general.typewriter_delay {
400            let _ = config.save().await;
401        }
402
403        Ok(config)
404    }
405
406    pub async fn save(&self) -> Result<()> {
407        let Some(path) = &self.config_path else {
408            return Ok(());
409        };
410
411        let themes = Self::load_existing_themes().await.unwrap_or_default();
412        let file = ConfigFile {
413            general: GeneralConfig {
414                max_messages: self.max_messages,
415                typewriter_delay: self.typewriter_delay.as_millis() as u64,
416                input_max_length: self.input_max_length,
417                max_history: self.max_history,
418                poll_rate: self.poll_rate.as_millis() as u64,
419                log_level: self.log_level.clone(),
420                current_theme: self.current_theme_name.clone(),
421            },
422            server: Some(ServerConfigToml {
423                port_range_start: self.server.port_range_start,
424                port_range_end: self.server.port_range_end,
425                max_concurrent: self.server.max_concurrent,
426                shutdown_timeout: self.server.shutdown_timeout,
427                startup_delay_ms: self.server.startup_delay_ms,
428                workers: self.server.workers,
429                auto_open_browser: self.server.auto_open_browser,
430                enable_https: self.server.enable_https,
431                https_port_offset: self.server.https_port_offset,
432                cert_dir: self.server.cert_dir.clone(),
433                auto_cert: self.server.auto_cert,
434                cert_validity_days: self.server.cert_validity_days,
435                use_lets_encrypt: self.server.use_lets_encrypt,
436                production_domain: self.server.production_domain.clone(),
437            }),
438            logging: Some(LoggingConfigToml {
439                max_file_size_mb: self.logging.max_file_size_mb,
440                max_archive_files: self.logging.max_archive_files,
441                compress_archives: self.logging.compress_archives,
442                log_requests: self.logging.log_requests,
443                log_security_alerts: self.logging.log_security_alerts,
444                log_performance: self.logging.log_performance,
445            }),
446            theme: if themes.is_empty() {
447                None
448            } else {
449                Some(themes)
450            },
451            language: LanguageConfig {
452                current: self.language.clone(),
453            },
454            proxy: Some(self.proxy.clone().into()),
455        };
456
457        let content = toml::to_string_pretty(&file)
458            .map_err(|e| AppError::Validation(format!("TOML: {}", e)))?;
459
460        // Ensure dir exists
461        if let Some(parent) = std::path::PathBuf::from(path).parent() {
462            tokio::fs::create_dir_all(parent)
463                .await
464                .map_err(AppError::Io)?;
465        }
466
467        tokio::fs::write(path, content).await.map_err(AppError::Io)
468    }
469
470    pub async fn change_theme(&mut self, name: &str) -> Result<()> {
471        let themes = Self::load_existing_themes().await?;
472        let def = themes
473            .get(name)
474            .ok_or_else(|| AppError::Validation(format!("Theme '{}' not found", name)))?;
475
476        self.theme = Theme::from_config(def)?;
477        self.current_theme_name = name.into();
478        self.save().await
479    }
480
481    pub fn get_performance_info(&self) -> String {
482        let fps = 1000.0 / self.poll_rate.as_millis() as f64;
483        let typewriter = if self.typewriter_delay.as_millis() > 0 {
484            1000.0 / self.typewriter_delay.as_millis() as f64
485        } else {
486            f64::INFINITY
487        };
488        format!(
489            "Performance: {:.1} FPS, Typewriter: {:.1} chars/sec, Max Servers: {}",
490            fps, typewriter, self.server.max_concurrent
491        )
492    }
493
494    // Helper methods
495    fn clamp(value: u64, min: u64, max: u64, default: u64) -> u64 {
496        if value < min || value > max {
497            default
498        } else {
499            value
500        }
501    }
502
503    fn load_theme(file: &ConfigFile) -> Option<Theme> {
504        let themes = file.theme.as_ref()?;
505        let def = themes.get(&file.general.current_theme)?;
506        Theme::from_config(def).ok()
507    }
508
509    async fn load_existing_themes() -> Result<HashMap<String, ThemeDefinitionConfig>> {
510        for path in crate::setup::setup_toml::get_config_paths() {
511            if path.exists() {
512                let content = tokio::fs::read_to_string(&path)
513                    .await
514                    .map_err(AppError::Io)?;
515                let file: ConfigFile = toml::from_str(&content)
516                    .map_err(|e| AppError::Validation(format!("TOML: {}", e)))?;
517
518                if let Some(themes) = file.theme {
519                    return Ok(themes);
520                }
521            }
522        }
523        Ok(HashMap::new())
524    }
525
526    async fn apply_language(config: &Config) {
527        let _ = crate::commands::lang::LanguageService::new()
528            .load_and_apply_from_config(config)
529            .await;
530    }
531
532    fn log_startup(config: &Config) {
533        if config.poll_rate.as_millis() < 16 {
534            log::warn!("Performance: poll_rate sehr niedrig!");
535        }
536        log::info!("Rush Sync Server v{}", crate::core::constants::VERSION);
537        log::info!(
538            "Server Config: Ports {}-{}, Max: {}",
539            config.server.port_range_start,
540            config.server.port_range_end,
541            config.server.max_concurrent
542        );
543    }
544}
545
546impl Theme {
547    fn from_config(def: &ThemeDefinitionConfig) -> Result<Self> {
548        Ok(Self {
549            input_text: AppColor::from_string(&def.input_text)?,
550            input_bg: AppColor::from_string(&def.input_bg)?,
551            output_text: AppColor::from_string(&def.output_text)?,
552            output_bg: AppColor::from_string(&def.output_bg)?,
553            input_cursor_prefix: def.input_cursor_prefix.clone(),
554            input_cursor_color: AppColor::from_string(&def.input_cursor_color)?,
555            input_cursor: def.input_cursor.clone(),
556            output_cursor: def.output_cursor.clone(),
557            output_cursor_color: AppColor::from_string(&def.output_cursor_color)?,
558        })
559    }
560}
561
562impl Default for Config {
563    fn default() -> Self {
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".into(),
572            theme: Theme::default(),
573            current_theme_name: "dark".into(),
574            language: crate::i18n::DEFAULT_LANGUAGE.into(),
575            debug_info: None,
576            server: ServerConfig::default(),
577            logging: LoggingConfig::default(),
578            proxy: ProxyConfig::default(),
579        }
580    }
581}