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 cursor: String,
14 pub output_text: String,
15 pub output_bg: String,
16
17 pub input_cursor_prefix: String, pub input_cursor_color: String, pub input_cursor: String, pub output_cursor: String, pub output_cursor_color: String, }
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(¤t_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(¤t_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(¤t_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 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 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