1use crate::commands::add::errors::GenerationError;
4use crate::commands::add::integration::add_module_to_mod_rs;
5use crate::commands::add::templates::{TemplateKind, WindowTemplate};
6use crate::commands::add::validation::{TargetPath, WindowName};
7use crate::commands::add::view_switching::{activate_view_switching, detect_second_window};
8use std::fs;
9use std::path::PathBuf;
10
11#[derive(Debug, Clone)]
13pub struct GeneratedFiles {
14 pub rust_file: PathBuf,
16
17 pub dampen_file: PathBuf,
19
20 pub window_name: WindowName,
22
23 pub target_dir: PathBuf,
25
26 pub updated_mod_file: Option<PathBuf>,
28
29 pub view_switching_activated: bool,
31}
32
33impl GeneratedFiles {
34 pub fn success_message(&self) -> String {
36 let mut message = format!(
37 "✓ Created UI window '{}'\n → {}\n → {}",
38 self.window_name.snake,
39 self.rust_file.display(),
40 self.dampen_file.display()
41 );
42
43 if let Some(mod_file) = &self.updated_mod_file {
45 message.push_str(&format!("\n → Updated {}", mod_file.display()));
46 }
47
48 if self.view_switching_activated {
50 message.push_str("\n → Activated multi-view in src/main.rs");
51 }
52
53 message.push_str("\n\nNext steps:");
54
55 if self.updated_mod_file.is_none() {
57 message.push_str(&format!(
58 "\n 1. Add `pub mod {};` to src/ui/mod.rs",
59 self.window_name.snake
60 ));
61 message.push_str("\n 2. Run `dampen check` to validate");
62 message.push_str("\n 3. Run your application to see the new window");
63 } else {
64 message.push_str("\n 1. Run `dampen check` to validate");
65 message.push_str("\n 2. Run your application to see the new window");
66 }
67
68 message
69 }
70}
71
72pub fn generate_window_files(
95 target_path: &TargetPath,
96 window_name: &WindowName,
97 enable_integration: bool,
98) -> Result<GeneratedFiles, GenerationError> {
99 let rust_file = target_path.file_path(&window_name.snake, "rs");
101 let dampen_file = target_path.file_path(&window_name.snake, "dampen");
102
103 if rust_file.exists() {
104 return Err(GenerationError::FileExists {
105 window_name: window_name.snake.clone(),
106 path: rust_file,
107 });
108 }
109
110 if dampen_file.exists() {
111 return Err(GenerationError::FileExists {
112 window_name: window_name.snake.clone(),
113 path: dampen_file,
114 });
115 }
116
117 fs::create_dir_all(&target_path.absolute).map_err(|e| GenerationError::DirectoryCreation {
119 path: target_path.absolute.clone(),
120 source: e,
121 })?;
122
123 let rust_template = WindowTemplate::load(TemplateKind::RustModule);
125 let dampen_template = WindowTemplate::load(TemplateKind::DampenXml);
126
127 let variants = window_name.to_variants();
128 let rust_content = rust_template.render(&variants);
129 let dampen_content = dampen_template.render(&variants);
130
131 fs::write(&rust_file, rust_content).map_err(|e| GenerationError::FileWrite {
133 path: rust_file.clone(),
134 source: e,
135 })?;
136
137 if let Err(e) = fs::write(&dampen_file, dampen_content) {
139 let _ = fs::remove_file(&rust_file);
141 return Err(GenerationError::FileWrite {
142 path: dampen_file,
143 source: e,
144 });
145 }
146
147 let updated_mod_file = if enable_integration {
149 match add_module_to_mod_rs(
150 &target_path.project_root,
151 &target_path.absolute,
152 &window_name.snake,
153 ) {
154 Ok(mod_path) => {
155 eprintln!("✓ Updated {}", mod_path.display());
156 Some(mod_path)
157 }
158 Err(e) => {
159 eprintln!("⚠ Warning: Failed to update mod.rs: {}", e);
160 eprintln!(
161 " Please manually add `pub mod {};` to the appropriate mod.rs file",
162 window_name.snake
163 );
164 None
165 }
166 }
167 } else {
168 None
169 };
170
171 let view_switching_activated = if enable_integration {
173 match detect_second_window(&target_path.project_root) {
174 Ok(true) => {
175 match activate_view_switching(&target_path.project_root) {
177 Ok(()) => {
178 eprintln!("✓ Activated multi-view in src/main.rs");
179 true
180 }
181 Err(e) => {
182 eprintln!("⚠ Warning: Failed to activate view switching: {}", e);
183 eprintln!(
184 " You may need to manually enable multi-view support in src/main.rs"
185 );
186 false
187 }
188 }
189 }
190 Ok(false) => false,
191 Err(e) => {
192 eprintln!("⚠ Warning: Could not detect second window: {}", e);
193 false
194 }
195 }
196 } else {
197 false
198 };
199
200 Ok(GeneratedFiles {
201 rust_file,
202 dampen_file,
203 window_name: window_name.clone(),
204 target_dir: target_path.absolute.clone(),
205 updated_mod_file,
206 view_switching_activated,
207 })
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::commands::add::validation::{TargetPath, WindowName};
214 use std::fs;
215 use tempfile::TempDir;
216
217 #[test]
218 fn test_generate_files_default_path() {
219 let temp = TempDir::new().unwrap();
220 let project_root = temp.path();
221 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
222 let window_name = WindowName::new("settings").unwrap();
223
224 let result = generate_window_files(&target_path, &window_name, false);
225
226 assert!(result.is_ok());
227 let generated = result.unwrap();
228 assert_eq!(generated.rust_file, project_root.join("src/ui/settings.rs"));
229 assert_eq!(
230 generated.dampen_file,
231 project_root.join("src/ui/settings.dampen")
232 );
233 assert!(generated.rust_file.exists());
234 assert!(generated.dampen_file.exists());
235 }
236
237 #[test]
238 fn test_generate_files_creates_directory() {
239 let temp = TempDir::new().unwrap();
240 let project_root = temp.path();
241 let target_path = TargetPath::resolve(project_root, Some("src/ui/admin")).unwrap();
242 let window_name = WindowName::new("dashboard").unwrap();
243
244 assert!(!target_path.absolute.exists());
246
247 let result = generate_window_files(&target_path, &window_name, false);
248
249 assert!(result.is_ok());
250 assert!(target_path.absolute.exists());
251 assert!(target_path.absolute.join("dashboard.rs").exists());
252 assert!(target_path.absolute.join("dashboard.dampen").exists());
253 }
254
255 #[test]
256 fn test_generate_files_rs_content() {
257 let temp = TempDir::new().unwrap();
258 let project_root = temp.path();
259 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
260 let window_name = WindowName::new("user_profile").unwrap();
261
262 let result = generate_window_files(&target_path, &window_name, false);
263
264 assert!(result.is_ok());
265 let content = fs::read_to_string(target_path.absolute.join("user_profile.rs")).unwrap();
266
267 assert!(
269 content.contains("user_profile"),
270 "Should contain module name"
271 );
272 assert!(
273 content.contains("pub struct Model"),
274 "Should contain Model struct"
275 );
276 assert!(
277 content.contains("#[dampen_ui"),
278 "Should contain dampen_ui attribute"
279 );
280 assert!(
281 content.contains("create_app_state"),
282 "Should contain create_app_state function"
283 );
284 }
285
286 #[test]
287 fn test_generate_files_dampen_content() {
288 let temp = TempDir::new().unwrap();
289 let project_root = temp.path();
290 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
291 let window_name = WindowName::new("settings").unwrap();
292
293 let result = generate_window_files(&target_path, &window_name, false);
294
295 assert!(result.is_ok());
296 let content = fs::read_to_string(target_path.absolute.join("settings.dampen")).unwrap();
297
298 assert!(
300 !content.contains("<?xml"),
301 "Should NOT have XML declaration"
302 );
303 assert!(
304 content.contains("<dampen version=\"1.1\" encoding=\"utf-8\">"),
305 "Should have correct root element"
306 );
307 assert!(content.contains("<column"), "Should have column layout");
308 assert!(content.contains("Settings"), "Should contain window title");
309 }
310
311 #[test]
312 fn test_generate_files_rejects_existing_rs() {
313 let temp = TempDir::new().unwrap();
315 let project_root = temp.path();
316 let target_dir = project_root.join("src/ui");
317 fs::create_dir_all(&target_dir).unwrap();
318
319 let existing_file = target_dir.join("settings.rs");
321 fs::write(&existing_file, "existing content").unwrap();
322
323 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
324 let window_name = WindowName::new("settings").unwrap();
325 let result = generate_window_files(&target_path, &window_name, false);
326
327 assert!(result.is_err());
328 match result {
329 Err(GenerationError::FileExists {
330 window_name: name,
331 path,
332 }) => {
333 assert_eq!(name, "settings");
334 assert_eq!(path, existing_file);
335 }
336 _ => panic!("Expected FileExists error"),
337 }
338 }
339
340 #[test]
341 fn test_generate_files_rejects_existing_dampen() {
342 let temp = TempDir::new().unwrap();
344 let project_root = temp.path();
345 let target_dir = project_root.join("src/ui");
346 fs::create_dir_all(&target_dir).unwrap();
347
348 let existing_file = target_dir.join("dashboard.dampen");
350 fs::write(&existing_file, "existing xml content").unwrap();
351
352 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
353 let window_name = WindowName::new("dashboard").unwrap();
354 let result = generate_window_files(&target_path, &window_name, false);
355
356 assert!(result.is_err());
357 match result {
358 Err(GenerationError::FileExists {
359 window_name: name,
360 path,
361 }) => {
362 assert_eq!(name, "dashboard");
363 assert_eq!(path, existing_file);
364 }
365 _ => panic!("Expected FileExists error"),
366 }
367 }
368
369 #[test]
370 fn test_generate_files_rejects_partial_conflict() {
371 let temp = TempDir::new().unwrap();
373 let project_root = temp.path();
374 let target_dir = project_root.join("src/ui");
375 fs::create_dir_all(&target_dir).unwrap();
376
377 let existing_rs = target_dir.join("profile.rs");
379 fs::write(&existing_rs, "existing rust content").unwrap();
380
381 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
382 let window_name = WindowName::new("profile").unwrap();
383 let result = generate_window_files(&target_path, &window_name, false);
384
385 assert!(result.is_err());
387 match result {
388 Err(GenerationError::FileExists {
389 window_name: name,
390 path,
391 }) => {
392 assert_eq!(name, "profile");
393 assert_eq!(path, existing_rs);
394 }
395 _ => panic!("Expected FileExists error for partial conflict"),
396 }
397
398 fs::remove_file(&existing_rs).unwrap();
400 let existing_dampen = target_dir.join("profile.dampen");
401 fs::write(&existing_dampen, "existing xml").unwrap();
402
403 let result2 = generate_window_files(&target_path, &window_name, false);
404
405 assert!(result2.is_err());
407 match result2 {
408 Err(GenerationError::FileExists {
409 window_name: name,
410 path,
411 }) => {
412 assert_eq!(name, "profile");
413 assert_eq!(path, existing_dampen);
414 }
415 _ => panic!("Expected FileExists error for .dampen conflict"),
416 }
417 }
418
419 #[test]
420 fn test_success_message_format() {
421 let temp = TempDir::new().unwrap();
422 let project_root = temp.path();
423 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
424 let window_name = WindowName::new("dashboard").unwrap();
425
426 let generated = GeneratedFiles {
427 rust_file: target_path.absolute.join("dashboard.rs"),
428 dampen_file: target_path.absolute.join("dashboard.dampen"),
429 window_name: window_name.clone(),
430 target_dir: target_path.absolute.clone(),
431 updated_mod_file: None,
432 view_switching_activated: false,
433 };
434
435 let message = generated.success_message();
436
437 assert!(message.contains("Created UI window 'dashboard'"));
438 assert!(message.contains("dashboard.rs"));
439 assert!(message.contains("dashboard.dampen"));
440 assert!(message.contains("pub mod dashboard"));
441 assert!(message.contains("Next steps"));
442 }
443
444 #[test]
445 fn test_success_message_with_integration() {
446 let temp = TempDir::new().unwrap();
447 let project_root = temp.path();
448 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
449 let window_name = WindowName::new("settings").unwrap();
450
451 let generated = GeneratedFiles {
452 rust_file: target_path.absolute.join("settings.rs"),
453 dampen_file: target_path.absolute.join("settings.dampen"),
454 window_name: window_name.clone(),
455 target_dir: target_path.absolute.clone(),
456 updated_mod_file: Some(project_root.join("src/ui/mod.rs")),
457 view_switching_activated: false,
458 };
459
460 let message = generated.success_message();
461
462 assert!(message.contains("Created UI window 'settings'"));
463 assert!(message.contains("settings.rs"));
464 assert!(message.contains("settings.dampen"));
465 assert!(message.contains("Updated"), "Should mention mod.rs update");
466 assert!(message.contains("src/ui/mod.rs"), "Should show mod.rs path");
467 assert!(
468 !message.contains("pub mod settings"),
469 "Should NOT show manual mod instruction"
470 );
471 assert!(message.contains("Next steps"));
472 }
473
474 #[test]
475 fn test_generate_files_with_integration() {
476 let temp = TempDir::new().unwrap();
477 let project_root = temp.path();
478
479 let ui_dir = project_root.join("src/ui");
481 fs::create_dir_all(&ui_dir).unwrap();
482 fs::write(ui_dir.join("mod.rs"), "").unwrap();
483
484 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
485 let window_name = WindowName::new("settings").unwrap();
486
487 let result = generate_window_files(&target_path, &window_name, true);
488
489 assert!(result.is_ok());
490 let generated = result.unwrap();
491
492 assert!(generated.rust_file.exists());
494 assert!(generated.dampen_file.exists());
495
496 assert!(generated.updated_mod_file.is_some());
498 let mod_file = generated.updated_mod_file.unwrap();
499 assert_eq!(mod_file, ui_dir.join("mod.rs"));
500
501 let mod_content = fs::read_to_string(&mod_file).unwrap();
503 assert!(mod_content.contains("pub mod settings;"));
504 }
505
506 #[test]
507 fn test_generate_files_integration_disabled() {
508 let temp = TempDir::new().unwrap();
509 let project_root = temp.path();
510
511 let ui_dir = project_root.join("src/ui");
513 fs::create_dir_all(&ui_dir).unwrap();
514 fs::write(ui_dir.join("mod.rs"), "").unwrap();
515
516 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
517 let window_name = WindowName::new("settings").unwrap();
518
519 let result = generate_window_files(&target_path, &window_name, false);
520
521 assert!(result.is_ok());
522 let generated = result.unwrap();
523
524 assert!(generated.rust_file.exists());
526 assert!(generated.dampen_file.exists());
527
528 assert!(generated.updated_mod_file.is_none());
530
531 let mod_content = fs::read_to_string(ui_dir.join("mod.rs")).unwrap();
533 assert!(mod_content.is_empty());
534 }
535
536 #[test]
537 fn test_view_switching_activated_on_second_window() {
538 let temp = TempDir::new().unwrap();
539 let project_root = temp.path();
540
541 let ui_dir = project_root.join("src/ui");
543 fs::create_dir_all(&ui_dir).unwrap();
544 fs::write(ui_dir.join("mod.rs"), "pub mod window;\n").unwrap();
545 fs::write(ui_dir.join("window.rs"), "// First window").unwrap();
546
547 let src_dir = project_root.join("src");
549 fs::create_dir_all(&src_dir).unwrap();
550 let main_content = r#"
551enum Message {
552 // SwitchToView(CurrentView),
553 Handler(HandlerMessage),
554}
555
556#[dampen_app(
557 ui_dir = "src/ui",
558 message_type = "Message",
559 // switch_view_variant = "SwitchToView",
560)]
561struct App;
562"#;
563 fs::write(src_dir.join("main.rs"), main_content).unwrap();
564
565 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
567 let window_name = WindowName::new("settings").unwrap();
568
569 let result = generate_window_files(&target_path, &window_name, true);
570
571 assert!(result.is_ok());
572 let generated = result.unwrap();
573
574 assert!(generated.view_switching_activated);
576
577 let main_content = fs::read_to_string(src_dir.join("main.rs")).unwrap();
579 assert!(main_content.contains("SwitchToView(CurrentView),"));
580 assert!(!main_content.contains("// SwitchToView(CurrentView),"));
581 assert!(main_content.contains(r#"switch_view_variant = "SwitchToView","#));
582 assert!(!main_content.contains(r#"// switch_view_variant"#));
583 }
584
585 #[test]
586 fn test_view_switching_not_activated_on_first_window() {
587 let temp = TempDir::new().unwrap();
588 let project_root = temp.path();
589
590 let ui_dir = project_root.join("src/ui");
592 fs::create_dir_all(&ui_dir).unwrap();
593 fs::write(ui_dir.join("mod.rs"), "").unwrap();
594
595 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
597 let window_name = WindowName::new("window").unwrap();
598
599 let result = generate_window_files(&target_path, &window_name, true);
600
601 assert!(result.is_ok());
602 let generated = result.unwrap();
603
604 assert!(!generated.view_switching_activated);
606 }
607}