rush_sync_server/commands/theme/
mod.rs

1// =====================================================
2// FILE: src/commands/theme/mod.rs - VEREINFACHTES THEME-SYSTEM
3// =====================================================
4
5use crate::core::prelude::*;
6use std::collections::HashMap;
7
8pub mod command;
9pub use command::ThemeCommand;
10
11// ✅ VEREINFACHT: Alle Theme-Logik in einer Struktur
12#[derive(Debug, Clone)]
13pub struct ThemeDefinition {
14    pub input_text: String,
15    pub input_bg: String,
16    pub cursor: String,
17    pub output_text: String,
18    pub output_bg: String,
19    pub prompt_text: String,
20    pub prompt_color: String,
21}
22
23// ✅ HAUPTKLASSE: Alles was vorher auf 4 Module verteilt war
24#[derive(Debug)] // ✅ NEU
25pub struct ThemeSystem {
26    themes: HashMap<String, ThemeDefinition>,
27    current_name: String,
28    config_paths: Vec<std::path::PathBuf>,
29}
30
31impl ThemeSystem {
32    /// Lädt Theme-System aus TOML-Dateien
33    pub fn load() -> Result<Self> {
34        let config_paths = crate::setup::setup_toml::get_config_paths();
35        let themes = Self::load_themes_from_paths(&config_paths)?;
36        let current_name = Self::load_current_theme_name(&config_paths).unwrap_or_else(|| {
37            // ✅ FALLBACK: Erstes verfügbares Theme
38            themes
39                .keys()
40                .next()
41                .cloned()
42                .unwrap_or_else(|| "default".to_string())
43        });
44
45        if themes.is_empty() {
46            log::warn!("❌ Keine Themes in TOML gefunden! Füge [theme.xyz] Sektionen hinzu.");
47        } else {
48            log::info!(
49                "✅ {} Themes aus TOML geladen: {}",
50                themes.len(),
51                themes.keys().cloned().collect::<Vec<String>>().join(", ")
52            );
53        }
54
55        Ok(Self {
56            themes,
57            current_name,
58            config_paths,
59        })
60    }
61
62    /// Zeigt aktuellen Status
63    pub fn show_status(&self) -> String {
64        if self.themes.is_empty() {
65            return "❌ Keine Themes verfügbar! Füge [theme.xyz] Sektionen zur rush.toml hinzu."
66                .to_string();
67        }
68
69        let available: Vec<String> = self.themes.keys().cloned().collect(); // ✅ FIX: .cloned()
70        format!(
71            "Current theme: {} (aus TOML)\nVerfügbare Themes aus TOML: {}",
72            self.current_name.to_uppercase(),
73            available.join(", ")
74        )
75    }
76
77    /// Live Theme Change mit TOML-Persistierung
78    pub fn change_theme(&mut self, theme_name: &str) -> Result<String> {
79        let theme_name_lower = theme_name.to_lowercase();
80
81        if self.themes.is_empty() {
82            return Ok(
83                "❌ Keine Themes verfügbar! Füge [theme.xyz] Sektionen zur rush.toml hinzu."
84                    .to_string(),
85            );
86        }
87
88        if !self.themes.contains_key(&theme_name_lower) {
89            let available: Vec<String> = self.themes.keys().cloned().collect(); // ✅ FIX: .cloned()
90            return Ok(format!(
91                "❌ Theme '{}' nicht in TOML gefunden. Verfügbare TOML-Themes: {}",
92                theme_name,
93                available.join(", ")
94            ));
95        }
96
97        // ✅ UPDATE current theme
98        self.current_name = theme_name_lower.clone();
99
100        // ✅ SAVE to config (background task)
101        let theme_name_clone = theme_name_lower.clone();
102        let config_paths = self.config_paths.clone();
103        tokio::spawn(async move {
104            if let Err(e) =
105                Self::save_current_theme_to_config(&config_paths, &theme_name_clone).await
106            {
107                log::error!("Failed to save theme to config: {}", e);
108            } else {
109                log::info!(
110                    "✅ TOML-Theme '{}' saved to config",
111                    theme_name_clone.to_uppercase()
112                );
113            }
114        });
115
116        // ✅ LIVE UPDATE MESSAGE
117        Ok(format!(
118            "__LIVE_THEME_UPDATE__{}__MESSAGE__🎨 TOML-Theme changed to: {} ✨",
119            theme_name_lower,
120            theme_name_lower.to_uppercase()
121        ))
122    }
123
124    /// Theme Preview
125    pub fn preview_theme(&self, theme_name: &str) -> Result<String> {
126        let theme_name_lower = theme_name.to_lowercase();
127
128        if self.themes.is_empty() {
129            return Ok(
130                "❌ Keine Themes verfügbar! Füge [theme.xyz] Sektionen zur rush.toml hinzu."
131                    .to_string(),
132            );
133        }
134
135        if let Some(theme_def) = self.themes.get(&theme_name_lower) {
136            Ok(format!(
137            "🎨 TOML-Theme '{}' Preview:\n  Input: {} auf {}\n  Output: {} auf {}\n  Cursor: {}\n  Prompt: '{}' in {}\n\n📁 Quelle: [theme.{}] in rush.toml",
138            theme_name_lower.to_uppercase(),
139            theme_def.input_text,
140            theme_def.input_bg,
141            theme_def.output_text,
142            theme_def.output_bg,
143            theme_def.cursor,
144            theme_def.prompt_text,
145            theme_def.prompt_color,
146            theme_name_lower
147        ))
148        } else {
149            let available: Vec<String> = self.themes.keys().cloned().collect(); // ✅ FIX: .cloned()
150            Ok(format!(
151                "❌ TOML-Theme '{}' nicht gefunden. Verfügbare: {}",
152                theme_name,
153                available.join(", ")
154            ))
155        }
156    }
157
158    /// Prüft ob Theme existiert
159    pub fn theme_exists(&self, theme_name: &str) -> bool {
160        self.themes.contains_key(&theme_name.to_lowercase())
161    }
162
163    /// Gibt Theme-Definition zurück
164    pub fn get_theme(&self, theme_name: &str) -> Option<&ThemeDefinition> {
165        self.themes.get(&theme_name.to_lowercase())
166    }
167
168    /// Verfügbare Theme-Namen
169    pub fn get_available_names(&self) -> Vec<String> {
170        let mut names: Vec<String> = self.themes.keys().cloned().collect();
171        names.sort();
172        names
173    }
174
175    /// Aktueller Theme-Name
176    pub fn get_current_name(&self) -> &str {
177        &self.current_name
178    }
179
180    // ✅ PRIVATE HELPERS
181
182    /// Lädt alle Themes aus TOML-Dateien
183    fn load_themes_from_paths(
184        config_paths: &[std::path::PathBuf],
185    ) -> Result<HashMap<String, ThemeDefinition>> {
186        for path in config_paths {
187            if path.exists() {
188                if let Ok(content) = std::fs::read_to_string(path) {
189                    if let Ok(themes) = Self::parse_themes_from_toml(&content) {
190                        log::debug!(
191                            "✅ {} TOML-Themes geladen aus: {}",
192                            themes.len(),
193                            themes.keys().cloned().collect::<Vec<String>>().join(", ") // ✅ FIX: .cloned() hinzugefügt
194                        );
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    /// Parst [theme.xyz] Sektionen aus TOML
206    fn parse_themes_from_toml(content: &str) -> Result<HashMap<String, ThemeDefinition>> {
207        let mut themes = HashMap::new();
208        let mut current_theme_name: Option<String> = None;
209        let mut current_theme_data: HashMap<String, String> = HashMap::new();
210
211        for line in content.lines() {
212            let trimmed = line.trim();
213
214            if trimmed.is_empty() || trimmed.starts_with('#') {
215                continue;
216            }
217
218            // Theme Section: [theme.dark]
219            if trimmed.starts_with("[theme.") && trimmed.ends_with(']') {
220                // Speichere vorheriges Theme
221                if let Some(theme_name) = current_theme_name.take() {
222                    if let Some(theme_def) = Self::build_theme_from_data(&current_theme_data) {
223                        themes.insert(theme_name, theme_def);
224                    }
225                    current_theme_data.clear();
226                }
227
228                // Extrahiere neuen Theme-Namen
229                if let Some(name) = trimmed
230                    .strip_prefix("[theme.")
231                    .and_then(|s| s.strip_suffix(']'))
232                {
233                    current_theme_name = Some(name.to_lowercase());
234                }
235            }
236            // Andere Section
237            else if trimmed.starts_with('[')
238                && trimmed.ends_with(']')
239                && !trimmed.starts_with("[theme.")
240            {
241                if let Some(theme_name) = current_theme_name.take() {
242                    if let Some(theme_def) = Self::build_theme_from_data(&current_theme_data) {
243                        themes.insert(theme_name, theme_def);
244                    }
245                    current_theme_data.clear();
246                }
247            }
248            // Theme Property
249            else if current_theme_name.is_some() && trimmed.contains('=') {
250                if let Some((key, value)) = trimmed.split_once('=') {
251                    let clean_key = key.trim().to_string();
252                    let clean_value = value
253                        .trim()
254                        .trim_matches('"')
255                        .trim_matches('\'')
256                        .to_string();
257                    if !clean_value.is_empty() {
258                        current_theme_data.insert(clean_key, clean_value);
259                    }
260                }
261            }
262        }
263
264        // Letztes Theme speichern
265        if let Some(theme_name) = current_theme_name {
266            if let Some(theme_def) = Self::build_theme_from_data(&current_theme_data) {
267                themes.insert(theme_name, theme_def);
268            }
269        }
270
271        Ok(themes)
272    }
273
274    /// Baut ThemeDefinition aus geparsten Daten
275    fn build_theme_from_data(data: &HashMap<String, String>) -> Option<ThemeDefinition> {
276        Some(ThemeDefinition {
277            input_text: data.get("input_text")?.clone(),
278            input_bg: data.get("input_bg")?.clone(),
279            cursor: data.get("cursor")?.clone(),
280            output_text: data.get("output_text")?.clone(),
281            output_bg: data.get("output_bg")?.clone(),
282            prompt_text: data
283                .get("prompt_text") // ✅ NEU
284                .unwrap_or(&"/// ".to_string())
285                .clone(),
286            prompt_color: data
287                .get("prompt_color") // ✅ NEU
288                .unwrap_or(&"LightBlue".to_string())
289                .clone(),
290        })
291    }
292
293    /// Lädt current_theme aus TOML
294    fn load_current_theme_name(config_paths: &[std::path::PathBuf]) -> Option<String> {
295        for path in config_paths {
296            if path.exists() {
297                if let Ok(content) = std::fs::read_to_string(path) {
298                    if let Some(theme) = Self::extract_current_theme_from_toml(&content) {
299                        return Some(theme);
300                    }
301                }
302            }
303        }
304        None
305    }
306
307    /// Extrahiert current_theme aus TOML
308    fn extract_current_theme_from_toml(content: &str) -> Option<String> {
309        let mut in_general_section = false;
310
311        for line in content.lines() {
312            let trimmed = line.trim();
313            if trimmed.is_empty() || trimmed.starts_with('#') {
314                continue;
315            }
316
317            if trimmed == "[general]" {
318                in_general_section = true;
319            } else if trimmed.starts_with('[') && trimmed != "[general]" {
320                in_general_section = false;
321            } else if in_general_section && trimmed.starts_with("current_theme") {
322                if let Some(value_part) = trimmed.split('=').nth(1) {
323                    let cleaned = value_part.trim().trim_matches('"').trim_matches('\'');
324                    if !cleaned.is_empty() {
325                        return Some(cleaned.to_string());
326                    }
327                }
328            }
329        }
330        None
331    }
332
333    /// Speichert current_theme in TOML-Config
334    async fn save_current_theme_to_config(
335        config_paths: &[std::path::PathBuf],
336        theme_name: &str,
337    ) -> Result<()> {
338        for path in config_paths {
339            if path.exists() {
340                let content = tokio::fs::read_to_string(path)
341                    .await
342                    .map_err(AppError::Io)?;
343                let updated_content = Self::update_current_theme_in_toml(&content, theme_name)?;
344                tokio::fs::write(path, updated_content)
345                    .await
346                    .map_err(AppError::Io)?;
347                return Ok(());
348            }
349        }
350        Err(AppError::Validation("No config file found".to_string()))
351    }
352
353    /// Updated current_theme in TOML-Inhalt
354    fn update_current_theme_in_toml(content: &str, theme_name: &str) -> Result<String> {
355        let mut lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
356        let mut in_general_section = false;
357        let mut theme_updated = false;
358
359        for line in lines.iter_mut() {
360            let trimmed = line.trim();
361
362            if trimmed == "[general]" {
363                in_general_section = true;
364            } else if trimmed.starts_with('[') && trimmed != "[general]" {
365                in_general_section = false;
366            } else if in_general_section && trimmed.starts_with("current_theme") {
367                *line = format!("current_theme = \"{}\"", theme_name);
368                theme_updated = true;
369            }
370        }
371
372        if !theme_updated {
373            // ✅ CLIPPY FIX: Iterator statt needless_range_loop
374            for (i, line) in lines.iter().enumerate() {
375                if line.trim() == "[general]" {
376                    // Finde Insert-Position
377                    let insert_index = lines
378                        .iter()
379                        .enumerate()
380                        .skip(i + 1) // ✅ Skip zur nächsten Position nach [general]
381                        .find(|(_, line)| line.trim().starts_with('['))
382                        .map(|(idx, _)| idx)
383                        .unwrap_or(lines.len()); // ✅ Fallback: Am Ende einfügen
384
385                    lines.insert(insert_index, format!("current_theme = \"{}\"", theme_name));
386                    break;
387                }
388            }
389        }
390
391        Ok(lines.join("\n"))
392    }
393}