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