rush_sync_server/commands/theme/
mod.rs1use 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 pub input_cursor_prefix: String, pub input_cursor_color: String, pub input_cursor: String, pub output_cursor: String, pub output_cursor_color: String, }
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 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 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(¤t_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(¤t_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(¤t_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 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()) .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()) .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, 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