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