rush_sync_server/commands/theme/
mod.rs1use crate::core::prelude::*;
6use std::collections::HashMap;
7
8pub mod command;
9pub use command::ThemeCommand;
10
11#[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#[derive(Debug)] pub 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> {
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
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 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(); format!(
71 "Current theme: {} (aus TOML)\nVerfügbare Themes aus TOML: {}",
72 self.current_name.to_uppercase(),
73 available.join(", ")
74 )
75 }
76
77 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(); return Ok(format!(
91 "❌ Theme '{}' nicht in TOML gefunden. Verfügbare TOML-Themes: {}",
92 theme_name,
93 available.join(", ")
94 ));
95 }
96
97 self.current_name = theme_name_lower.clone();
99
100 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 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 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(); Ok(format!(
151 "❌ TOML-Theme '{}' nicht gefunden. Verfügbare: {}",
152 theme_name,
153 available.join(", ")
154 ))
155 }
156 }
157
158 pub fn theme_exists(&self, theme_name: &str) -> bool {
160 self.themes.contains_key(&theme_name.to_lowercase())
161 }
162
163 pub fn get_theme(&self, theme_name: &str) -> Option<&ThemeDefinition> {
165 self.themes.get(&theme_name.to_lowercase())
166 }
167
168 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 pub fn get_current_name(&self) -> &str {
177 &self.current_name
178 }
179
180 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(", ") );
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>> {
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 if trimmed.starts_with("[theme.") && trimmed.ends_with(']') {
220 if let Some(theme_name) = current_theme_name.take() {
222 if let Some(theme_def) = Self::build_theme_from_data(¤t_theme_data) {
223 themes.insert(theme_name, theme_def);
224 }
225 current_theme_data.clear();
226 }
227
228 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 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(¤t_theme_data) {
243 themes.insert(theme_name, theme_def);
244 }
245 current_theme_data.clear();
246 }
247 }
248 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 if let Some(theme_name) = current_theme_name {
266 if let Some(theme_def) = Self::build_theme_from_data(¤t_theme_data) {
267 themes.insert(theme_name, theme_def);
268 }
269 }
270
271 Ok(themes)
272 }
273
274 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") .unwrap_or(&"/// ".to_string())
285 .clone(),
286 prompt_color: data
287 .get("prompt_color") .unwrap_or(&"LightBlue".to_string())
289 .clone(),
290 })
291 }
292
293 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 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 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 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 for (i, line) in lines.iter().enumerate() {
375 if line.trim() == "[general]" {
376 let insert_index = lines
378 .iter()
379 .enumerate()
380 .skip(i + 1) .find(|(_, line)| line.trim().starts_with('['))
382 .map(|(idx, _)| idx)
383 .unwrap_or(lines.len()); lines.insert(insert_index, format!("current_theme = \"{}\"", theme_name));
386 break;
387 }
388 }
389 }
390
391 Ok(lines.join("\n"))
392 }
393}