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::path::{Path, PathBuf};
10
11#[derive(Debug, Serialize, Deserialize)]
12struct ConfigFile {
13 general: GeneralConfig,
14 theme: ThemeConfig,
15 prompt: PromptConfig,
16 language: LanguageConfig,
17}
18
19#[derive(Debug, Serialize, Deserialize)]
20struct GeneralConfig {
21 max_messages: usize,
22 typewriter_delay: u64,
23 input_max_length: usize,
24 max_history: usize, poll_rate: u64,
26}
27
28#[derive(Debug, Serialize, Deserialize)]
29struct ThemeConfig {
30 input_text: String,
31 input_bg: String,
32 cursor: String,
33 output_text: String,
34 output_bg: String,
35}
36
37#[derive(Debug, Serialize, Deserialize)]
38struct PromptConfig {
39 text: String,
40 color: String,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
44struct LanguageConfig {
45 current: String,
46}
47
48pub struct Config {
49 config_path: Option<String>,
50 pub max_messages: usize,
51 pub typewriter_delay: Duration,
52 pub input_max_length: usize,
53 pub max_history: usize, pub poll_rate: Duration,
55 pub theme: Theme,
56 pub prompt: Prompt,
57 pub language: String,
58 pub debug_info: Option<String>,
59}
60
61pub struct Theme {
62 pub input_text: AppColor,
63 pub input_bg: AppColor,
64 pub cursor: AppColor,
65 pub output_text: AppColor,
66 pub output_bg: AppColor,
67}
68
69pub struct Prompt {
70 pub text: String,
71 pub color: AppColor,
72}
73
74impl Config {
75 pub async fn load() -> Result<Self> {
76 Self::load_with_messages(true).await
77 }
78
79 pub async fn load_with_messages(show_messages: bool) -> Result<Self> {
80 let mut last_error = None;
81
82 for path in crate::setup::setup_toml::get_config_paths() {
83 if path.exists() {
84 match Self::from_file(&path).await {
85 Ok(config) => {
86 if let Err(e) = crate::commands::lang::config::LanguageConfig::load_and_apply_from_config(&config).await {
88 if show_messages {
89 log::warn!(
90 "{}",
91 get_translation(
92 "system.config.language_set_failed",
93 &[&e.to_string()]
94 )
95 );
96 }
97 }
98
99 if show_messages {
100 log::debug!(
101 "{}",
102 get_translation(
103 "system.config.loaded",
104 &[&path.display().to_string()]
105 )
106 );
107
108 log::info!(
109 "{}",
110 crate::i18n::get_command_translation(
111 "system.startup.version",
112 &[crate::core::constants::VERSION]
113 )
114 );
115 }
116
117 return Ok(config);
118 }
119 Err(e) => {
120 last_error = Some(e);
121 continue;
122 }
123 }
124 }
125 }
126
127 if show_messages {
128 log::info!("{}", get_translation("system.config.no_existing", &[]));
129 }
130
131 match crate::setup::setup_toml::ensure_config_exists().await {
132 Ok(config_path) => {
133 match Self::from_file(&config_path).await {
134 Ok(mut config) => {
135 if show_messages {
136 let plain_msg = get_translation(
137 "system.config.new_default",
138 &[&config_path.display().to_string()],
139 );
140 log::info!("{}", plain_msg);
141 config.debug_info = Some(plain_msg);
142
143 log::info!(
144 "{}",
145 crate::i18n::get_command_translation(
146 "system.startup.version",
147 &[crate::core::constants::VERSION]
148 )
149 );
150 }
151
152 let _ = crate::commands::lang::config::LanguageConfig::load_and_apply_from_config(&config).await;
153
154 Ok(config)
155 }
156 Err(e) => {
157 if show_messages {
158 log::error!(
159 "{}",
160 get_translation("system.config.load_error", &[&format!("{:?}", e)])
161 );
162 }
163 Err(e)
164 }
165 }
166 }
167 Err(e) => {
168 if show_messages {
169 log::error!(
170 "{}",
171 get_translation("system.config.setup_failed", &[&format!("{:?}", e)])
172 );
173 if let Some(last_e) = last_error {
174 log::debug!(
175 "{}",
176 get_translation(
177 "system.config.last_error",
178 &[&format!("{:?}", last_e)]
179 )
180 );
181 }
182 }
183 Err(e)
184 }
185 }
186 }
187
188 pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
189 let content = tokio::fs::read_to_string(&path)
190 .await
191 .map_err(AppError::Io)?;
192
193 let config_file: ConfigFile = toml::from_str(&content)
194 .map_err(|e| AppError::Validation(format!("Ungültiges TOML-Format: {}", e)))?;
195
196 Ok(Self {
197 config_path: Some(path.as_ref().to_string_lossy().into_owned()),
198 max_messages: config_file.general.max_messages,
199 typewriter_delay: Duration::from_millis(config_file.general.typewriter_delay),
200 input_max_length: config_file.general.input_max_length,
201 max_history: config_file.general.max_history, poll_rate: Duration::from_millis(config_file.general.poll_rate),
203 theme: Theme::from_config(&config_file.theme)?,
204 prompt: Prompt::from_config(&config_file.prompt)?,
205 language: config_file.language.current,
206 debug_info: None,
207 })
208 }
209
210 pub async fn save(&self) -> Result<()> {
211 if let Some(path) = &self.config_path {
212 let config_file = ConfigFile {
213 general: GeneralConfig {
214 max_messages: self.max_messages,
215 typewriter_delay: self.typewriter_delay.as_millis() as u64,
216 input_max_length: self.input_max_length,
217 max_history: self.max_history, poll_rate: self.poll_rate.as_millis() as u64,
219 },
220 theme: ThemeConfig {
221 input_text: self.theme.input_text.to_string(),
222 input_bg: self.theme.input_bg.to_string(),
223 cursor: self.theme.cursor.to_string(),
224 output_text: self.theme.output_text.to_string(),
225 output_bg: self.theme.output_bg.to_string(),
226 },
227 prompt: PromptConfig {
228 text: self.prompt.text.clone(),
229 color: self.prompt.color.to_string(),
230 },
231 language: LanguageConfig {
232 current: self.language.clone(),
233 },
234 };
235
236 let content = toml::to_string_pretty(&config_file)
237 .map_err(|e| AppError::Validation(format!("Serialisierungsfehler: {}", e)))?;
238
239 if let Some(parent) = PathBuf::from(path).parent() {
240 if !parent.exists() {
241 tokio::fs::create_dir_all(parent)
242 .await
243 .map_err(AppError::Io)?;
244 }
245 }
246
247 tokio::fs::write(path, content)
248 .await
249 .map_err(AppError::Io)?;
250 }
251 Ok(())
252 }
253}
254
255impl Theme {
256 fn from_config(config: &ThemeConfig) -> Result<Self> {
257 Ok(Self {
258 input_text: AppColor::from_string(&config.input_text)?,
259 input_bg: AppColor::from_string(&config.input_bg)?,
260 cursor: AppColor::from_string(&config.cursor)?,
261 output_text: AppColor::from_string(&config.output_text)?,
262 output_bg: AppColor::from_string(&config.output_bg)?,
263 })
264 }
265}
266
267impl Prompt {
268 fn from_config(config: &PromptConfig) -> Result<Self> {
269 Ok(Self {
270 text: config.text.clone(),
271 color: AppColor::from_string(&config.color)?,
272 })
273 }
274}
275
276crate::impl_default!(
277 Config,
278 Self {
279 config_path: None,
280 max_messages: DEFAULT_BUFFER_SIZE,
281 typewriter_delay: Duration::from_millis(50),
282 input_max_length: DEFAULT_BUFFER_SIZE,
283 max_history: 30,
284 poll_rate: Duration::from_millis(DEFAULT_POLL_RATE),
285 theme: Theme::default(),
286 prompt: Prompt::default(),
287 language: crate::i18n::DEFAULT_LANGUAGE.to_string(),
288 debug_info: None,
289 }
290);
291
292crate::impl_default!(
293 Theme,
294 Self {
295 input_text: AppColor::new(Color::Black),
296 input_bg: AppColor::new(Color::Black),
297 cursor: AppColor::new(Color::Black),
298 output_text: AppColor::new(Color::White),
299 output_bg: AppColor::new(Color::White),
300 }
301);
302
303crate::impl_default!(
304 Prompt,
305 Self {
306 text: "/// ".to_string(),
307 color: AppColor::new(Color::Black),
308 }
309);