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}