polykit_core/
watcher.rs

1//! File watching for incremental rebuilds.
2
3use std::collections::HashSet;
4use std::path::{Path, PathBuf};
5
6use notify::Config as NotifyConfig;
7use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
8
9use crate::error::{Error, Result};
10use crate::path_utils;
11
12pub struct WatcherConfig {
13    pub debounce_ms: u64,
14    pub packages_dir: PathBuf,
15}
16
17impl Default for WatcherConfig {
18    fn default() -> Self {
19        Self {
20            debounce_ms: 300,
21            packages_dir: PathBuf::from("./packages"),
22        }
23    }
24}
25
26pub struct FileWatcher {
27    watcher: RecommendedWatcher,
28    receiver: std::sync::mpsc::Receiver<notify::Result<Event>>,
29    config: WatcherConfig,
30}
31
32impl FileWatcher {
33    pub fn new(config: WatcherConfig) -> Result<Self> {
34        let (tx, rx) = std::sync::mpsc::channel();
35        let notify_config = NotifyConfig::default();
36
37        let watcher = RecommendedWatcher::new(
38            move |res| {
39                if let Err(e) = tx.send(res) {
40                    eprintln!("Failed to send watcher event: {}", e);
41                }
42            },
43            notify_config,
44        )
45        .map_err(|e| Error::Adapter {
46            package: "watcher".to_string(),
47            message: format!("Failed to create watcher: {}", e),
48        })?;
49
50        let mut file_watcher = Self {
51            watcher,
52            receiver: rx,
53            config,
54        };
55
56        file_watcher.watch_packages_dir()?;
57
58        Ok(file_watcher)
59    }
60
61    fn watch_packages_dir(&mut self) -> Result<()> {
62        self.watcher
63            .watch(&self.config.packages_dir, RecursiveMode::Recursive)
64            .map_err(|e| Error::Adapter {
65                package: "watcher".to_string(),
66                message: format!("Failed to watch directory: {}", e),
67            })?;
68        Ok(())
69    }
70
71    pub fn next_event(&mut self) -> Result<Option<Event>> {
72        match self.receiver.try_recv() {
73            Ok(Ok(event)) => Ok(Some(event)),
74            Ok(Err(e)) => Err(Error::Adapter {
75                package: "watcher".to_string(),
76                message: format!("Watcher error: {}", e),
77            }),
78            Err(std::sync::mpsc::TryRecvError::Empty) => Ok(None),
79            Err(std::sync::mpsc::TryRecvError::Disconnected) => Err(Error::Adapter {
80                package: "watcher".to_string(),
81                message: "Watcher channel disconnected".to_string(),
82            }),
83        }
84    }
85
86    pub fn wait_for_event(&mut self) -> Result<Event> {
87        self.receiver
88            .recv()
89            .map_err(|_| Error::Adapter {
90                package: "watcher".to_string(),
91                message: "Watcher channel disconnected".to_string(),
92            })?
93            .map_err(|e| Error::Adapter {
94                package: "watcher".to_string(),
95                message: format!("Watcher error: {}", e),
96            })
97    }
98
99    pub fn get_affected_packages(&self, event: &Event) -> HashSet<String> {
100        let mut affected = HashSet::new();
101
102        match &event.kind {
103            EventKind::Any | EventKind::Other => {
104                for path in &event.paths {
105                    if let Some(package_name) =
106                        Self::file_to_package(path, &self.config.packages_dir)
107                    {
108                        affected.insert(package_name);
109                    }
110                }
111            }
112            _ => {
113                for path in &event.paths {
114                    if let Some(package_name) =
115                        Self::file_to_package(path, &self.config.packages_dir)
116                    {
117                        affected.insert(package_name);
118                    }
119                }
120            }
121        }
122
123        affected
124    }
125
126    fn file_to_package(file_path: &Path, packages_dir: &Path) -> Option<String> {
127        path_utils::file_to_package(file_path, packages_dir)
128    }
129}