1use std::path::PathBuf;
12
13use anyhow::{Context, Result};
14use notify::{EventKind, RecursiveMode, Watcher};
15
16use crate::ipc::{Client, Observation};
17
18pub fn kind_label(kind: &EventKind) -> Option<&'static str> {
21 match kind {
22 EventKind::Create(_) => Some("created"),
23 EventKind::Modify(notify::event::ModifyKind::Name(_)) => Some("renamed"),
24 EventKind::Modify(_) => Some("modified"),
25 EventKind::Remove(_) => Some("removed"),
26 _ => None,
27 }
28}
29
30pub fn run(roots: &[PathBuf]) -> Result<()> {
32 if roots.is_empty() {
33 anyhow::bail!("nothing to watch (pass one or more paths)");
34 }
35 let (tx, rx) = std::sync::mpsc::channel();
36 let mut watcher = notify::recommended_watcher(move |res| {
37 let _ = tx.send(res);
38 })
39 .context("create filesystem watcher")?;
40
41 for root in roots {
42 watcher
43 .watch(root, RecursiveMode::Recursive)
44 .with_context(|| format!("watch {}", root.display()))?;
45 eprintln!("kintsugi-watch: watching {}", root.display());
46 }
47
48 for res in rx {
49 match res {
50 Ok(event) => forward(&event),
51 Err(e) => eprintln!("kintsugi-watch: watch error: {e}"),
52 }
53 }
54 Ok(())
55}
56
57fn forward(event: ¬ify::Event) {
59 let Some(kind) = kind_label(&event.kind) else {
60 return;
61 };
62 for path in &event.paths {
63 let obs = Observation {
64 kind: kind.to_string(),
65 path: path.display().to_string(),
66 };
67 if let Err(e) = Client::observe(&obs) {
68 eprintln!("kintsugi-watch: could not record {}: {e}", path.display());
69 }
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76 use notify::event::{CreateKind, ModifyKind, RemoveKind};
77
78 #[test]
79 fn maps_interesting_kinds() {
80 assert_eq!(
81 kind_label(&EventKind::Create(CreateKind::File)),
82 Some("created")
83 );
84 assert_eq!(
85 kind_label(&EventKind::Remove(RemoveKind::File)),
86 Some("removed")
87 );
88 assert_eq!(
89 kind_label(&EventKind::Modify(ModifyKind::Data(
90 notify::event::DataChange::Any
91 ))),
92 Some("modified")
93 );
94 assert_eq!(
95 kind_label(&EventKind::Access(notify::event::AccessKind::Any)),
96 None
97 );
98 }
99
100 #[test]
101 fn empty_roots_is_an_error() {
102 assert!(run(&[]).is_err());
103 }
104}