-
-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
23c9008
commit b91f415
Showing
7 changed files
with
253 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
[package] | ||
name = "hotwatch" | ||
version = "0.4.2" | ||
version = "0.4.3" | ||
authors = ["Francesca Plebani <[email protected]>"] | ||
edition = "2018" | ||
description = "A Rust library for conveniently watching and handling file changes." | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
use hotwatch::{ | ||
blocking::{Flow, Hotwatch}, | ||
Event, | ||
}; | ||
use std::path::Path; | ||
|
||
fn main() -> Result<(), failure::Error> { | ||
let mut watcher = Hotwatch::new()?; | ||
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("examples/data.json"); | ||
watcher.watch(&path, move |event| { | ||
if let Event::Write(_path) = event { | ||
Flow::Exit | ||
} else { | ||
Flow::Continue | ||
} | ||
})?; | ||
println!("Edit data.json, and thou shalt be rewarded..."); | ||
watcher.run(); | ||
println!("🌭 🍔 🍟"); | ||
Ok(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
//! Blocking file watching | ||
use crate::{util, Error, Event}; | ||
use notify::Watcher as _; | ||
use std::{ | ||
collections::HashMap, | ||
path::{Path, PathBuf}, | ||
sync::mpsc::{channel, Receiver}, | ||
}; | ||
|
||
#[derive(Clone, Copy, Debug, Eq, PartialEq)] | ||
pub enum Flow { | ||
/// Continue watching and blocking the thread. | ||
Continue, | ||
/// Stop watching, returning control of the thread. | ||
Exit, | ||
} | ||
|
||
impl Default for Flow { | ||
fn default() -> Self { | ||
Self::Continue | ||
} | ||
} | ||
|
||
/// A blocking hotwatch instance. | ||
/// | ||
/// No watching will actually happen until you call [`Hotwatch::run`], which blocks | ||
/// the thread until a handler returns [`Flow::Exit`]. This is useful if you just | ||
/// want to wait on some criteria, rather than if you're building some long-running | ||
/// sexy hot reload service. | ||
/// | ||
/// Dropping this will unwatch everything. | ||
pub struct Hotwatch { | ||
watcher: notify::RecommendedWatcher, | ||
handlers: HashMap<PathBuf, Box<dyn Fn(Event) -> Flow>>, | ||
rx: Receiver<Event>, | ||
} | ||
|
||
impl std::fmt::Debug for Hotwatch { | ||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { | ||
fmt.debug_struct("Hotwatch").finish() | ||
} | ||
} | ||
|
||
impl Hotwatch { | ||
/// Creates a new blocking hotwatch instance. | ||
/// | ||
/// # Errors | ||
/// | ||
/// This will fail if the underlying [notify](https://docs.rs/notify/4.0/notify/) | ||
/// instance fails to initialize. | ||
/// | ||
/// # Examples | ||
/// | ||
/// ``` | ||
/// use hotwatch::blocking::Hotwatch; | ||
/// | ||
/// let hotwatch = Hotwatch::new().expect("hotwatch failed to initialize"); | ||
/// ``` | ||
pub fn new() -> Result<Self, Error> { | ||
Self::new_with_custom_delay(std::time::Duration::from_secs(2)) | ||
} | ||
|
||
/// Using [`Hotwatch::new`] will give you a default delay of 2 seconds. | ||
/// This method allows you to specify your own value. | ||
/// | ||
/// # Notes | ||
/// | ||
/// A delay of over 30 seconds will prevent repetitions of previous events on macOS. | ||
pub fn new_with_custom_delay(delay: std::time::Duration) -> Result<Self, Error> { | ||
let (tx, rx) = channel(); | ||
let watcher = notify::Watcher::new(tx, delay).map_err(Error::Notify)?; | ||
Ok(Self { | ||
watcher, | ||
handlers: Default::default(), | ||
rx, | ||
}) | ||
} | ||
|
||
/// Watch a path and register a handler to it. | ||
/// | ||
/// Handlers won't actually be run until you call [`Hotwatch::run`]. | ||
/// | ||
/// When watching a directory, that handler will receive all events for all directory | ||
/// contents, even recursing through subdirectories. | ||
/// | ||
/// Only the most specific applicable handler will be called. In other words, if you're | ||
/// watching "dir" and "dir/file1", then only the latter handler will fire for changes to | ||
/// `file1`. | ||
/// | ||
/// # Errors | ||
/// | ||
/// Watching will fail if the path can't be read, returning [`Error::Io`]. | ||
/// | ||
/// # Examples | ||
/// | ||
/// ``` | ||
/// use hotwatch::{blocking::{Flow, Hotwatch}, Event}; | ||
/// | ||
/// let mut hotwatch = Hotwatch::new().expect("hotwatch failed to initialize!"); | ||
/// // Note that this won't actually do anything until you call `hotwatch.run()`! | ||
/// hotwatch.watch("README.md", |event: Event| { | ||
/// if let Event::Write(path) = event { | ||
/// println!("{:?} changed!", path); | ||
/// Flow::Exit | ||
/// } else { | ||
/// Flow::Continue | ||
/// } | ||
/// }).expect("failed to watch file!"); | ||
/// ``` | ||
pub fn watch<P, F>(&mut self, path: P, handler: F) -> Result<(), Error> | ||
where | ||
P: AsRef<Path>, | ||
F: 'static + Fn(Event) -> Flow, | ||
{ | ||
let absolute_path = path.as_ref().canonicalize()?; | ||
self.watcher | ||
.watch(&absolute_path, notify::RecursiveMode::Recursive)?; | ||
self.handlers.insert(absolute_path, Box::new(handler)); | ||
Ok(()) | ||
} | ||
|
||
/// Stop watching a path. | ||
/// | ||
/// # Errors | ||
/// | ||
/// This will fail if the path wasn't being watched, or if the path | ||
/// couldn't be unwatched for some platform-specific internal reason. | ||
pub fn unwatch<P: AsRef<Path>>(&mut self, path: P) -> Result<(), Error> { | ||
let absolute_path = path.as_ref().canonicalize()?; | ||
self.watcher.unwatch(&absolute_path)?; | ||
self.handlers.remove(&absolute_path); | ||
Ok(()) | ||
} | ||
|
||
/// Run handlers in an endless loop, blocking the thread. | ||
/// | ||
/// The loop will only exit if a handler returns [`Flow::Exit`]. | ||
pub fn run(&mut self) { | ||
loop { | ||
match self.rx.recv() { | ||
Ok(event) => { | ||
util::log_event(&event); | ||
if let Some(handler) = util::handler_for_event(&event, &self.handlers) { | ||
if let Flow::Exit = handler(event) { | ||
break; | ||
} | ||
} | ||
} | ||
Err(_) => { | ||
util::log_dead(); | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
use crate::Event; | ||
use std::{collections::HashMap, path::PathBuf}; | ||
|
||
pub fn log_event(e: &Event) { | ||
log::debug!("received event 🎉: {:#?}", e); | ||
} | ||
|
||
pub fn log_dead() { | ||
log::debug!("sender disconnected! the watcher is dead 💀"); | ||
} | ||
|
||
pub fn handler_for_event<'a, H>(e: &Event, handlers: &'a HashMap<PathBuf, H>) -> Option<&'a H> { | ||
fn path_from_event(e: &Event) -> Option<PathBuf> { | ||
match e { | ||
Event::NoticeWrite(p) | ||
| Event::NoticeRemove(p) | ||
| Event::Create(p) | ||
| Event::Write(p) | ||
| Event::Chmod(p) | ||
| Event::Remove(p) | ||
| Event::Rename(p, _) => Some(p.clone()), | ||
_ => None, | ||
} | ||
} | ||
|
||
fn find_handler<'a, H>(mut path: PathBuf, handlers: &'a HashMap<PathBuf, H>) -> Option<&'a H> { | ||
let mut handler = None; | ||
let mut poppable = true; | ||
while handler.is_none() && poppable { | ||
log::debug!("matching against {:?}", path); | ||
handler = handlers.get(&path); | ||
poppable = path.pop(); | ||
} | ||
handler | ||
} | ||
|
||
path_from_event(e).and_then(|path| find_handler(path, handlers)) | ||
} |