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