rush_sync_server/commands/theme/
mod.rs

1// ## FILE: src/commands/theme/mod.rs - MIT input_cursor SUPPORT
2// ## BEGIN ##
3use 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    // ✅ PERFEKTE CURSOR-KONFIGURATION (5 Felder)
17    pub input_cursor_prefix: String, // NEU: Prompt-Text
18    pub input_cursor_color: String,  // NEU: Prompt-Farbe
19    pub input_cursor: String,        // NEU: Input-Cursor-Typ
20    pub output_cursor: String,       // Output-Cursor-Typ
21    pub output_cursor_color: String, // NEU: Output-Cursor-Farbe
22}
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        // ✅ DETAILED DEBUG für Cursor-Konfiguration
96        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        // Async save
113        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(&current_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(&current_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(&current_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        // ✅ BEREINIGTE BACKWARD-KOMPATIBILITÄT
272        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())  // Changed from DEFAULT
306            .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()) // Changed from DEFAULT
332                .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, // ⬅️ Das sollte "LightBlue" sein!
452                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// ## END ##