dampen_cli/commands/add/
view_switching.rs1use std::fs;
9use std::io;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, thiserror::Error)]
14pub enum ViewSwitchError {
15 #[error("Failed to read main.rs: {source}")]
16 MainFileRead {
17 path: PathBuf,
18 #[source]
19 source: io::Error,
20 },
21
22 #[error("Failed to write main.rs: {source}")]
23 MainFileWrite {
24 path: PathBuf,
25 #[source]
26 source: io::Error,
27 },
28
29 #[error("Failed to read directory {path}: {source}")]
30 DirectoryRead {
31 path: PathBuf,
32 #[source]
33 source: io::Error,
34 },
35
36 #[error("Could not find Message enum in main.rs")]
37 MessageEnumNotFound,
38
39 #[error("Could not find #[dampen_app] macro in main.rs")]
40 DampenAppMacroNotFound,
41}
42
43pub fn detect_second_window(project_root: &Path) -> Result<bool, ViewSwitchError> {
53 let ui_dir = project_root.join("src/ui");
54
55 if !ui_dir.exists() {
56 return Ok(false);
57 }
58
59 let entries = fs::read_dir(&ui_dir).map_err(|source| ViewSwitchError::DirectoryRead {
60 path: ui_dir.clone(),
61 source,
62 })?;
63
64 let mut rs_file_count = 0;
65 for entry in entries {
66 let entry = entry.map_err(|source| ViewSwitchError::DirectoryRead {
67 path: ui_dir.clone(),
68 source,
69 })?;
70
71 let path = entry.path();
72 if path.is_file() && path.extension().is_some_and(|ext| ext == "rs") {
73 if path.file_name().is_some_and(|name| name != "mod.rs") {
75 rs_file_count += 1;
76
77 if rs_file_count >= 2 {
79 return Ok(true);
80 }
81 }
82 }
83 }
84
85 Ok(false)
86}
87
88pub fn activate_view_switching(project_root: &Path) -> Result<(), ViewSwitchError> {
102 let main_path = project_root.join("src/main.rs");
103
104 let content =
105 fs::read_to_string(&main_path).map_err(|source| ViewSwitchError::MainFileRead {
106 path: main_path.clone(),
107 source,
108 })?;
109
110 let content = uncomment_or_add_switch_message(&content)?;
112
113 let content = add_switch_variant_to_macro(&content)?;
115
116 fs::write(&main_path, content).map_err(|source| ViewSwitchError::MainFileWrite {
118 path: main_path.clone(),
119 source,
120 })?;
121
122 Ok(())
123}
124
125fn uncomment_or_add_switch_message(content: &str) -> Result<String, ViewSwitchError> {
131 if content.contains("SwitchToView(CurrentView)")
133 && !content.contains("// SwitchToView(CurrentView)")
134 {
135 return Ok(content.to_string());
137 }
138
139 if content.contains("// SwitchToView(CurrentView)") {
141 let result = content.replace(
143 "// SwitchToView(CurrentView),",
144 "SwitchToView(CurrentView),",
145 );
146 return Ok(result);
147 }
148
149 let enum_pattern = "enum Message {";
152 if let Some(enum_pos) = content.find(enum_pattern) {
153 let insert_pos = enum_pos + enum_pattern.len();
154 let mut result = String::with_capacity(content.len() + 100);
155 result.push_str(&content[..insert_pos]);
156 result.push_str("\n /// View switching\n SwitchToView(CurrentView),");
157 result.push_str(&content[insert_pos..]);
158 return Ok(result);
159 }
160
161 Err(ViewSwitchError::MessageEnumNotFound)
162}
163
164fn add_switch_variant_to_macro(content: &str) -> Result<String, ViewSwitchError> {
170 if content.contains(r#"switch_view_variant = "SwitchToView""#)
172 && !content.contains(r#"// switch_view_variant = "SwitchToView""#)
173 {
174 return Ok(content.to_string());
176 }
177
178 if content.contains(r#"// switch_view_variant = "SwitchToView""#) {
180 let result = content
182 .replace(
183 r#"// switch_view_variant = "SwitchToView","#,
184 r#"switch_view_variant = "SwitchToView","#,
185 )
186 .replace(
187 r#"// switch_view_variant = "SwitchToView""#,
188 r#"switch_view_variant = "SwitchToView","#,
189 );
190 return Ok(result);
191 }
192
193 if let Some(macro_start) = content.find("#[dampen_app(")
196 && let Some(macro_end) = content[macro_start..].find(")]")
197 {
198 let absolute_macro_end = macro_start + macro_end;
199
200 let before_close = &content[..absolute_macro_end];
202
203 let lines: Vec<&str> = before_close.lines().collect();
205 let last_non_empty_idx = lines.iter().rposition(|line| !line.trim().is_empty());
206
207 if let Some(idx) = last_non_empty_idx {
208 let mut result = String::with_capacity(content.len() + 100);
210
211 for (i, line) in lines.iter().enumerate() {
213 result.push_str(line);
214 result.push('\n');
215
216 if i == idx {
217 if !line.trim_end().ends_with(',') {
220 result.pop(); result.push(',');
223 result.push('\n');
224 }
225
226 result.push_str(r#" switch_view_variant = "SwitchToView","#);
228 result.push('\n');
229 }
230 }
231
232 result.push_str(&content[absolute_macro_end..]);
234 return Ok(result);
235 }
236
237 let mut result = String::with_capacity(content.len() + 100);
239 result.push_str(&content[..absolute_macro_end]);
240 result.push_str(r#" switch_view_variant = "SwitchToView","#);
241 result.push('\n');
242 result.push_str(&content[absolute_macro_end..]);
243 return Ok(result);
244 }
245
246 Err(ViewSwitchError::DampenAppMacroNotFound)
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use std::fs;
253 use tempfile::TempDir;
254
255 #[test]
256 fn test_detect_second_window_no_ui_dir() {
257 let temp = TempDir::new().unwrap();
258 let result = detect_second_window(temp.path());
259 assert!(result.is_ok());
260 assert!(!result.unwrap());
261 }
262
263 #[test]
264 fn test_detect_second_window_only_one_file() {
265 let temp = TempDir::new().unwrap();
266 let ui_dir = temp.path().join("src/ui");
267 fs::create_dir_all(&ui_dir).unwrap();
268 fs::write(ui_dir.join("window.rs"), "").unwrap();
269 fs::write(ui_dir.join("mod.rs"), "").unwrap();
270
271 let result = detect_second_window(temp.path());
272 assert!(result.is_ok());
273 assert!(!result.unwrap());
274 }
275
276 #[test]
277 fn test_detect_second_window_two_files() {
278 let temp = TempDir::new().unwrap();
279 let ui_dir = temp.path().join("src/ui");
280 fs::create_dir_all(&ui_dir).unwrap();
281 fs::write(ui_dir.join("window.rs"), "").unwrap();
282 fs::write(ui_dir.join("settings.rs"), "").unwrap();
283 fs::write(ui_dir.join("mod.rs"), "").unwrap();
284
285 let result = detect_second_window(temp.path());
286 assert!(result.is_ok());
287 assert!(result.unwrap());
288 }
289
290 #[test]
291 fn test_uncomment_switch_message() {
292 let input = r#"
293enum Message {
294 // SwitchToView(CurrentView),
295 Handler(HandlerMessage),
296}
297"#;
298
299 let result = uncomment_or_add_switch_message(input).unwrap();
300 assert!(result.contains("SwitchToView(CurrentView),"));
301 assert!(!result.contains("// SwitchToView(CurrentView),"));
302 }
303
304 #[test]
305 fn test_add_switch_message_when_missing() {
306 let input = r#"
307enum Message {
308 Handler(HandlerMessage),
309}
310"#;
311
312 let result = uncomment_or_add_switch_message(input).unwrap();
313 assert!(result.contains("SwitchToView(CurrentView),"));
314 assert!(result.contains("/// View switching"));
315 }
316
317 #[test]
318 fn test_uncomment_switch_variant_in_macro() {
319 let input = r#"
320#[dampen_app(
321 ui_dir = "src/ui",
322 message_type = "Message",
323 // switch_view_variant = "SwitchToView",
324)]
325"#;
326
327 let result = add_switch_variant_to_macro(input).unwrap();
328 assert!(result.contains(r#"switch_view_variant = "SwitchToView","#));
329 assert!(!result.contains(r#"// switch_view_variant"#));
330 }
331
332 #[test]
333 fn test_add_switch_variant_when_missing() {
334 let input = r#"
335#[dampen_app(
336 ui_dir = "src/ui",
337 message_type = "Message"
338)]
339"#;
340
341 let result = add_switch_variant_to_macro(input).unwrap();
342 assert!(result.contains(r#"switch_view_variant = "SwitchToView","#));
343 assert!(result.contains(r#"message_type = "Message","#));
345 }
346
347 #[test]
348 fn test_add_switch_variant_when_missing_with_comma() {
349 let input = r#"
350#[dampen_app(
351 ui_dir = "src/ui",
352 message_type = "Message",
353)]
354"#;
355
356 let result = add_switch_variant_to_macro(input).unwrap();
357 assert!(result.contains(r#"switch_view_variant = "SwitchToView","#));
358 assert!(result.contains(r#"message_type = "Message","#));
360 }
361
362 #[test]
363 fn test_already_activated_no_changes() {
364 let input = r#"
365enum Message {
366 SwitchToView(CurrentView),
367 Handler(HandlerMessage),
368}
369
370#[dampen_app(
371 ui_dir = "src/ui",
372 switch_view_variant = "SwitchToView",
373)]
374"#;
375
376 let result1 = uncomment_or_add_switch_message(input).unwrap();
377 let result2 = add_switch_variant_to_macro(&result1).unwrap();
378
379 assert_eq!(result1, input);
381 assert_eq!(result2, result1);
382 }
383}