Skip to main content

dmc/cli/
dev.rs

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/// Initial build, then watch every collection's `base_dir` (plus the
12/// config file) and rebuild on change.
13#[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  /// Debounce window for filesystem events (ms).
22  #[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}