venus_server/
watcher.rs

1//! File watcher for detecting notebook changes.
2//!
3//! Watches `.rs` notebook files and notifies the session when changes occur.
4
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7use std::time::Duration;
8
9use notify_debouncer_mini::{DebounceEventResult, new_debouncer, notify::RecursiveMode};
10use tokio::sync::mpsc;
11
12use crate::error::{ServerError, ServerResult};
13
14/// File change event.
15#[derive(Debug, Clone)]
16pub enum FileEvent {
17    /// File was modified.
18    Modified(PathBuf),
19    /// File was created.
20    Created(PathBuf),
21    /// File was removed.
22    Removed(PathBuf),
23}
24
25/// File watcher handle.
26pub struct FileWatcher {
27    /// Debouncer handle (kept alive to maintain watcher).
28    _debouncer: notify_debouncer_mini::Debouncer<notify::RecommendedWatcher>,
29    /// Receiver for file events.
30    rx: mpsc::UnboundedReceiver<FileEvent>,
31}
32
33impl FileWatcher {
34    /// Create a new file watcher for the given path.
35    pub fn new(path: impl AsRef<Path>) -> ServerResult<Self> {
36        let path = path.as_ref().to_path_buf();
37        let watch_path = if path.is_file() {
38            path.parent().unwrap_or(Path::new(".")).to_path_buf()
39        } else {
40            path.clone()
41        };
42
43        let (tx, rx) = mpsc::unbounded_channel();
44        let target_file = if path.is_file() {
45            Some(Arc::new(path))
46        } else {
47            None
48        };
49
50        let mut debouncer = new_debouncer(
51            Duration::from_millis(200),
52            move |result: DebounceEventResult| {
53                if let Ok(events) = result {
54                    for event in events {
55                        let event_path = &event.path;
56
57                        // Filter to only .rs files
58                        if event_path.extension().is_none_or(|ext| ext != "rs") {
59                            continue;
60                        }
61
62                        // If watching a specific file, only report events for that file
63                        if let Some(ref target) = target_file
64                            && event_path != target.as_ref()
65                        {
66                            continue;
67                        }
68
69                        let file_event = if event_path.exists() {
70                            FileEvent::Modified(event_path.clone())
71                        } else {
72                            FileEvent::Removed(event_path.clone())
73                        };
74
75                        let _ = tx.send(file_event);
76                    }
77                }
78            },
79        )
80        .map_err(|e| ServerError::Watch(e.to_string()))?;
81
82        debouncer
83            .watcher()
84            .watch(&watch_path, RecursiveMode::NonRecursive)
85            .map_err(|e| ServerError::Watch(e.to_string()))?;
86
87        Ok(Self {
88            _debouncer: debouncer,
89            rx,
90        })
91    }
92
93    /// Receive the next file event.
94    pub async fn recv(&mut self) -> Option<FileEvent> {
95        self.rx.recv().await
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use std::fs;
103    use tempfile::TempDir;
104
105    #[tokio::test]
106    async fn test_watcher_creation() {
107        let temp = TempDir::new().unwrap();
108        let notebook = temp.path().join("test.rs");
109        fs::write(&notebook, "// test").unwrap();
110
111        let watcher = FileWatcher::new(&notebook);
112        assert!(watcher.is_ok());
113    }
114}