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;
6use duck_diagnostic::{DiagnosticEngine, 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) -> std::io::Result<()> {
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 =
40      new_debouncer(Duration::from_millis(self.debounce), tx).map_err(|e| std::io::Error::other(e.to_string()))?;
41
42    let mut roots: Vec<PathBuf> = cfg.collections.iter().map(|c| c.base_dir.clone()).collect();
43    roots.push(self.config.clone());
44    for r in &roots {
45      if r.exists() {
46        deb.watcher().watch(r, RecursiveMode::Recursive).map_err(|e| std::io::Error::other(e.to_string()))?;
47      }
48    }
49
50    println!("watching {} root(s) - Ctrl-C to stop", roots.len());
51
52    while let Ok(events) = rx.recv() {
53      let Ok(events) = events else { continue };
54      let touched: Vec<PathBuf> = events.iter().map(|e| e.path.clone()).collect();
55      let cfg_canon = self.config.canonicalize().ok();
56      let config_changed = touched.iter().any(|p| p.canonicalize().ok() == cfg_canon);
57
58      if config_changed {
59        match EngineConfig::load(&self.config) {
60          Ok(mut next) => {
61            if self.strict {
62              next.strict = true;
63            }
64            if self.clean {
65              next.clean = true;
66            }
67            cfg = next;
68          },
69          Err(e) => {
70            eprintln!("config reload failed: {e}");
71            continue;
72          },
73        }
74      }
75
76      rebuild(&cfg, &self.config, if config_changed { "config" } else { "files" });
77    }
78
79    Ok(())
80  }
81}
82
83fn rebuild(cfg: &EngineConfig, config_path: &std::path::Path, trigger: &str) {
84  let mut diag_engine = DiagnosticEngine::<Code>::new();
85  let started = Instant::now();
86  let result = Engine::run(cfg, Some(config_path), &mut diag_engine);
87  let elapsed = started.elapsed();
88  print_all_smart(&diag_engine, None);
89  match result {
90    Ok(()) => println!("rebuilt ({trigger}) in {:?}", elapsed),
91    Err(e) => eprintln!("build error ({trigger}): {e}"),
92  }
93}