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