1use crate::core::constants::{DEFAULT_BUFFER_SIZE, DEFAULT_POLL_RATE};
6use crate::core::prelude::*;
7use crate::ui::color::AppColor;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::path::Path;
11use toml_edit::{value, Document};
12
13const MIN_POLL_RATE: u64 = 16; const MAX_POLL_RATE: u64 = 1000; const MAX_TYPEWRITER_DELAY: u64 = 2000; #[derive(Debug, Serialize, Deserialize)]
20struct ConfigFile {
21 general: GeneralConfig,
22 #[serde(default)]
23 theme: Option<HashMap<String, ThemeDefinitionConfig>>,
24 language: LanguageConfig,
25 }
27
28#[derive(Debug, Serialize, Deserialize)]
29struct GeneralConfig {
30 max_messages: usize,
31 typewriter_delay: u64,
32 input_max_length: usize,
33 max_history: usize,
34 poll_rate: u64,
35 log_level: String,
36 #[serde(default = "default_theme_name")]
37 current_theme: String,
38}
39
40#[derive(Debug, Serialize, Deserialize)]
41struct LanguageConfig {
42 current: String,
43}
44
45#[derive(Debug, Serialize, Deserialize, Clone)]
46struct ThemeDefinitionConfig {
47 input_text: String,
48 input_bg: String,
49 cursor: String,
50 output_text: String,
51 output_bg: String,
52 prompt_text: String, prompt_color: String, }
55
56fn default_theme_name() -> String {
57 "dark".to_string()
58}
59
60#[derive(Clone)]
62pub struct Config {
63 config_path: Option<String>,
64 pub max_messages: usize,
65 pub typewriter_delay: Duration,
66 pub input_max_length: usize,
67 pub max_history: usize,
68 pub poll_rate: Duration,
69 pub log_level: String,
70 pub theme: Theme, pub current_theme_name: String,
72 pub language: String,
73 pub debug_info: Option<String>,
74 }
76
77#[derive(Clone)]
78pub struct Theme {
79 pub input_text: AppColor,
80 pub input_bg: AppColor,
81 pub cursor: AppColor,
82 pub output_text: AppColor,
83 pub output_bg: AppColor,
84 pub prompt_text: String, pub prompt_color: AppColor, }
87
88impl Config {
91 pub async fn load() -> Result<Self> {
92 Self::load_with_messages(true).await
93 }
94
95 pub async fn load_with_messages(show_messages: bool) -> Result<Self> {
96 for path in crate::setup::setup_toml::get_config_paths() {
98 if path.exists() {
99 match Self::from_file(&path).await {
100 Ok(config) => {
101 if config.poll_rate.as_millis() < 16 && show_messages {
103 log::warn!(
104 "⚡ PERFORMANCE: poll_rate sehr niedrig! {}",
105 config.get_performance_info()
106 );
107 }
108
109 if let Err(e) = crate::commands::lang::LanguageService::new()
111 .load_and_apply_from_config(&config)
112 .await
113 {
114 if show_messages {
115 log::warn!(
116 "{}",
117 get_translation(
118 "system.config.language_set_failed",
119 &[&e.to_string()]
120 )
121 );
122 }
123 }
124
125 if show_messages {
127 crate::output::logging::AppLogger::log_plain(
128 crate::i18n::get_command_translation(
129 "system.startup.version",
130 &[crate::core::constants::VERSION],
131 ),
132 );
133 }
134
135 return Ok(config);
136 }
137 Err(_e) => {
138 continue;
139 }
140 }
141 }
142 }
143
144 if show_messages {
146 log::info!("{}", get_translation("system.config.no_existing", &[]));
147 }
148
149 match crate::setup::setup_toml::ensure_config_exists().await {
150 Ok(config_path) => {
151 match Self::from_file(&config_path).await {
152 Ok(mut config) => {
153 if show_messages {
155 let plain_msg = get_translation(
156 "system.config.new_default",
157 &[&config_path.display().to_string()],
158 );
159 log::info!("{}", plain_msg);
160 config.debug_info = Some(plain_msg);
161
162 crate::output::logging::AppLogger::log_plain(
163 crate::i18n::get_command_translation(
164 "system.startup.version",
165 &[crate::core::constants::VERSION],
166 ),
167 );
168 }
169
170 let _ = crate::commands::lang::LanguageService::new()
171 .load_and_apply_from_config(&config)
172 .await;
173
174 Ok(config)
175 }
176 Err(e) => {
177 if show_messages {
178 log::error!(
179 "{}",
180 get_translation("system.config.load_error", &[&format!("{:?}", e)])
181 );
182 }
183 Err(e)
184 }
185 }
186 }
187 Err(e) => {
188 if show_messages {
189 log::error!(
190 "{}",
191 get_translation("system.config.setup_failed", &[&format!("{:?}", e)])
192 );
193 }
194 Err(e)
195 }
196 }
197 }
198
199 pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
200 let content = tokio::fs::read_to_string(&path)
201 .await
202 .map_err(AppError::Io)?;
203
204 let config_file: ConfigFile = toml::from_str(&content)
205 .map_err(|e| AppError::Validation(format!("Ungültiges TOML-Format: {}", e)))?;
206
207 let original_poll_rate = config_file.general.poll_rate;
209 let original_typewriter_delay = config_file.general.typewriter_delay;
210
211 let poll_rate = Self::validate_poll_rate(original_poll_rate);
212 let typewriter_delay = Self::validate_typewriter_delay(original_typewriter_delay);
213
214 let theme = Self::load_theme_from_config(&config_file)?;
216 let current_theme_name = config_file.general.current_theme.clone();
217
218 let config = Self {
219 config_path: Some(path.as_ref().to_string_lossy().into_owned()),
220 max_messages: config_file.general.max_messages,
221 typewriter_delay: Duration::from_millis(typewriter_delay),
222 input_max_length: config_file.general.input_max_length,
223 max_history: config_file.general.max_history,
224 poll_rate: Duration::from_millis(poll_rate),
225 log_level: config_file.general.log_level,
226 theme,
227 current_theme_name,
228 language: config_file.language.current,
230 debug_info: None,
231 };
232
233 let values_changed =
235 original_poll_rate != poll_rate || original_typewriter_delay != typewriter_delay;
236
237 if values_changed {
238 log::warn!("🔧 Ungültige Config-Werte korrigiert und gespeichert:");
239 if original_poll_rate != poll_rate {
240 log::warn!(" poll_rate: {}ms → {}ms", original_poll_rate, poll_rate);
241 }
242 if original_typewriter_delay != typewriter_delay {
243 log::warn!(
244 " typewriter_delay: {}ms → {}ms",
245 original_typewriter_delay,
246 typewriter_delay
247 );
248 }
249
250 if let Err(e) = config.save().await {
251 log::warn!("Konnte korrigierte Config nicht speichern: {}", e);
252 } else {
253 log::info!("✅ Korrigierte Werte in Config-Datei gespeichert");
254 }
255 }
256
257 Ok(config)
258 }
259
260 fn load_theme_from_config(config_file: &ConfigFile) -> Result<Theme> {
262 let current_theme_name = &config_file.general.current_theme;
263
264 if let Some(ref themes) = config_file.theme {
266 if let Some(theme_def) = themes.get(current_theme_name) {
267 return Theme::from_theme_definition_config(theme_def);
268 }
269 }
270
271 log::warn!(
273 "Theme '{}' nicht in TOML gefunden, verwende minimales Standard-Theme",
274 current_theme_name
275 );
276 Ok(Theme::default())
277 }
278
279 fn validate_poll_rate(value: u64) -> u64 {
281 match value {
282 0 => {
283 log::warn!(
284 "poll_rate = 0 nicht erlaubt, verwende Minimum: {}ms",
285 MIN_POLL_RATE
286 );
287 MIN_POLL_RATE
288 }
289 v if v < MIN_POLL_RATE => {
290 log::warn!(
291 "poll_rate = {}ms zu schnell (Performance!), verwende Minimum: {}ms",
292 v,
293 MIN_POLL_RATE
294 );
295 MIN_POLL_RATE
296 }
297 v if v > MAX_POLL_RATE => {
298 log::warn!(
299 "poll_rate = {}ms zu langsam, verwende Maximum: {}ms",
300 v,
301 MAX_POLL_RATE
302 );
303 MAX_POLL_RATE
304 }
305 v => {
306 if v < 33 {
307 log::trace!("poll_rate = {}ms (sehr schnell, aber OK)", v);
308 }
309 v
310 }
311 }
312 }
313
314 fn validate_typewriter_delay(value: u64) -> u64 {
316 match value {
317 0 => {
318 log::info!("typewriter_delay = 0 → Typewriter-Effekt deaktiviert");
319 0
320 }
321 v if v > MAX_TYPEWRITER_DELAY => {
322 log::warn!(
323 "typewriter_delay = {}ms zu langsam, verwende Maximum: {}ms",
324 v,
325 MAX_TYPEWRITER_DELAY
326 );
327 MAX_TYPEWRITER_DELAY
328 }
329 v => v,
330 }
331 }
332
333 pub fn get_performance_info(&self) -> String {
334 let fps = 1000.0 / self.poll_rate.as_millis() as f64;
335 let typewriter_chars_per_sec = if self.typewriter_delay.as_millis() > 0 {
336 1000.0 / self.typewriter_delay.as_millis() as f64
337 } else {
338 f64::INFINITY
339 };
340
341 format!(
342 "Performance: {:.1} FPS, Typewriter: {:.1} chars/sec",
343 fps, typewriter_chars_per_sec
344 )
345 }
346
347 pub async fn save(&self) -> Result<()> {
349 if let Some(path) = &self.config_path {
350 self.save_with_retry(path).await
351 } else {
352 Err(AppError::Validation("No config path available".to_string()))
353 }
354 }
355
356 async fn save_with_retry(&self, path: &str) -> Result<()> {
358 const MAX_RETRIES: u32 = 3;
359 let mut last_error = None;
360
361 for attempt in 1..=MAX_RETRIES {
362 match self.save_to_file(path).await {
363 Ok(_) => {
364 if attempt > 1 {
365 log::debug!("Config save succeeded on attempt {}", attempt);
366 }
367 return Ok(());
368 }
369 Err(e) => {
370 log::warn!("Config save attempt {} failed: {}", attempt, e);
371 last_error = Some(e);
372
373 if attempt < MAX_RETRIES {
374 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
375 }
376 }
377 }
378 }
379
380 Err(last_error.unwrap_or_else(|| AppError::Validation("Unknown save error".to_string())))
381 }
382
383 async fn save_to_file(&self, path: &str) -> Result<()> {
385 log::debug!("Saving config to: {}", path);
386
387 if std::path::Path::new(path).exists() {
389 let backup_path = format!("{}.backup", path);
390 if let Err(e) = tokio::fs::copy(path, &backup_path).await {
391 log::warn!("Could not create backup: {}", e);
392 } else {
393 log::debug!("Config backup created: {}", backup_path);
394 }
395 }
396
397 let existing_themes = Self::load_available_themes_from_current_config()
399 .await
400 .unwrap_or_else(|_| std::collections::HashMap::new());
401
402 let config_file = ConfigFile {
403 general: GeneralConfig {
404 max_messages: self.max_messages,
405 typewriter_delay: self.typewriter_delay.as_millis() as u64,
406 input_max_length: self.input_max_length,
407 max_history: self.max_history,
408 poll_rate: self.poll_rate.as_millis() as u64,
409 log_level: self.log_level.clone(),
410 current_theme: self.current_theme_name.clone(),
411 },
412 theme: if existing_themes.is_empty() {
413 None
414 } else {
415 Some(existing_themes)
416 },
417 language: LanguageConfig {
418 current: self.language.clone(),
419 },
420 };
421
422 let content = toml::to_string_pretty(&config_file).map_err(|e| {
424 log::error!("TOML serialization failed: {}", e);
425 AppError::Validation(format!("Serialisierungsfehler: {}", e))
426 })?;
427
428 if let Some(parent) = std::path::PathBuf::from(path).parent() {
430 if !parent.exists() {
431 log::debug!("Creating config directory: {}", parent.display());
432 tokio::fs::create_dir_all(parent).await.map_err(|e| {
433 log::error!("Failed to create config directory: {}", e);
434 AppError::Io(e)
435 })?;
436 }
437 }
438
439 let temp_path = format!("{}.tmp", path);
441 match tokio::fs::write(&temp_path, &content).await {
442 Ok(_) => match tokio::fs::rename(&temp_path, path).await {
443 Ok(_) => {
444 log::debug!("✅ Config successfully written to: {}", path);
445 log::debug!(" current_theme = {}", self.current_theme_name);
446 Ok(())
447 }
448 Err(e) => {
449 log::error!("Failed to rename temp file: {}", e);
450 let _ = tokio::fs::remove_file(&temp_path).await;
451 Err(AppError::Io(e))
452 }
453 },
454 Err(e) => {
455 log::error!("Failed to write temp config file: {}", e);
456 Err(AppError::Io(e))
457 }
458 }
459 }
460
461 async fn update_current_theme_in_file(&self) -> Result<()> {
463 let path = self
464 .config_path
465 .as_ref()
466 .ok_or_else(|| AppError::Validation("Kein config-Pfad gesetzt".to_string()))?;
467 let text = tokio::fs::read_to_string(path)
468 .await
469 .map_err(AppError::Io)?;
470 let mut doc = text
471 .parse::<Document>()
472 .map_err(|e| AppError::Validation(format!("Failed to parse TOML: {}", e)))?;
473 doc["general"]["current_theme"] = value(self.current_theme_name.clone());
474 if let Some(parent) = Path::new(path).parent() {
476 tokio::fs::create_dir_all(parent)
477 .await
478 .map_err(AppError::Io)?;
479 }
480 tokio::fs::write(path, doc.to_string())
481 .await
482 .map_err(AppError::Io)?;
483 Ok(())
484 }
485
486 pub async fn change_theme(&mut self, theme_name: &str) -> Result<()> {
488 log::debug!("Switch theme to {}", theme_name);
489
490 let available_themes = Self::load_available_themes_from_current_config().await?;
492
493 if let Some(theme_def) = available_themes.get(theme_name) {
494 self.theme = Theme {
495 input_text: AppColor::from_string(&theme_def.input_text)?,
496 input_bg: AppColor::from_string(&theme_def.input_bg)?,
497 cursor: AppColor::from_string(&theme_def.cursor)?,
498 output_text: AppColor::from_string(&theme_def.output_text)?,
499 output_bg: AppColor::from_string(&theme_def.output_bg)?,
500 prompt_text: theme_def.prompt_text.clone(),
501 prompt_color: AppColor::from_string(&theme_def.prompt_color)?,
502 };
503 self.current_theme_name = theme_name.to_string();
504
505 self.update_current_theme_in_file().await?;
507 log::info!("Saved current_theme to config: {}", theme_name);
508 Ok(())
509 } else {
510 let available: Vec<String> = available_themes.keys().cloned().collect();
511 Err(AppError::Validation(format!(
512 "Theme '{}' nicht in TOML gefunden. Verfügbar: {}",
513 theme_name,
514 available.join(", ")
515 )))
516 }
517 }
518
519 async fn load_available_themes_from_current_config(
521 ) -> Result<std::collections::HashMap<String, ThemeDefinitionConfig>> {
522 for path in crate::setup::setup_toml::get_config_paths() {
523 if path.exists() {
524 let content = tokio::fs::read_to_string(&path)
525 .await
526 .map_err(AppError::Io)?;
527 let config_file: ConfigFile = toml::from_str(&content)
528 .map_err(|e| AppError::Validation(format!("TOML parse error: {}", e)))?;
529
530 if let Some(themes) = config_file.theme {
531 log::debug!(
532 "Loaded {} themes from TOML: {}",
533 themes.len(),
534 themes.keys().cloned().collect::<Vec<String>>().join(", ") );
536 return Ok(themes);
537 }
538 }
539 }
540
541 log::warn!("Keine Themes in TOML gefunden");
543 Ok(std::collections::HashMap::new())
544 }
545}
546
547impl Theme {
548 fn from_theme_definition_config(theme_def: &ThemeDefinitionConfig) -> Result<Self> {
549 Ok(Self {
550 input_text: AppColor::from_string(&theme_def.input_text)?,
551 input_bg: AppColor::from_string(&theme_def.input_bg)?,
552 cursor: AppColor::from_string(&theme_def.cursor)?,
553 output_text: AppColor::from_string(&theme_def.output_text)?,
554 output_bg: AppColor::from_string(&theme_def.output_bg)?,
555 prompt_text: theme_def.prompt_text.clone(),
556 prompt_color: AppColor::from_string(&theme_def.prompt_color)?,
557 })
558 }
559}
560
561crate::impl_default!(
563 Config,
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".to_string(),
572 theme: Theme::default(),
573 current_theme_name: "dark".to_string(),
574 language: crate::i18n::DEFAULT_LANGUAGE.to_string(),
576 debug_info: None,
577 }
578);
579
580crate::impl_default!(
581 Theme,
582 Self {
583 input_text: AppColor::new(Color::White),
584 input_bg: AppColor::new(Color::Black),
585 cursor: AppColor::new(Color::White),
586 output_text: AppColor::new(Color::White),
587 output_bg: AppColor::new(Color::Black),
588 prompt_text: "/// ".to_string(), prompt_color: AppColor::new(Color::LightBlue), }
591);
592
593#[cfg(debug_assertions)]
594impl Config {
595 pub fn debug_performance_warning(&self) {
596 if self.poll_rate.as_millis() < 16 {
597 log::warn!(
598 "🔥 PERFORMANCE WARNING: poll_rate = {}ms verursacht hohe CPU-Last!",
599 self.poll_rate.as_millis()
600 );
601 log::warn!("💡 EMPFEHLUNG: Setze poll_rate auf 16-33ms für bessere Performance");
602 }
603
604 if self.typewriter_delay.as_millis() < 10 {
605 log::warn!(
606 "⚡ PERFORMANCE INFO: typewriter_delay = {}ms (sehr schnell)",
607 self.typewriter_delay.as_millis()
608 );
609 }
610
611 log::info!("📊 AKTUELLE WERTE: {}", self.get_performance_info());
612 }
613}