KnotifyEx

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.

Why KnotifyEx?

Prerequisites

KnotifyEx requires the knotify binary to be installed on your system and available in your PATH.

Installing knotify

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)

Installation

Add knotify_ex to your dependencies in mix.exs:

def deps do
  [
    {:knotify_ex, "~> 0.1.0"}
  ]
end

Then run:

mix deps.get

Quick Start

The 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]}}

Event Types

KnotifyEx recognizes these file system events:

Filtering Events

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])

Real-World Example: Hot Reloading

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
end

Add 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)
end

Configuration Options

When starting a watcher with KnotifyEx.start_link/1, you can pass these options:

Required Options

Optional Options

Complete Example

{:ok, watcher} = KnotifyEx.start_link(
  dirs: ["./lib", "./priv"],
  name: :my_app_watcher,
  recursive: true,
  backend: :auto,
  debounce: 250
)

API Reference

start_link/1

Starts a new file watcher process. Returns {:ok, pid} on success.

subscribe/2

Subscribes the calling process to receive file events from the watcher.

Options:

unsubscribe/1

Unsubscribes the calling process from the watcher.

known_dirs/1

Returns the list of directories currently being watched.

KnotifyEx.known_dirs(watcher)
# => ["./lib", "./config"]

stop/1

Stops the watcher process gracefully.

Message Format

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]}}

Use Cases

KnotifyEx is perfect for:

Performance Considerations

Troubleshooting

"Could not find knotify binary in system PATH"

Make sure knotify is installed and available in your PATH:

which knotify
# Should output the path to knotify

If not found, install it:

cargo install knotify

Events not firing

  1. Verify the directory exists and is readable
  2. Check that you're subscribed to the watcher
  3. Ensure the event type you're expecting is in your filter (if using event filtering)
  4. Try increasing the debounce delay if events are happening too quickly

Too many events

  1. Increase the :debounce value to coalesce rapid events
  2. Use event filtering to only receive relevant events
  3. Consider watching a more specific directory

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT License - see LICENSE file for details.

Credits

KnotifyEx is powered by: