1use 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#[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 #[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 #[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
111fn 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
128fn 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
151fn 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
174fn 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#[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 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 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 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 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 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 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 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 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}