ucxl/
watcher.rs

1//! UCXL filesystem watcher.
2//!
3//! This module provides a thin wrapper around the `notify` crate to watch a
4//! directory (or "project") for filesystem events. When a change is detected,
5//! the watcher attempts to construct a corresponding `UCXLAddress` using a
6//! simple heuristic and logs the event. This is primarily used by CHORUS for
7//! reactive workflows such as automatically updating metadata when files are
8//! added, modified or removed.
9
10use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
11use std::path::Path;
12use std::sync::mpsc::channel;
13use crate::UCXLAddress;
14use std::str::FromStr;
15
16/// Represents a watcher rooted at a specific base path.
17///
18/// **What**: Holds the absolute path that the watcher monitors.
19///
20/// **How**: The path is stored as a `PathBuf`. The watcher is created via the
21/// `new` constructor which accepts any type that can be referenced as a `Path`.
22/// The underlying `notify::RecommendedWatcher` is configured with the default
23/// `Config` and set to watch recursively.
24///
25/// **Why**: Encapsulating the watcher logic in a dedicated struct makes it easy
26/// to instantiate multiple independent watchers and keeps the public API tidy.
27pub struct UCXLWatcher {
28    base_path: std::path::PathBuf,
29}
30
31impl UCXLWatcher {
32    /// Creates a new `UCXLWatcher` for the given path.
33    ///
34    /// **What**: Accepts any generic `AsRef<Path>` so callers can pass a `&str`,
35    /// `Path`, or `PathBuf`.
36    ///
37    /// **How**: The provided path is converted to a `PathBuf` and stored.
38    ///
39    /// **Why**: Convenience constructor used throughout CHORUS when a watcher is
40    /// needed for a project directory.
41    pub fn new<P: AsRef<Path>>(path: P) -> Self {
42        Self {
43            base_path: path.as_ref().to_path_buf(),
44        }
45    }
46
47    /// Starts the watch loop, blocking indefinitely while handling events.
48    ///
49    /// **What**: Sets up a channel, creates a `RecommendedWatcher`, and begins
50    /// watching the `base_path` recursively. For each incoming event, it
51    /// attempts to map the filesystem path to a UCXL address and prints a log.
52    ///
53    /// **How**: Uses the `notify` crate's event API. The heuristic address
54    /// format is `ucxl://system:watcher@local:filesystem/#/<relative_path>`.
55    /// It parses this string with `UCXLAddress::from_str` and logs the result.
56    /// Errors from parsing are ignored (they simply aren't printed).
57    ///
58    /// **Why**: Provides a simple, observable bridge between raw filesystem
59    /// changes and the UCXL addressing scheme, allowing other components to react
60    /// to changes using a uniform identifier.
61    pub fn watch_loop(&self) -> Result<(), Box<dyn std::error::Error>> {
62        let (tx, rx) = channel();
63
64        let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
65        watcher.watch(&self.base_path, RecursiveMode::Recursive)?;
66
67        println!("UCXL Watcher started on {:?}", self.base_path);
68
69        for res in rx {
70            match res {
71                Ok(event) => {
72                    for path in event.paths {
73                        if let Some(rel_path) = path.strip_prefix(&self.base_path).ok() {
74                            let rel_str = rel_path.to_string_lossy();
75                            // Heuristic address mapping: ucxl://system:watcher@local:filesystem/#/path
76                            let addr_str = format!(
77                                "ucxl://system:watcher@local:filesystem/#/{}",
78                                rel_str
79                            );
80                            if let Ok(addr) = UCXLAddress::from_str(&addr_str) {
81                                println!("[UCXL EVENT] {:?} -> {}", event.kind, addr);
82                            }
83                        }
84                    }
85                }
86                Err(e) => println!("watch error: {:?}", e),
87            }
88        }
89        Ok(())
90    }
91}