hotwatch/
lib.rs

1//! `hotwatch` is a Rust library for comfortably watching and handling file changes.
2//! It's a thin convenience wrapper over [`notify`](https://github.com/passcod/notify),
3//! allowing you to easily set callbacks for each path you want to watch.
4//!
5//! Watching is done on a separate thread so you don't have to worry about blocking.
6//! All handlers are run on that thread as well, so keep that in mind when attempting to access
7//! outside data from within a handler.
8//!
9//! (There's also a [`blocking`] mode, in case you're a big fan of blocking.)
10//!
11//! Only the latest stable version of Rust is supported.
12
13pub mod blocking;
14mod util;
15
16use notify::Watcher as _;
17pub use notify::{self, EventKind};
18pub use notify_debouncer_full::DebouncedEvent as Event;
19use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, FileIdMap};
20use std::{
21    collections::HashMap,
22    path::{Path, PathBuf},
23    sync::{
24        mpsc::{channel, Receiver},
25        Arc, Mutex,
26    },
27};
28
29const RECURSIVE_MODE: notify::RecursiveMode = notify::RecursiveMode::Recursive;
30
31#[derive(Debug)]
32pub enum Error {
33    Io(std::io::Error),
34    Notify(notify::Error),
35}
36
37impl std::fmt::Display for Error {
38    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
39        match self {
40            Self::Io(error) => error.fmt(fmt),
41            Self::Notify(error) => error.fmt(fmt),
42        }
43    }
44}
45
46impl std::error::Error for Error {
47    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
48        match self {
49            Self::Io(error) => error.source(),
50            Self::Notify(error) => error.source(),
51        }
52    }
53}
54
55impl From<std::io::Error> for Error {
56    fn from(err: std::io::Error) -> Self {
57        Self::Io(err)
58    }
59}
60
61impl From<notify::Error> for Error {
62    fn from(err: notify::Error) -> Self {
63        if let notify::ErrorKind::Io(err) = err.kind {
64            err.into()
65        } else {
66            Self::Notify(err)
67        }
68    }
69}
70
71type HandlerMap = HashMap<PathBuf, Box<dyn FnMut(Event) + Send>>;
72
73/// A non-blocking hotwatch instance.
74///
75/// Watching begins as soon as [`Self::watch`] is called, and occurs in a
76/// background thread. The background thread runs until this is dropped.
77///
78/// Dropping this will also unwatch everything.
79pub struct Hotwatch {
80    debouncer: Debouncer<notify::RecommendedWatcher, FileIdMap>,
81    handlers: Arc<Mutex<HandlerMap>>,
82}
83
84impl std::fmt::Debug for Hotwatch {
85    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
86        fmt.debug_struct("Hotwatch").finish()
87    }
88}
89
90impl Hotwatch {
91    /// Creates a new non-blocking hotwatch instance.
92    ///
93    /// # Errors
94    ///
95    /// This will fail if the underlying [notify](https://docs.rs/notify/4.0/notify/)
96    /// instance fails to initialize.
97    ///
98    /// # Examples
99    ///
100    /// ```
101    /// use hotwatch::Hotwatch;
102    ///
103    /// let hotwatch = Hotwatch::new().expect("hotwatch failed to initialize");
104    /// ```
105    pub fn new() -> Result<Self, Error> {
106        Self::new_with_custom_delay(std::time::Duration::from_secs(2))
107    }
108
109    /// Using [`Hotwatch::new`] will give you a default delay of 2 seconds.
110    /// This method allows you to specify your own value.
111    ///
112    /// # Notes
113    ///
114    /// A delay of over 30 seconds will prevent repetitions of previous events on macOS.
115    pub fn new_with_custom_delay(delay: std::time::Duration) -> Result<Self, Error> {
116        let (tx, rx) = channel();
117        let handlers = Arc::<Mutex<_>>::default();
118        Self::run(Arc::clone(&handlers), rx);
119        let debouncer = new_debouncer(delay, None, tx).map_err(Error::Notify)?;
120        Ok(Self {
121            debouncer,
122            handlers,
123        })
124    }
125
126    /// Watch a path and register a handler to it.
127    ///
128    /// When watching a directory, that handler will receive all events for all directory
129    /// contents, even recursing through subdirectories.
130    ///
131    /// Only the most specific applicable handler will be called. In other words, if you're
132    /// watching "dir" and "dir/file1", then only the latter handler will fire for changes to
133    /// `file1`.
134    ///
135    /// Note that handlers will be run in hotwatch's watch thread, so you'll have to use `move`
136    /// if the closure captures anything.
137    ///
138    /// # Errors
139    ///
140    /// Watching will fail if the path can't be read, returning [`Error::Io`].
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use hotwatch::{Hotwatch, Event, EventKind};
146    ///
147    /// let mut hotwatch = Hotwatch::new().expect("hotwatch failed to initialize!");
148    /// hotwatch.watch("README.md", |event: Event| {
149    ///     if let EventKind::Modify(_) = event.kind {
150    ///         println!("{:?} changed!", event.paths[0]);
151    ///     }
152    /// }).expect("failed to watch file!");
153    /// ```
154    pub fn watch<P, F>(&mut self, path: P, handler: F) -> Result<(), Error>
155    where
156        P: AsRef<Path>,
157        F: 'static + FnMut(Event) + Send,
158    {
159        let absolute_path = path.as_ref().canonicalize()?;
160        self.debouncer
161            .watcher()
162            .watch(&absolute_path, RECURSIVE_MODE)?;
163        self.debouncer
164            .cache()
165            .add_root(&absolute_path, RECURSIVE_MODE);
166        let mut handlers = self.handlers.lock().expect("handler mutex poisoned!");
167        handlers.insert(absolute_path, Box::new(handler));
168        Ok(())
169    }
170
171    /// Stop watching a path.
172    ///
173    /// # Errors
174    ///
175    /// This will fail if the path wasn't being watched, or if the path
176    /// couldn't be unwatched for some platform-specific internal reason.
177    pub fn unwatch<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> {
178        let absolute_path = path.as_ref().canonicalize()?;
179        self.debouncer.watcher().unwatch(&absolute_path)?;
180        self.debouncer.cache().remove_root(&absolute_path);
181        let mut handlers = self.handlers.lock().expect("handler mutex poisoned!");
182        handlers.remove(&absolute_path);
183        Ok(())
184    }
185
186    fn run(handlers: Arc<Mutex<HandlerMap>>, rx: Receiver<DebounceEventResult>) {
187        std::thread::spawn(move || loop {
188            match rx.recv() {
189                Ok(result) => match result {
190                    Ok(events) => {
191                        for event in events {
192                            util::log_event(&event);
193                            let mut handlers = handlers.lock().expect("handler mutex poisoned!");
194                            if let Some(handler) = util::handler_for_event(&event, &mut handlers) {
195                                handler(event);
196                            }
197                        }
198                    }
199                    Err(errs) => {
200                        for err in errs {
201                            util::log_error(&err);
202                        }
203                    }
204                },
205                Err(_) => {
206                    util::log_dead();
207                    break;
208                }
209            }
210        });
211    }
212}