1use std::path::PathBuf;
2use std::sync::mpsc::channel;
3use std::time::{Duration, Instant};
4
5use dmc_diagnostic::{Code, DiagResult};
6use duck_diagnostic::{Diagnostic, DiagnosticEngine, diag, print_all_smart};
7use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode};
8
9use crate::{Engine, engine::config::EngineConfig};
10
11#[derive(clap::Args)]
14pub struct DevCmd {
15 #[arg(long, default_value = "dmc.toml")]
16 pub config: PathBuf,
17 #[arg(short, long)]
18 pub strict: bool,
19 #[arg(long)]
20 pub clean: bool,
21 #[arg(long, default_value_t = 100)]
23 pub debounce: u64,
24}
25
26impl DevCmd {
27 pub fn run(self) -> DiagResult<Diagnostic<Code>> {
28 let mut cfg = EngineConfig::load(&self.config)?;
29 if self.strict {
30 cfg.strict = true;
31 }
32 if self.clean {
33 cfg.clean = true;
34 }
35
36 rebuild(&cfg, &self.config, "initial")?;
37
38 let (tx, rx) = channel();
39 let mut deb = new_debouncer(Duration::from_millis(self.debounce), tx).map_err(|e| {
40 diag!(
41 Code::Custom { code: String::from("N001"), severity: duck_diagnostic::Severity::Note },
42 format!("watch error: {}", e.to_string())
43 )
44 })?;
45
46 let mut roots: Vec<PathBuf> = cfg.collections.iter().map(|c| c.base_dir.clone()).collect();
47 roots.push(self.config.clone());
48
49 for r in &roots {
50 if r.exists() {
51 deb.watcher().watch(r, RecursiveMode::Recursive).map_err(|e| {
52 diag!(
53 Code::Custom { code: String::from("N001"), severity: duck_diagnostic::Severity::Note },
54 format!("watch error: {}", e.to_string())
55 )
56 })?;
57 }
58 }
59
60 println!("watching {} root(s) - Ctrl-C to stop", roots.len());
61
62 while let Ok(events) = rx.recv() {
63 let Ok(events) = events else { continue };
64 let touched: Vec<PathBuf> = events.iter().map(|e| e.path.clone()).collect();
65 let cfg_canon = self.config.canonicalize().ok();
66 let config_changed = touched.iter().any(|p| p.canonicalize().ok() == cfg_canon);
67
68 if config_changed {
69 let mut next = EngineConfig::load(&self.config)?;
70
71 if self.strict {
72 next.strict = true;
73 }
74 if self.clean {
75 next.clean = true;
76 }
77
78 cfg = next;
79 }
80
81 rebuild(&cfg, &self.config, if config_changed { "config" } else { "files" })?;
82 }
83
84 Ok(diag!(
85 Code::Custom { code: String::from("N001"), severity: duck_diagnostic::Severity::Note },
86 format!("watching {} root(s) - Ctrl-C to stop", roots.len())
87 ))
88 }
89}
90
91fn rebuild(cfg: &EngineConfig, config_path: &std::path::Path, _trigger: &str) -> DiagResult {
92 let mut diag_engine = DiagnosticEngine::<Code>::new();
93 let started = Instant::now();
94 Engine::run(cfg, Some(config_path), &mut diag_engine)?;
95 let elapsed = started.elapsed();
96 print_all_smart(&diag_engine, None);
97
98 diag_engine.emit(diag!(
99 Code::Custom { code: String::from("N001"), severity: duck_diagnostic::Severity::Note },
100 format!("built in {:<.3?}", elapsed)
101 ));
102
103 Ok(())
104}