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;
11
12#[derive(Debug, Serialize, Deserialize)]
13struct ConfigFile {
14 general: GeneralConfig,
15 #[serde(default)]
16 theme: Option<HashMap<String, ThemeDefinitionConfig>>,
17 language: LanguageConfig,
18}
19
20#[derive(Debug, Serialize, Deserialize)]
21struct GeneralConfig {
22 max_messages: usize,
23 typewriter_delay: u64,
24 input_max_length: usize,
25 max_history: usize,
26 poll_rate: u64,
27 log_level: String,
28 #[serde(default = "default_theme_name")]
29 current_theme: String,
30}
31
32#[derive(Debug, Serialize, Deserialize)]
33struct LanguageConfig {
34 current: String,
35}
36
37#[derive(Debug, Serialize, Deserialize, Clone)]
38struct ThemeDefinitionConfig {
39 input_text: String,
40 input_bg: String,
41 output_text: String,
42 output_bg: String,
43
44 #[serde(default = "default_input_cursor_prefix")]
46 input_cursor_prefix: String,
47
48 #[serde(default = "default_input_cursor_color")]
49 input_cursor_color: String,
50
51 #[serde(default = "default_input_cursor")]
52 input_cursor: String,
53
54 #[serde(default = "default_output_cursor")]
55 output_cursor: String,
56
57 #[serde(default = "default_output_cursor_color")]
58 output_cursor_color: String,
59}
60
61fn default_theme_name() -> String {
62 "dark".to_string()
63}
64
65fn default_input_cursor_prefix() -> String {
67 "/// ".to_string()
68}
69fn default_input_cursor_color() -> String {
70 "LightBlue".to_string()
71}
72fn default_input_cursor() -> String {
73 "PIPE".to_string()
74}
75fn default_output_cursor() -> String {
76 "PIPE".to_string()
77}
78fn default_output_cursor_color() -> String {
79 "White".to_string()
80}
81
82#[derive(Clone)]
83pub struct Config {
84 config_path: Option<String>,
85 pub max_messages: usize,
86 pub typewriter_delay: Duration,
87 pub input_max_length: usize,
88 pub max_history: usize,
89 pub poll_rate: Duration,
90 pub log_level: String,
91 pub theme: Theme,
92 pub current_theme_name: String,
93 pub language: String,
94 pub debug_info: Option<String>,
95}
96
97#[derive(Clone)]
98pub struct Theme {
99 pub input_text: AppColor,
100 pub input_bg: AppColor,
101 pub output_text: AppColor,
102 pub output_bg: AppColor,
103
104 pub input_cursor_prefix: String,
106 pub input_cursor_color: AppColor,
107 pub input_cursor: String,
108 pub output_cursor: String,
109 pub output_cursor_color: AppColor,
110}
111
112impl Default for Theme {
113 fn default() -> Self {
114 Self {
115 input_text: AppColor::new(Color::White),
116 input_bg: AppColor::new(Color::Black),
117 output_text: AppColor::new(Color::White),
118 output_bg: AppColor::new(Color::Black),
119
120 input_cursor_prefix: "/// ".to_string(),
122 input_cursor_color: AppColor::new(Color::LightBlue),
123 input_cursor: "PIPE".to_string(),
124 output_cursor: "PIPE".to_string(),
125 output_cursor_color: AppColor::new(Color::White),
126 }
127 }
128}
129
130impl Config {
131 pub async fn load() -> Result<Self> {
132 Self::load_with_messages(true).await
133 }
134
135 pub async fn load_with_messages(show_messages: bool) -> Result<Self> {
136 for path in crate::setup::setup_toml::get_config_paths() {
137 if path.exists() {
138 if let Ok(config) = Self::from_file(&path).await {
139 if show_messages && config.poll_rate.as_millis() < 16 {
140 log::warn!("⚡ PERFORMANCE: poll_rate sehr niedrig!");
141 }
142
143 let _ = crate::commands::lang::LanguageService::new()
144 .load_and_apply_from_config(&config)
145 .await;
146
147 if show_messages {
148 log::info!("Rush Sync Server v{}", crate::core::constants::VERSION);
149 }
150 return Ok(config);
151 }
152 }
153 }
154
155 if show_messages {
156 log::info!("Keine Config gefunden, erstelle neue");
157 }
158
159 let config_path = crate::setup::setup_toml::ensure_config_exists().await?;
160 let mut config = Self::from_file(&config_path).await?;
161
162 if show_messages {
163 config.debug_info = Some(format!("Neue Config erstellt: {}", config_path.display()));
164 log::info!("Rush Sync Server v{}", crate::core::constants::VERSION);
165 }
166
167 let _ = crate::commands::lang::LanguageService::new()
168 .load_and_apply_from_config(&config)
169 .await;
170
171 Ok(config)
172 }
173
174 pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
175 let content = tokio::fs::read_to_string(&path)
176 .await
177 .map_err(AppError::Io)?;
178 let config_file: ConfigFile = toml::from_str(&content)
179 .map_err(|e| AppError::Validation(format!("TOML Error: {}", e)))?;
180
181 let poll_rate = Self::validate_range(config_file.general.poll_rate, 16, 1000, 16);
182 let typewriter_delay =
183 Self::validate_range(config_file.general.typewriter_delay, 0, 2000, 50);
184
185 let theme = Self::load_theme_from_config(&config_file)?;
186
187 let config = Self {
188 config_path: Some(path.as_ref().to_string_lossy().into_owned()),
189 max_messages: config_file.general.max_messages,
190 typewriter_delay: Duration::from_millis(typewriter_delay),
191 input_max_length: config_file.general.input_max_length,
192 max_history: config_file.general.max_history,
193 poll_rate: Duration::from_millis(poll_rate),
194 log_level: config_file.general.log_level,
195 theme,
196 current_theme_name: config_file.general.current_theme,
197 language: config_file.language.current,
198 debug_info: None,
199 };
200
201 if poll_rate != config_file.general.poll_rate
202 || typewriter_delay != config_file.general.typewriter_delay
203 {
204 log::warn!("Config-Werte korrigiert und gespeichert");
205 let _ = config.save().await;
206 }
207
208 Ok(config)
209 }
210
211 fn validate_range(value: u64, min: u64, max: u64, default: u64) -> u64 {
212 if value < min || value > max {
213 log::warn!(
214 "Wert {} außerhalb Bereich {}-{}, verwende {}",
215 value,
216 min,
217 max,
218 default
219 );
220 default
221 } else {
222 value
223 }
224 }
225
226 fn load_theme_from_config(config_file: &ConfigFile) -> Result<Theme> {
227 let current_theme_name = &config_file.general.current_theme;
228
229 if let Some(ref themes) = config_file.theme {
230 if let Some(theme_def) = themes.get(current_theme_name) {
231 return Theme::from_config(theme_def);
232 }
233 }
234
235 log::warn!(
236 "Theme '{}' nicht gefunden, verwende Standard",
237 current_theme_name
238 );
239 Ok(Theme::default())
240 }
241
242 pub async fn save(&self) -> Result<()> {
243 if let Some(path) = &self.config_path {
244 let existing_themes = Self::load_themes_from_config().await.unwrap_or_default();
245
246 let config_file = ConfigFile {
247 general: GeneralConfig {
248 max_messages: self.max_messages,
249 typewriter_delay: self.typewriter_delay.as_millis() as u64,
250 input_max_length: self.input_max_length,
251 max_history: self.max_history,
252 poll_rate: self.poll_rate.as_millis() as u64,
253 log_level: self.log_level.clone(),
254 current_theme: self.current_theme_name.clone(),
255 },
256 theme: if existing_themes.is_empty() {
257 None
258 } else {
259 Some(existing_themes)
260 },
261 language: LanguageConfig {
262 current: self.language.clone(),
263 },
264 };
265
266 let content = toml::to_string_pretty(&config_file)
267 .map_err(|e| AppError::Validation(format!("TOML Error: {}", e)))?;
268
269 if let Some(parent) = std::path::PathBuf::from(path).parent() {
270 tokio::fs::create_dir_all(parent)
271 .await
272 .map_err(AppError::Io)?;
273 }
274
275 tokio::fs::write(path, content)
276 .await
277 .map_err(AppError::Io)?;
278 }
279 Ok(())
280 }
281
282 pub async fn change_theme(&mut self, theme_name: &str) -> Result<()> {
283 let available_themes = Self::load_themes_from_config().await?;
284
285 if let Some(theme_def) = available_themes.get(theme_name) {
286 self.theme = Theme::from_config(theme_def)?;
287 self.current_theme_name = theme_name.to_string();
288 self.save().await?;
289 log::info!("Theme gewechselt zu: {}", theme_name);
290 Ok(())
291 } else {
292 Err(AppError::Validation(format!(
293 "Theme '{}' nicht gefunden",
294 theme_name
295 )))
296 }
297 }
298
299 async fn load_themes_from_config() -> Result<HashMap<String, ThemeDefinitionConfig>> {
300 for path in crate::setup::setup_toml::get_config_paths() {
301 if path.exists() {
302 let content = tokio::fs::read_to_string(&path)
303 .await
304 .map_err(AppError::Io)?;
305 let config_file: ConfigFile = toml::from_str(&content)
306 .map_err(|e| AppError::Validation(format!("TOML Error: {}", e)))?;
307
308 if let Some(themes) = config_file.theme {
309 return Ok(themes);
310 }
311 }
312 }
313 Ok(HashMap::new())
314 }
315
316 pub fn get_performance_info(&self) -> String {
317 let fps = 1000.0 / self.poll_rate.as_millis() as f64;
318 let typewriter_chars_per_sec = if self.typewriter_delay.as_millis() > 0 {
319 1000.0 / self.typewriter_delay.as_millis() as f64
320 } else {
321 f64::INFINITY
322 };
323
324 format!(
325 "Performance: {:.1} FPS, Typewriter: {:.1} chars/sec",
326 fps, typewriter_chars_per_sec
327 )
328 }
329}
330
331impl Theme {
332 fn from_config(theme_def: &ThemeDefinitionConfig) -> Result<Self> {
334 Ok(Self {
335 input_text: AppColor::from_string(&theme_def.input_text)?,
336 input_bg: AppColor::from_string(&theme_def.input_bg)?,
337 output_text: AppColor::from_string(&theme_def.output_text)?,
338 output_bg: AppColor::from_string(&theme_def.output_bg)?,
339
340 input_cursor_prefix: theme_def.input_cursor_prefix.clone(),
342 input_cursor_color: AppColor::from_string(&theme_def.input_cursor_color)?,
343 input_cursor: theme_def.input_cursor.clone(),
344 output_cursor: theme_def.output_cursor.clone(),
345 output_cursor_color: AppColor::from_string(&theme_def.output_cursor_color)?,
346 })
347 }
348}
349
350impl Default for Config {
351 fn default() -> Self {
352 Self {
353 config_path: None,
354 max_messages: DEFAULT_BUFFER_SIZE,
355 typewriter_delay: Duration::from_millis(50),
356 input_max_length: DEFAULT_BUFFER_SIZE,
357 max_history: 30,
358 poll_rate: Duration::from_millis(DEFAULT_POLL_RATE),
359 log_level: "info".to_string(),
360 theme: Theme::default(),
361 current_theme_name: "dark".to_string(),
362 language: crate::i18n::DEFAULT_LANGUAGE.to_string(),
363 debug_info: None,
364 }
365 }
366}