A fast, reliable file system watcher for Elixir powered by knotify, which wraps Rust's excellent notify crate.
KnotifyEx gives you real-time notifications when files and directories change. It's cross-platform, efficient, and works great in development and production environments.
- Fast and Efficient: Built on Rust's notify crate, one of the fastest file watching libraries available
- Cross-Platform: Native support for Linux (inotify), macOS (FSEvents), and other Unix systems (kqueue)
- Simple API: Subscribe to file events with just a few lines of code
- Flexible Filtering: Watch only the events you care about
- Battle-Tested: Leverages the mature Rust ecosystem for reliability
KnotifyEx requires the knotify binary to be installed on your system and available in your PATH.
Pre-built binaries are available at Knotify Binaries. Download the appropriate binary for your platform, extract, and add it to your system PATH.
Verify installation:
which knotify
# Should output: /usr/local/bin/knotify (or similar)Add knotify_ex to your dependencies in mix.exs:
def deps do
[
{:knotify_ex, "~> 0.1.0"}
]
endThen run:
mix deps.getThe simplest way to use KnotifyEx is to start a watcher and subscribe to events:
# Start watching a directory
{:ok, watcher} = KnotifyEx.start_link(dirs: ["./lib"])
# Subscribe to all file events
KnotifyEx.subscribe(watcher)
# You'll receive messages like:
# {:file_event, watcher_pid, {"/path/to/file.ex", [:modify]}}KnotifyEx recognizes these file system events:
:create- A new file was created:create_folder- A new directory was created:modify- A file's contents changed:remove- A file was deleted:remove_folder- A directory was deleted:rename- A file or directory was renamed
Subscribe to only the events you care about:
# Only notify me about new files and modifications
KnotifyEx.subscribe(watcher, events: [:create, :modify])
# Only track directory changes
KnotifyEx.subscribe(watcher, events: [:create_folder, :remove_folder])Here's a complete example of a GenServer that watches for file changes and triggers recompilation:
defmodule MyApp.DevReloader do
use GenServer
require Logger
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def init(opts) do
dirs = Keyword.get(opts, :dirs, ["./lib", "./config"])
# Start the file watcher
{:ok, watcher} = KnotifyEx.start_link(
dirs: dirs,
recursive: true,
debounce: 100
)
# Subscribe to file changes (only create and modify)
KnotifyEx.subscribe(watcher, events: [:create, :modify])
Logger.info("Watching #{length(dirs)} directories for changes...")
{:ok, %{watcher: watcher, dirs: dirs}}
end
def handle_info({:file_event, _watcher, {path, events}}, state) do
# Only reload for Elixir files
if String.ends_with?(path, ".ex") or String.ends_with?(path, ".exs") do
Logger.info("File changed: #{Path.relative_to_cwd(path)}")
Logger.debug("Events: #{inspect(events)}")
# Trigger your reload logic here
IEx.Helpers.recompile()
end
{:noreply, state}
end
def handle_info({:file_event, _watcher, :stop}, state) do
Logger.warning("File watcher stopped unexpectedly")
{:noreply, state}
end
endAdd it to your application supervision tree:
# In your application.ex
def start(_type, _args) do
children = [
# Your other processes...
{MyApp.DevReloader, dirs: ["./lib", "./config"]}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
endWhen starting a watcher with KnotifyEx.start_link/1, you can pass these options:
:dirs(list of strings) - One or more directories to watch. Each directory must exist.dirs: ["./lib"] dirs: ["/var/log", "/etc/config"]
-
:name(atom) - Register the watcher with a name so you can reference it without keeping the PID.{:ok, _pid} = KnotifyEx.start_link(dirs: ["./lib"], name: :my_watcher) KnotifyEx.subscribe(:my_watcher)
-
:recursive(boolean, default:true) - Whether to watch subdirectories recursively.# Only watch the top-level directory, not subdirectories recursive: false
-
:backend(atom, default::auto) - Which file watching backend to use. Options are::auto- Automatically choose the best backend for your OS (recommended):inotify- Linux inotify (Linux only):kqueue- BSD kqueue (macOS, BSD):fsevent- macOS FSEvents (macOS only)
# Force inotify on Linux backend: :inotify
-
:debounce(integer, default:100) - Debounce delay in milliseconds. File system events that occur within this window will be coalesced to prevent flooding your application with duplicate events.# Wait 500ms before sending events (useful for editors that save multiple times) debounce: 500
{:ok, watcher} = KnotifyEx.start_link(
dirs: ["./lib", "./priv"],
name: :my_app_watcher,
recursive: true,
backend: :auto,
debounce: 250
)Starts a new file watcher process. Returns {:ok, pid} on success.
Subscribes the calling process to receive file events from the watcher.
Options:
:events- List of event types to filter (optional). If omitted, all events are sent.
Unsubscribes the calling process from the watcher.
Returns the list of directories currently being watched.
KnotifyEx.known_dirs(watcher)
# => ["./lib", "./config"]Stops the watcher process gracefully.
Subscribed processes receive messages in this format:
# File event
{:file_event, watcher_pid, {path, events}}
# Watcher stopped
{:file_event, watcher_pid, :stop}Example:
{:file_event, #PID<0.123.0>, {"/Users/you/project/lib/my_module.ex", [:modify]}}
KnotifyEx is perfect for:
- Development tools - Auto-reloading, hot code reloading
- Build systems - Triggering rebuilds when source files change
- Testing - Running tests automatically when files are saved
- Log monitoring - Watching log directories for new entries
- Configuration reloading - Reloading app config when files change
- File processing pipelines - Processing files as they arrive in a directory
- Debouncing: Use a reasonable debounce value (100-500ms) to avoid overwhelming your system with events, especially when watching directories with frequent changes.
- Event filtering: Subscribe only to the events you need. This reduces message passing overhead.
- Directory scope: Watch specific directories rather than entire file systems. The more files you watch, the more system resources are used.
- Recursive watching: Be mindful when using
recursive: trueon large directory trees. Consider watching only the directories you need.
Make sure knotify is installed and available in your PATH:
which knotify
# Should output the path to knotifyIf not found, install it:
cargo install knotify- Verify the directory exists and is readable
- Check that you're subscribed to the watcher
- Ensure the event type you're expecting is in your filter (if using event filtering)
- Try increasing the debounce delay if events are happening too quickly
- Increase the
:debouncevalue to coalesce rapid events - Use event filtering to only receive relevant events
- Consider watching a more specific directory
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details.
KnotifyEx is powered by: