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!(content.contains("<?xml"), "Should have XML declaration");
300 assert!(content.contains("<column"), "Should have column layout");
301 assert!(content.contains("Settings"), "Should contain window title");
302 }
303
304 #[test]
305 fn test_generate_files_rejects_existing_rs() {
306 let temp = TempDir::new().unwrap();
308 let project_root = temp.path();
309 let target_dir = project_root.join("src/ui");
310 fs::create_dir_all(&target_dir).unwrap();
311
312 let existing_file = target_dir.join("settings.rs");
314 fs::write(&existing_file, "existing content").unwrap();
315
316 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
317 let window_name = WindowName::new("settings").unwrap();
318 let result = generate_window_files(&target_path, &window_name, false);
319
320 assert!(result.is_err());
321 match result {
322 Err(GenerationError::FileExists {
323 window_name: name,
324 path,
325 }) => {
326 assert_eq!(name, "settings");
327 assert_eq!(path, existing_file);
328 }
329 _ => panic!("Expected FileExists error"),
330 }
331 }
332
333 #[test]
334 fn test_generate_files_rejects_existing_dampen() {
335 let temp = TempDir::new().unwrap();
337 let project_root = temp.path();
338 let target_dir = project_root.join("src/ui");
339 fs::create_dir_all(&target_dir).unwrap();
340
341 let existing_file = target_dir.join("dashboard.dampen");
343 fs::write(&existing_file, "existing xml content").unwrap();
344
345 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
346 let window_name = WindowName::new("dashboard").unwrap();
347 let result = generate_window_files(&target_path, &window_name, false);
348
349 assert!(result.is_err());
350 match result {
351 Err(GenerationError::FileExists {
352 window_name: name,
353 path,
354 }) => {
355 assert_eq!(name, "dashboard");
356 assert_eq!(path, existing_file);
357 }
358 _ => panic!("Expected FileExists error"),
359 }
360 }
361
362 #[test]
363 fn test_generate_files_rejects_partial_conflict() {
364 let temp = TempDir::new().unwrap();
366 let project_root = temp.path();
367 let target_dir = project_root.join("src/ui");
368 fs::create_dir_all(&target_dir).unwrap();
369
370 let existing_rs = target_dir.join("profile.rs");
372 fs::write(&existing_rs, "existing rust content").unwrap();
373
374 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
375 let window_name = WindowName::new("profile").unwrap();
376 let result = generate_window_files(&target_path, &window_name, false);
377
378 assert!(result.is_err());
380 match result {
381 Err(GenerationError::FileExists {
382 window_name: name,
383 path,
384 }) => {
385 assert_eq!(name, "profile");
386 assert_eq!(path, existing_rs);
387 }
388 _ => panic!("Expected FileExists error for partial conflict"),
389 }
390
391 fs::remove_file(&existing_rs).unwrap();
393 let existing_dampen = target_dir.join("profile.dampen");
394 fs::write(&existing_dampen, "existing xml").unwrap();
395
396 let result2 = generate_window_files(&target_path, &window_name, false);
397
398 assert!(result2.is_err());
400 match result2 {
401 Err(GenerationError::FileExists {
402 window_name: name,
403 path,
404 }) => {
405 assert_eq!(name, "profile");
406 assert_eq!(path, existing_dampen);
407 }
408 _ => panic!("Expected FileExists error for .dampen conflict"),
409 }
410 }
411
412 #[test]
413 fn test_success_message_format() {
414 let temp = TempDir::new().unwrap();
415 let project_root = temp.path();
416 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
417 let window_name = WindowName::new("dashboard").unwrap();
418
419 let generated = GeneratedFiles {
420 rust_file: target_path.absolute.join("dashboard.rs"),
421 dampen_file: target_path.absolute.join("dashboard.dampen"),
422 window_name: window_name.clone(),
423 target_dir: target_path.absolute.clone(),
424 updated_mod_file: None,
425 view_switching_activated: false,
426 };
427
428 let message = generated.success_message();
429
430 assert!(message.contains("Created UI window 'dashboard'"));
431 assert!(message.contains("dashboard.rs"));
432 assert!(message.contains("dashboard.dampen"));
433 assert!(message.contains("pub mod dashboard"));
434 assert!(message.contains("Next steps"));
435 }
436
437 #[test]
438 fn test_success_message_with_integration() {
439 let temp = TempDir::new().unwrap();
440 let project_root = temp.path();
441 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
442 let window_name = WindowName::new("settings").unwrap();
443
444 let generated = GeneratedFiles {
445 rust_file: target_path.absolute.join("settings.rs"),
446 dampen_file: target_path.absolute.join("settings.dampen"),
447 window_name: window_name.clone(),
448 target_dir: target_path.absolute.clone(),
449 updated_mod_file: Some(project_root.join("src/ui/mod.rs")),
450 view_switching_activated: false,
451 };
452
453 let message = generated.success_message();
454
455 assert!(message.contains("Created UI window 'settings'"));
456 assert!(message.contains("settings.rs"));
457 assert!(message.contains("settings.dampen"));
458 assert!(message.contains("Updated"), "Should mention mod.rs update");
459 assert!(message.contains("src/ui/mod.rs"), "Should show mod.rs path");
460 assert!(
461 !message.contains("pub mod settings"),
462 "Should NOT show manual mod instruction"
463 );
464 assert!(message.contains("Next steps"));
465 }
466
467 #[test]
468 fn test_generate_files_with_integration() {
469 let temp = TempDir::new().unwrap();
470 let project_root = temp.path();
471
472 let ui_dir = project_root.join("src/ui");
474 fs::create_dir_all(&ui_dir).unwrap();
475 fs::write(ui_dir.join("mod.rs"), "").unwrap();
476
477 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
478 let window_name = WindowName::new("settings").unwrap();
479
480 let result = generate_window_files(&target_path, &window_name, true);
481
482 assert!(result.is_ok());
483 let generated = result.unwrap();
484
485 assert!(generated.rust_file.exists());
487 assert!(generated.dampen_file.exists());
488
489 assert!(generated.updated_mod_file.is_some());
491 let mod_file = generated.updated_mod_file.unwrap();
492 assert_eq!(mod_file, ui_dir.join("mod.rs"));
493
494 let mod_content = fs::read_to_string(&mod_file).unwrap();
496 assert!(mod_content.contains("pub mod settings;"));
497 }
498
499 #[test]
500 fn test_generate_files_integration_disabled() {
501 let temp = TempDir::new().unwrap();
502 let project_root = temp.path();
503
504 let ui_dir = project_root.join("src/ui");
506 fs::create_dir_all(&ui_dir).unwrap();
507 fs::write(ui_dir.join("mod.rs"), "").unwrap();
508
509 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
510 let window_name = WindowName::new("settings").unwrap();
511
512 let result = generate_window_files(&target_path, &window_name, false);
513
514 assert!(result.is_ok());
515 let generated = result.unwrap();
516
517 assert!(generated.rust_file.exists());
519 assert!(generated.dampen_file.exists());
520
521 assert!(generated.updated_mod_file.is_none());
523
524 let mod_content = fs::read_to_string(ui_dir.join("mod.rs")).unwrap();
526 assert!(mod_content.is_empty());
527 }
528
529 #[test]
530 fn test_view_switching_activated_on_second_window() {
531 let temp = TempDir::new().unwrap();
532 let project_root = temp.path();
533
534 let ui_dir = project_root.join("src/ui");
536 fs::create_dir_all(&ui_dir).unwrap();
537 fs::write(ui_dir.join("mod.rs"), "pub mod window;\n").unwrap();
538 fs::write(ui_dir.join("window.rs"), "// First window").unwrap();
539
540 let src_dir = project_root.join("src");
542 fs::create_dir_all(&src_dir).unwrap();
543 let main_content = r#"
544enum Message {
545 // SwitchToView(CurrentView),
546 Handler(HandlerMessage),
547}
548
549#[dampen_app(
550 ui_dir = "src/ui",
551 message_type = "Message",
552 // switch_view_variant = "SwitchToView",
553)]
554struct App;
555"#;
556 fs::write(src_dir.join("main.rs"), main_content).unwrap();
557
558 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
560 let window_name = WindowName::new("settings").unwrap();
561
562 let result = generate_window_files(&target_path, &window_name, true);
563
564 assert!(result.is_ok());
565 let generated = result.unwrap();
566
567 assert!(generated.view_switching_activated);
569
570 let main_content = fs::read_to_string(src_dir.join("main.rs")).unwrap();
572 assert!(main_content.contains("SwitchToView(CurrentView),"));
573 assert!(!main_content.contains("// SwitchToView(CurrentView),"));
574 assert!(main_content.contains(r#"switch_view_variant = "SwitchToView","#));
575 assert!(!main_content.contains(r#"// switch_view_variant"#));
576 }
577
578 #[test]
579 fn test_view_switching_not_activated_on_first_window() {
580 let temp = TempDir::new().unwrap();
581 let project_root = temp.path();
582
583 let ui_dir = project_root.join("src/ui");
585 fs::create_dir_all(&ui_dir).unwrap();
586 fs::write(ui_dir.join("mod.rs"), "").unwrap();
587
588 let target_path = TargetPath::resolve(project_root, Some("src/ui")).unwrap();
590 let window_name = WindowName::new("window").unwrap();
591
592 let result = generate_window_files(&target_path, &window_name, true);
593
594 assert!(result.is_ok());
595 let generated = result.unwrap();
596
597 assert!(!generated.view_switching_activated);
599 }
600}