Skip to main content

rush_sync_server/commands/theme/
mod.rs

1use crate::core::prelude::*;
2use std::collections::HashMap;
3
4pub mod command;
5pub use command::ThemeCommand;
6
7#[derive(Debug, Clone)]
8pub struct ThemeDefinition {
9    pub input_text: String,
10    pub input_bg: String,
11    pub output_text: String,
12    pub output_bg: String,
13    pub input_cursor_prefix: String,
14    pub input_cursor_color: String,
15    pub input_cursor: String,
16    pub output_cursor: String,
17    pub output_cursor_color: String,
18}
19
20#[derive(Debug)]
21pub struct ThemeSystem {
22    themes: HashMap<String, ThemeDefinition>,
23    current_name: String,
24    config_paths: Vec<std::path::PathBuf>,
25}
26
27impl ThemeSystem {
28    pub fn load() -> Result<Self> {
29        let config_paths = crate::setup::setup_toml::get_config_paths();
30        let themes = Self::load_themes_from_paths(&config_paths)?;
31        let current_name = Self::load_current_theme_name(&config_paths).unwrap_or_else(|| {
32            themes
33                .keys()
34                .next()
35                .cloned()
36                .unwrap_or_else(|| "default".to_string())
37        });
38
39        log::info!(
40            "{} themes loaded: {}",
41            themes.len(),
42            themes.keys().cloned().collect::<Vec<_>>().join(", ")
43        );
44
45        Ok(Self {
46            themes,
47            current_name,
48            config_paths,
49        })
50    }
51
52    pub fn show_status(&self) -> String {
53        if self.themes.is_empty() {
54            return "No themes available! Add [theme.xyz] sections to rush.toml.".to_string();
55        }
56        format!(
57            "Current theme: {} (from TOML)\nAvailable: {}",
58            self.current_name.to_uppercase(),
59            self.themes.keys().cloned().collect::<Vec<_>>().join(", ")
60        )
61    }
62
63    pub fn change_theme(&mut self, theme_name: &str) -> Result<String> {
64        let theme_name_lower = theme_name.to_lowercase();
65
66        if !self.themes.contains_key(&theme_name_lower) {
67            return Ok(if self.themes.is_empty() {
68                "No themes available! Add [theme.xyz] sections to rush.toml.".to_string()
69            } else {
70                format!(
71                    "Theme '{}' not found. Available: {}",
72                    theme_name,
73                    self.themes.keys().cloned().collect::<Vec<_>>().join(", ")
74                )
75            });
76        }
77
78        self.current_name = theme_name_lower.clone();
79
80        // Log cursor details
81        if let Some(theme_def) = self.themes.get(&theme_name_lower) {
82            log::info!(
83                "Theme '{}': input_cursor='{}' ({}), output_cursor='{}' ({}), prefix='{}'",
84                theme_name_lower.to_uppercase(),
85                theme_def.input_cursor,
86                theme_def.input_cursor_color,
87                theme_def.output_cursor,
88                theme_def.output_cursor_color,
89                theme_def.input_cursor_prefix
90            );
91        }
92
93        // Async save
94        let name_clone = theme_name_lower.clone();
95        let paths_clone = self.config_paths.clone();
96        tokio::spawn(async move {
97            if let Err(e) = Self::save_current_theme_to_config(&paths_clone, &name_clone).await {
98                log::error!("Failed to save theme: {}", e);
99            }
100        });
101
102        Ok(format!(
103            "__LIVE_THEME_UPDATE__{}__MESSAGE__Theme changed to: {}",
104            theme_name_lower,
105            theme_name_lower.to_uppercase()
106        ))
107    }
108
109    pub fn preview_theme(&self, theme_name: &str) -> Result<String> {
110        let theme_name_lower = theme_name.to_lowercase();
111
112        if let Some(theme_def) = self.themes.get(&theme_name_lower) {
113            Ok(format!("Theme '{}' Preview:\nInput: {} on {}\nOutput: {} on {}\nCursor Prefix: '{}' in {}\nInput Cursor: {}\nOutput Cursor: {} in {}\n\nSource: [theme.{}] in rush.toml",
114                theme_name_lower.to_uppercase(), theme_def.input_text, theme_def.input_bg,
115                theme_def.output_text, theme_def.output_bg, theme_def.input_cursor_prefix,
116                theme_def.input_cursor_color, theme_def.input_cursor, theme_def.output_cursor,
117                theme_def.output_cursor_color, theme_name_lower))
118        } else {
119            Ok(format!(
120                "Theme '{}' not found. Available: {}",
121                theme_name,
122                self.themes.keys().cloned().collect::<Vec<_>>().join(", ")
123            ))
124        }
125    }
126
127    pub fn debug_theme_details(&self, theme_name: &str) -> String {
128        if let Some(theme_def) = self.themes.get(&theme_name.to_lowercase()) {
129            format!("Theme '{}':\ninput_text: '{}'\ninput_bg: '{}'\noutput_text: '{}'\noutput_bg: '{}'\ninput_cursor_prefix: '{}'\ninput_cursor_color: '{}'\ninput_cursor: '{}'\noutput_cursor: '{}'\noutput_cursor_color: '{}'",
130                theme_name.to_uppercase(), theme_def.input_text, theme_def.input_bg,
131                theme_def.output_text, theme_def.output_bg, theme_def.input_cursor_prefix,
132                theme_def.input_cursor_color, theme_def.input_cursor, theme_def.output_cursor,
133                theme_def.output_cursor_color)
134        } else {
135            format!("Theme '{}' not found!", theme_name)
136        }
137    }
138
139    pub fn theme_exists(&self, theme_name: &str) -> bool {
140        self.themes.contains_key(&theme_name.to_lowercase())
141    }
142    pub fn get_theme(&self, theme_name: &str) -> Option<&ThemeDefinition> {
143        self.themes.get(&theme_name.to_lowercase())
144    }
145    pub fn get_available_names(&self) -> Vec<String> {
146        let mut names: Vec<_> = self.themes.keys().cloned().collect();
147        names.sort();
148        names
149    }
150    pub fn get_current_name(&self) -> &str {
151        &self.current_name
152    }
153
154    fn load_themes_from_paths(
155        config_paths: &[std::path::PathBuf],
156    ) -> Result<HashMap<String, ThemeDefinition>> {
157        for path in config_paths {
158            if path.exists() {
159                if let Ok(content) = std::fs::read_to_string(path) {
160                    if let Ok(themes) = Self::parse_themes_from_toml(&content) {
161                        return Ok(themes);
162                    }
163                }
164            }
165        }
166        Ok(HashMap::new())
167    }
168
169    fn parse_themes_from_toml(content: &str) -> Result<HashMap<String, ThemeDefinition>> {
170        let mut themes = HashMap::new();
171        let mut current_theme: Option<String> = None;
172        let mut current_data = HashMap::new();
173
174        for line in content.lines() {
175            let trimmed = line.trim();
176            if trimmed.is_empty() || trimmed.starts_with('#') {
177                continue;
178            }
179
180            if let Some(theme_name) = trimmed
181                .strip_prefix("[theme.")
182                .and_then(|s| s.strip_suffix(']'))
183            {
184                Self::finalize_theme(&mut themes, current_theme.take(), &mut current_data);
185                current_theme = Some(theme_name.to_lowercase());
186            } else if trimmed.starts_with('[') && !trimmed.starts_with("[theme.") {
187                Self::finalize_theme(&mut themes, current_theme.take(), &mut current_data);
188            } else if current_theme.is_some() && trimmed.contains('=') {
189                if let Some((key, value)) = trimmed.split_once('=') {
190                    let clean_value = value.trim().trim_matches('"').trim_matches('\'');
191                    if !clean_value.is_empty() {
192                        current_data.insert(key.trim().to_string(), clean_value.to_string());
193                    }
194                }
195            }
196        }
197        Self::finalize_theme(&mut themes, current_theme, &mut current_data);
198        Ok(themes)
199    }
200
201    fn finalize_theme(
202        themes: &mut HashMap<String, ThemeDefinition>,
203        theme_name: Option<String>,
204        data: &mut HashMap<String, String>,
205    ) {
206        if let Some(name) = theme_name {
207            if let Some(theme_def) = Self::build_theme_from_data(data) {
208                themes.insert(name, theme_def);
209            }
210            data.clear();
211        }
212    }
213
214    fn build_theme_from_data(data: &HashMap<String, String>) -> Option<ThemeDefinition> {
215        Some(ThemeDefinition {
216            input_text: data.get("input_text")?.clone(),
217            input_bg: data.get("input_bg")?.clone(),
218            output_text: data.get("output_text")?.clone(),
219            output_bg: data.get("output_bg")?.clone(),
220            input_cursor_prefix: data
221                .get("input_cursor_prefix")
222                .or(data.get("prompt_text"))
223                .unwrap_or(&"/// ".to_string())
224                .clone(),
225            input_cursor_color: data
226                .get("input_cursor_color")
227                .or(data.get("prompt_color"))
228                .unwrap_or(&"LightBlue".to_string())
229                .clone(),
230            input_cursor: data
231                .get("input_cursor")
232                .or(data.get("prompt_cursor"))
233                .unwrap_or(&"PIPE".to_string())
234                .clone(),
235            output_cursor: data
236                .get("output_cursor")
237                .unwrap_or(&"PIPE".to_string())
238                .clone(),
239            output_cursor_color: data
240                .get("output_cursor_color")
241                .or(data.get("output_color"))
242                .unwrap_or(&"White".to_string())
243                .clone(),
244        })
245    }
246
247    fn load_current_theme_name(config_paths: &[std::path::PathBuf]) -> Option<String> {
248        for path in config_paths {
249            if path.exists() {
250                if let Ok(content) = std::fs::read_to_string(path) {
251                    if let Some(theme) = Self::extract_current_theme_from_toml(&content) {
252                        return Some(theme);
253                    }
254                }
255            }
256        }
257        None
258    }
259
260    fn extract_current_theme_from_toml(content: &str) -> Option<String> {
261        let mut in_general = false;
262        for line in content.lines() {
263            let trimmed = line.trim();
264            if trimmed == "[general]" {
265                in_general = true;
266            } else if trimmed.starts_with('[') {
267                in_general = false;
268            } else if in_general && trimmed.starts_with("current_theme") {
269                return trimmed
270                    .split('=')
271                    .nth(1)?
272                    .trim()
273                    .trim_matches('"')
274                    .trim_matches('\'')
275                    .to_string()
276                    .into();
277            }
278        }
279        None
280    }
281
282    async fn save_current_theme_to_config(
283        config_paths: &[std::path::PathBuf],
284        theme_name: &str,
285    ) -> Result<()> {
286        for path in config_paths {
287            if path.exists() {
288                let content = tokio::fs::read_to_string(path)
289                    .await
290                    .map_err(AppError::Io)?;
291                let updated = Self::update_current_theme_in_toml(&content, theme_name)?;
292                tokio::fs::write(path, updated)
293                    .await
294                    .map_err(AppError::Io)?;
295                return Ok(());
296            }
297        }
298        Err(AppError::Validation("No config file found".to_string()))
299    }
300
301    fn update_current_theme_in_toml(content: &str, theme_name: &str) -> Result<String> {
302        let mut lines: Vec<String> = content.lines().map(|l| l.to_string()).collect();
303        let mut in_general = false;
304        let mut updated = false;
305
306        for line in lines.iter_mut() {
307            let trimmed = line.trim();
308            if trimmed == "[general]" {
309                in_general = true;
310            } else if trimmed.starts_with('[') {
311                in_general = false;
312            } else if in_general && trimmed.starts_with("current_theme") {
313                *line = format!("current_theme = \"{}\"", theme_name);
314                updated = true;
315            }
316        }
317
318        if !updated {
319            if let Some(general_idx) = lines.iter().position(|line| line.trim() == "[general]") {
320                let insert_idx = lines
321                    .iter()
322                    .enumerate()
323                    .skip(general_idx + 1)
324                    .find(|(_, line)| line.trim().starts_with('['))
325                    .map(|(idx, _)| idx)
326                    .unwrap_or(lines.len());
327                lines.insert(insert_idx, format!("current_theme = \"{}\"", theme_name));
328            }
329        }
330        Ok(lines.join("\n"))
331    }
332}