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                        return Ok(themes);
196                    }
197                }
198            }
199        }
200
201        log::warn!("❌ Keine TOML-Themes gefunden! Erstelle [theme.xyz] Sektionen.");
202        Ok(HashMap::new())
203    }
204
205    fn parse_themes_from_toml(content: &str) -> Result<HashMap<String, ThemeDefinition>> {
206        let mut themes = HashMap::new();
207        let mut current_theme_name: Option<String> = None;
208        let mut current_theme_data: HashMap<String, String> = HashMap::new();
209
210        for line in content.lines() {
211            let trimmed = line.trim();
212
213            if trimmed.is_empty() || trimmed.starts_with('#') {
214                continue;
215            }
216
217            if trimmed.starts_with("[theme.") && trimmed.ends_with(']') {
218                if let Some(theme_name) = current_theme_name.take() {
219                    if let Some(theme_def) = Self::build_theme_from_data(&current_theme_data) {
220                        themes.insert(theme_name, theme_def);
221                    }
222                    current_theme_data.clear();
223                }
224
225                if let Some(name) = trimmed
226                    .strip_prefix("[theme.")
227                    .and_then(|s| s.strip_suffix(']'))
228                {
229                    current_theme_name = Some(name.to_lowercase());
230                }
231            } else if trimmed.starts_with('[')
232                && trimmed.ends_with(']')
233                && !trimmed.starts_with("[theme.")
234            {
235                if let Some(theme_name) = current_theme_name.take() {
236                    if let Some(theme_def) = Self::build_theme_from_data(&current_theme_data) {
237                        themes.insert(theme_name, theme_def);
238                    }
239                    current_theme_data.clear();
240                }
241            } else if current_theme_name.is_some() && trimmed.contains('=') {
242                if let Some((key, value)) = trimmed.split_once('=') {
243                    let clean_key = key.trim().to_string();
244                    let clean_value = value
245                        .trim()
246                        .trim_matches('"')
247                        .trim_matches('\'')
248                        .to_string();
249                    if !clean_value.is_empty() {
250                        current_theme_data.insert(clean_key, clean_value);
251                    }
252                }
253            }
254        }
255
256        if let Some(theme_name) = current_theme_name {
257            if let Some(theme_def) = Self::build_theme_from_data(&current_theme_data) {
258                themes.insert(theme_name, theme_def);
259            }
260        }
261
262        Ok(themes)
263    }
264
265    fn build_theme_from_data(data: &HashMap<String, String>) -> Option<ThemeDefinition> {
266        // ✅ BEREINIGTE BACKWARD-KOMPATIBILITÄT
267        let input_cursor_prefix = data.get("input_cursor_prefix")
268            .or_else(|| {
269                if let Some(legacy) = data.get("prompt_text") {
270                    log::warn!("⚠️ Veraltetes 'prompt_text' in Theme gefunden, verwende als 'input_cursor_prefix': {}", legacy);
271                    Some(legacy)
272                } else {
273                    None
274                }
275            })
276            .unwrap_or(&"/// ".to_string())
277            .clone();
278
279        let input_cursor_color = data.get("input_cursor_color")
280            .or_else(|| {
281                if let Some(legacy) = data.get("prompt_color") {
282                    log::warn!("⚠️ Veraltetes 'prompt_color' in Theme gefunden, verwende als 'input_cursor_color': {}", legacy);
283                    Some(legacy)
284                } else {
285                    None
286                }
287            })
288            .unwrap_or(&"LightBlue".to_string())
289            .clone();
290
291        let input_cursor = data.get("input_cursor")
292            .or_else(|| {
293                if let Some(legacy) = data.get("prompt_cursor") {
294                    log::warn!("⚠️ Veraltetes 'prompt_cursor' in Theme gefunden, verwende als 'input_cursor': {}", legacy);
295                    Some(legacy)
296                } else {
297                    None
298                }
299            })
300            .unwrap_or(&"PIPE".to_string())  // Changed from DEFAULT
301            .clone();
302
303        let output_cursor_color = data.get("output_cursor_color")
304            .or_else(|| {
305                if let Some(legacy) = data.get("output_color") {
306                    log::warn!("⚠️ Veraltetes 'output_color' in Theme gefunden, verwende als 'output_cursor_color': {}", legacy);
307                    Some(legacy)
308                } else {
309                    None
310                }
311            })
312            .unwrap_or(&"White".to_string())
313            .clone();
314
315        Some(ThemeDefinition {
316            input_text: data.get("input_text")?.clone(),
317            input_bg: data.get("input_bg")?.clone(),
318            output_text: data.get("output_text")?.clone(),
319            output_bg: data.get("output_bg")?.clone(),
320
321            input_cursor_prefix,
322            input_cursor_color,
323            input_cursor,
324            output_cursor: data
325                .get("output_cursor")
326                .unwrap_or(&"PIPE".to_string()) // Changed from DEFAULT
327                .clone(),
328            output_cursor_color,
329        })
330    }
331
332    fn load_current_theme_name(config_paths: &[std::path::PathBuf]) -> Option<String> {
333        for path in config_paths {
334            if path.exists() {
335                if let Ok(content) = std::fs::read_to_string(path) {
336                    if let Some(theme) = Self::extract_current_theme_from_toml(&content) {
337                        return Some(theme);
338                    }
339                }
340            }
341        }
342        None
343    }
344
345    fn extract_current_theme_from_toml(content: &str) -> Option<String> {
346        let mut in_general_section = false;
347
348        for line in content.lines() {
349            let trimmed = line.trim();
350            if trimmed.is_empty() || trimmed.starts_with('#') {
351                continue;
352            }
353
354            if trimmed == "[general]" {
355                in_general_section = true;
356            } else if trimmed.starts_with('[') && trimmed != "[general]" {
357                in_general_section = false;
358            } else if in_general_section && trimmed.starts_with("current_theme") {
359                if let Some(value_part) = trimmed.split('=').nth(1) {
360                    let cleaned = value_part.trim().trim_matches('"').trim_matches('\'');
361                    if !cleaned.is_empty() {
362                        return Some(cleaned.to_string());
363                    }
364                }
365            }
366        }
367        None
368    }
369
370    async fn save_current_theme_to_config(
371        config_paths: &[std::path::PathBuf],
372        theme_name: &str,
373    ) -> Result<()> {
374        for path in config_paths {
375            if path.exists() {
376                let content = tokio::fs::read_to_string(path)
377                    .await
378                    .map_err(AppError::Io)?;
379                let updated_content = Self::update_current_theme_in_toml(&content, theme_name)?;
380                tokio::fs::write(path, updated_content)
381                    .await
382                    .map_err(AppError::Io)?;
383                return Ok(());
384            }
385        }
386        Err(AppError::Validation("No config file found".to_string()))
387    }
388
389    fn update_current_theme_in_toml(content: &str, theme_name: &str) -> Result<String> {
390        let mut lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
391        let mut in_general_section = false;
392        let mut theme_updated = false;
393
394        for line in lines.iter_mut() {
395            let trimmed = line.trim();
396
397            if trimmed == "[general]" {
398                in_general_section = true;
399            } else if trimmed.starts_with('[') && trimmed != "[general]" {
400                in_general_section = false;
401            } else if in_general_section && trimmed.starts_with("current_theme") {
402                *line = format!("current_theme = \"{}\"", theme_name);
403                theme_updated = true;
404            }
405        }
406
407        if !theme_updated {
408            for (i, line) in lines.iter().enumerate() {
409                if line.trim() == "[general]" {
410                    let insert_index = lines
411                        .iter()
412                        .enumerate()
413                        .skip(i + 1)
414                        .find(|(_, line)| line.trim().starts_with('['))
415                        .map(|(idx, _)| idx)
416                        .unwrap_or(lines.len());
417
418                    lines.insert(insert_index, format!("current_theme = \"{}\"", theme_name));
419                    break;
420                }
421            }
422        }
423
424        Ok(lines.join("\n"))
425    }
426
427    pub fn debug_theme_details(&self, theme_name: &str) -> String {
428        if let Some(theme_def) = self.themes.get(&theme_name.to_lowercase()) {
429            format!(
430                "🔍 THEME DEBUG für '{}':\n\
431                📝 input_text: '{}'\n\
432                📝 input_bg: '{}'\n\
433                📝 output_text: '{}'\n\
434                📝 output_bg: '{}'\n\
435                🎯 input_cursor_prefix: '{}'\n\
436                🎨 input_cursor_color: '{}' ⬅️ DAS IST WICHTIG!\n\
437                🎯 input_cursor: '{}'\n\
438                🎯 output_cursor: '{}'\n\
439                🎨 output_cursor_color: '{}'",
440                theme_name.to_uppercase(),
441                theme_def.input_text,
442                theme_def.input_bg,
443                theme_def.output_text,
444                theme_def.output_bg,
445                theme_def.input_cursor_prefix,
446                theme_def.input_cursor_color, // ⬅️ Das sollte "LightBlue" sein!
447                theme_def.input_cursor,
448                theme_def.output_cursor,
449                theme_def.output_cursor_color
450            )
451        } else {
452            format!("❌ Theme '{}' nicht gefunden!", theme_name)
453        }
454    }
455}
456// ## END ##