rush_sync_server/commands/theme/
mod.rs1use crate::core::prelude::*;
4use std::collections::HashMap;
5
6pub mod command;
7pub use command::ThemeCommand;
8
9#[derive(Debug, Clone)]
10pub struct ThemeDefinition {
11 pub input_text: String,
12 pub input_bg: String,
13 pub output_text: String,
14 pub output_bg: String,
15
16 pub input_cursor_prefix: String, pub input_cursor_color: String, pub input_cursor: String, pub output_cursor: String, pub output_cursor_color: String, }
23
24#[derive(Debug)]
25pub struct ThemeSystem {
26 themes: HashMap<String, ThemeDefinition>,
27 current_name: String,
28 config_paths: Vec<std::path::PathBuf>,
29}
30
31impl ThemeSystem {
32 pub fn load() -> Result<Self> {
33 let config_paths = crate::setup::setup_toml::get_config_paths();
34 let themes = Self::load_themes_from_paths(&config_paths)?;
35 let current_name = Self::load_current_theme_name(&config_paths).unwrap_or_else(|| {
36 themes
37 .keys()
38 .next()
39 .cloned()
40 .unwrap_or_else(|| "default".to_string())
41 });
42
43 if themes.is_empty() {
44 log::warn!("❌ Keine Themes in TOML gefunden! Füge [theme.xyz] Sektionen hinzu.");
45 } else {
46 log::info!(
47 "✅ {} Themes aus TOML geladen: {}",
48 themes.len(),
49 themes.keys().cloned().collect::<Vec<String>>().join(", ")
50 );
51 }
52
53 Ok(Self {
54 themes,
55 current_name,
56 config_paths,
57 })
58 }
59
60 pub fn show_status(&self) -> String {
61 if self.themes.is_empty() {
62 return "❌ Keine Themes verfügbar! Füge [theme.xyz] Sektionen zur rush.toml hinzu."
63 .to_string();
64 }
65
66 let available: Vec<String> = self.themes.keys().cloned().collect();
67 format!(
68 "Current theme: {} (aus TOML)\nVerfügbare Themes aus TOML: {}",
69 self.current_name.to_uppercase(),
70 available.join(", ")
71 )
72 }
73
74 pub fn change_theme(&mut self, theme_name: &str) -> Result<String> {
75 let theme_name_lower = theme_name.to_lowercase();
76
77 if self.themes.is_empty() {
78 return Ok(
79 "❌ Keine Themes verfügbar! Füge [theme.xyz] Sektionen zur rush.toml hinzu."
80 .to_string(),
81 );
82 }
83
84 if !self.themes.contains_key(&theme_name_lower) {
85 let available: Vec<String> = self.themes.keys().cloned().collect();
86 return Ok(format!(
87 "❌ Theme '{}' nicht in TOML gefunden. Verfügbare TOML-Themes: {}",
88 theme_name,
89 available.join(", ")
90 ));
91 }
92
93 self.current_name = theme_name_lower.clone();
94
95 if let Some(theme_def) = self.themes.get(&theme_name_lower) {
97 log::info!(
98 "🎨 THEME CHANGE DETAILS for '{}':\n \
99 input_cursor: '{}' (color: {})\n \
100 output_cursor: '{}' (color: {})\n \
101 input_cursor_prefix: '{}' (color: {})",
102 theme_name_lower.to_uppercase(),
103 theme_def.input_cursor,
104 theme_def.input_cursor_color,
105 theme_def.output_cursor,
106 theme_def.output_cursor_color,
107 theme_def.input_cursor_prefix,
108 theme_def.input_cursor_color
109 );
110 }
111
112 let theme_name_clone = theme_name_lower.clone();
114 let config_paths = self.config_paths.clone();
115 tokio::spawn(async move {
116 if let Err(e) =
117 Self::save_current_theme_to_config(&config_paths, &theme_name_clone).await
118 {
119 log::error!("Failed to save theme to config: {}", e);
120 } else {
121 log::info!(
122 "✅ TOML-Theme '{}' saved to config",
123 theme_name_clone.to_uppercase()
124 );
125 }
126 });
127
128 Ok(format!(
129 "__LIVE_THEME_UPDATE__{}__MESSAGE__🎨 TOML-Theme changed to: {} ✨",
130 theme_name_lower,
131 theme_name_lower.to_uppercase()
132 ))
133 }
134
135 pub fn preview_theme(&self, theme_name: &str) -> Result<String> {
136 let theme_name_lower = theme_name.to_lowercase();
137
138 if self.themes.is_empty() {
139 return Ok(
140 "❌ Keine Themes verfügbar! Füge [theme.xyz] Sektionen zur rush.toml hinzu."
141 .to_string(),
142 );
143 }
144
145 if let Some(theme_def) = self.themes.get(&theme_name_lower) {
146 Ok(format!(
147 "🎨 TOML-Theme '{}' Preview:\n Input: {} auf {}\n Output: {} auf {}\n Input-Cursor-Prefix: '{}' in {} ✅ NEU!\n Input-Cursor: {} ✅ NEU!\n Output-Cursor: {} in {} ✅ NEU!\n\n📁 Quelle: [theme.{}] in rush.toml",
148 theme_name_lower.to_uppercase(),
149 theme_def.input_text,
150 theme_def.input_bg,
151 theme_def.output_text,
152 theme_def.output_bg,
153 theme_def.input_cursor_prefix,
154 theme_def.input_cursor_color,
155 theme_def.input_cursor,
156 theme_def.output_cursor,
157 theme_def.output_cursor_color,
158 theme_name_lower
159 ))
160 } else {
161 let available: Vec<String> = self.themes.keys().cloned().collect();
162 Ok(format!(
163 "❌ TOML-Theme '{}' nicht gefunden. Verfügbare: {}",
164 theme_name,
165 available.join(", ")
166 ))
167 }
168 }
169
170 pub fn theme_exists(&self, theme_name: &str) -> bool {
171 self.themes.contains_key(&theme_name.to_lowercase())
172 }
173
174 pub fn get_theme(&self, theme_name: &str) -> Option<&ThemeDefinition> {
175 self.themes.get(&theme_name.to_lowercase())
176 }
177
178 pub fn get_available_names(&self) -> Vec<String> {
179 let mut names: Vec<String> = self.themes.keys().cloned().collect();
180 names.sort();
181 names
182 }
183
184 pub fn get_current_name(&self) -> &str {
185 &self.current_name
186 }
187
188 fn load_themes_from_paths(
189 config_paths: &[std::path::PathBuf],
190 ) -> Result<HashMap<String, ThemeDefinition>> {
191 for path in config_paths {
192 if path.exists() {
193 if let Ok(content) = std::fs::read_to_string(path) {
194 if let Ok(themes) = Self::parse_themes_from_toml(&content) {
195 log::debug!(
196 "✅ {} TOML-Themes geladen aus: {}",
197 themes.len(),
198 themes.keys().cloned().collect::<Vec<String>>().join(", ")
199 );
200 return Ok(themes);
201 }
202 }
203 }
204 }
205
206 log::warn!("❌ Keine TOML-Themes gefunden! Erstelle [theme.xyz] Sektionen.");
207 Ok(HashMap::new())
208 }
209
210 fn parse_themes_from_toml(content: &str) -> Result<HashMap<String, ThemeDefinition>> {
211 let mut themes = HashMap::new();
212 let mut current_theme_name: Option<String> = None;
213 let mut current_theme_data: HashMap<String, String> = HashMap::new();
214
215 for line in content.lines() {
216 let trimmed = line.trim();
217
218 if trimmed.is_empty() || trimmed.starts_with('#') {
219 continue;
220 }
221
222 if trimmed.starts_with("[theme.") && trimmed.ends_with(']') {
223 if let Some(theme_name) = current_theme_name.take() {
224 if let Some(theme_def) = Self::build_theme_from_data(¤t_theme_data) {
225 themes.insert(theme_name, theme_def);
226 }
227 current_theme_data.clear();
228 }
229
230 if let Some(name) = trimmed
231 .strip_prefix("[theme.")
232 .and_then(|s| s.strip_suffix(']'))
233 {
234 current_theme_name = Some(name.to_lowercase());
235 }
236 } else if trimmed.starts_with('[')
237 && trimmed.ends_with(']')
238 && !trimmed.starts_with("[theme.")
239 {
240 if let Some(theme_name) = current_theme_name.take() {
241 if let Some(theme_def) = Self::build_theme_from_data(¤t_theme_data) {
242 themes.insert(theme_name, theme_def);
243 }
244 current_theme_data.clear();
245 }
246 } else if current_theme_name.is_some() && trimmed.contains('=') {
247 if let Some((key, value)) = trimmed.split_once('=') {
248 let clean_key = key.trim().to_string();
249 let clean_value = value
250 .trim()
251 .trim_matches('"')
252 .trim_matches('\'')
253 .to_string();
254 if !clean_value.is_empty() {
255 current_theme_data.insert(clean_key, clean_value);
256 }
257 }
258 }
259 }
260
261 if let Some(theme_name) = current_theme_name {
262 if let Some(theme_def) = Self::build_theme_from_data(¤t_theme_data) {
263 themes.insert(theme_name, theme_def);
264 }
265 }
266
267 Ok(themes)
268 }
269
270 fn build_theme_from_data(data: &HashMap<String, String>) -> Option<ThemeDefinition> {
271 let input_cursor_prefix = data.get("input_cursor_prefix")
273 .or_else(|| {
274 if let Some(legacy) = data.get("prompt_text") {
275 log::warn!("⚠️ Veraltetes 'prompt_text' in Theme gefunden, verwende als 'input_cursor_prefix': {}", legacy);
276 Some(legacy)
277 } else {
278 None
279 }
280 })
281 .unwrap_or(&"/// ".to_string())
282 .clone();
283
284 let input_cursor_color = data.get("input_cursor_color")
285 .or_else(|| {
286 if let Some(legacy) = data.get("prompt_color") {
287 log::warn!("⚠️ Veraltetes 'prompt_color' in Theme gefunden, verwende als 'input_cursor_color': {}", legacy);
288 Some(legacy)
289 } else {
290 None
291 }
292 })
293 .unwrap_or(&"LightBlue".to_string())
294 .clone();
295
296 let input_cursor = data.get("input_cursor")
297 .or_else(|| {
298 if let Some(legacy) = data.get("prompt_cursor") {
299 log::warn!("⚠️ Veraltetes 'prompt_cursor' in Theme gefunden, verwende als 'input_cursor': {}", legacy);
300 Some(legacy)
301 } else {
302 None
303 }
304 })
305 .unwrap_or(&"PIPE".to_string()) .clone();
307
308 let output_cursor_color = data.get("output_cursor_color")
309 .or_else(|| {
310 if let Some(legacy) = data.get("output_color") {
311 log::warn!("⚠️ Veraltetes 'output_color' in Theme gefunden, verwende als 'output_cursor_color': {}", legacy);
312 Some(legacy)
313 } else {
314 None
315 }
316 })
317 .unwrap_or(&"White".to_string())
318 .clone();
319
320 Some(ThemeDefinition {
321 input_text: data.get("input_text")?.clone(),
322 input_bg: data.get("input_bg")?.clone(),
323 output_text: data.get("output_text")?.clone(),
324 output_bg: data.get("output_bg")?.clone(),
325
326 input_cursor_prefix,
327 input_cursor_color,
328 input_cursor,
329 output_cursor: data
330 .get("output_cursor")
331 .unwrap_or(&"PIPE".to_string()) .clone(),
333 output_cursor_color,
334 })
335 }
336
337 fn load_current_theme_name(config_paths: &[std::path::PathBuf]) -> Option<String> {
338 for path in config_paths {
339 if path.exists() {
340 if let Ok(content) = std::fs::read_to_string(path) {
341 if let Some(theme) = Self::extract_current_theme_from_toml(&content) {
342 return Some(theme);
343 }
344 }
345 }
346 }
347 None
348 }
349
350 fn extract_current_theme_from_toml(content: &str) -> Option<String> {
351 let mut in_general_section = false;
352
353 for line in content.lines() {
354 let trimmed = line.trim();
355 if trimmed.is_empty() || trimmed.starts_with('#') {
356 continue;
357 }
358
359 if trimmed == "[general]" {
360 in_general_section = true;
361 } else if trimmed.starts_with('[') && trimmed != "[general]" {
362 in_general_section = false;
363 } else if in_general_section && trimmed.starts_with("current_theme") {
364 if let Some(value_part) = trimmed.split('=').nth(1) {
365 let cleaned = value_part.trim().trim_matches('"').trim_matches('\'');
366 if !cleaned.is_empty() {
367 return Some(cleaned.to_string());
368 }
369 }
370 }
371 }
372 None
373 }
374
375 async fn save_current_theme_to_config(
376 config_paths: &[std::path::PathBuf],
377 theme_name: &str,
378 ) -> Result<()> {
379 for path in config_paths {
380 if path.exists() {
381 let content = tokio::fs::read_to_string(path)
382 .await
383 .map_err(AppError::Io)?;
384 let updated_content = Self::update_current_theme_in_toml(&content, theme_name)?;
385 tokio::fs::write(path, updated_content)
386 .await
387 .map_err(AppError::Io)?;
388 return Ok(());
389 }
390 }
391 Err(AppError::Validation("No config file found".to_string()))
392 }
393
394 fn update_current_theme_in_toml(content: &str, theme_name: &str) -> Result<String> {
395 let mut lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
396 let mut in_general_section = false;
397 let mut theme_updated = false;
398
399 for line in lines.iter_mut() {
400 let trimmed = line.trim();
401
402 if trimmed == "[general]" {
403 in_general_section = true;
404 } else if trimmed.starts_with('[') && trimmed != "[general]" {
405 in_general_section = false;
406 } else if in_general_section && trimmed.starts_with("current_theme") {
407 *line = format!("current_theme = \"{}\"", theme_name);
408 theme_updated = true;
409 }
410 }
411
412 if !theme_updated {
413 for (i, line) in lines.iter().enumerate() {
414 if line.trim() == "[general]" {
415 let insert_index = lines
416 .iter()
417 .enumerate()
418 .skip(i + 1)
419 .find(|(_, line)| line.trim().starts_with('['))
420 .map(|(idx, _)| idx)
421 .unwrap_or(lines.len());
422
423 lines.insert(insert_index, format!("current_theme = \"{}\"", theme_name));
424 break;
425 }
426 }
427 }
428
429 Ok(lines.join("\n"))
430 }
431
432 pub fn debug_theme_details(&self, theme_name: &str) -> String {
433 if let Some(theme_def) = self.themes.get(&theme_name.to_lowercase()) {
434 format!(
435 "🔍 THEME DEBUG für '{}':\n\
436 📝 input_text: '{}'\n\
437 📝 input_bg: '{}'\n\
438 📝 output_text: '{}'\n\
439 📝 output_bg: '{}'\n\
440 🎯 input_cursor_prefix: '{}'\n\
441 🎨 input_cursor_color: '{}' ⬅️ DAS IST WICHTIG!\n\
442 🎯 input_cursor: '{}'\n\
443 🎯 output_cursor: '{}'\n\
444 🎨 output_cursor_color: '{}'",
445 theme_name.to_uppercase(),
446 theme_def.input_text,
447 theme_def.input_bg,
448 theme_def.output_text,
449 theme_def.output_bg,
450 theme_def.input_cursor_prefix,
451 theme_def.input_cursor_color, theme_def.input_cursor,
453 theme_def.output_cursor,
454 theme_def.output_cursor_color
455 )
456 } else {
457 format!("❌ Theme '{}' nicht gefunden!", theme_name)
458 }
459 }
460}
461