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