FsNotify

Hex.pmHexDocsLicenseCI

Cross-platform filesystem watcher for Elixir, backed by notify-rs via Rustler.

Watch files and directories and receive change events as messages in your own process. Each native watcher runs on its own OS-backed thread (FSEvents on macOS, inotify on Linux, ReadDirectoryChangesW on Windows) and forwards events into the BEAM.

Features

Installation

Add fs_notify to your deps in mix.exs:

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

FsNotify ships precompiled NIFs via rustler_precompiled, so a Rust toolchain is not required for the supported targets (macOS / Linux / Windows on x86_64 and aarch64). The artifact for your platform is downloaded and verified against a checksum at compile time.

Building from source

To compile the NIF locally instead of downloading it (e.g. an unsupported target, or to hack on the Rust code), force a build — this needs cargo:

FS_NOTIFY_BUILD=1 mix compile

You can also force a build for all rustler_precompiled packages with config :rustler_precompiled, force_build_all: true. Non-:prod Mix environments build from source by default.

Quick start

Watch from a process and handle events in handle_info/2:

defmodule DirWatcher do
use GenServer
def start_link(dir), do: GenServer.start_link(__MODULE__, dir)
@impl true
def init(dir) do
{:ok, _ref} = FsNotify.watch(dir, recursive: true)
{:ok, dir}
end
@impl true
def handle_info({:fs_notify_event, %FsNotify.Event{kind: kind, paths: paths}}, dir) do
IO.puts("#{kind}: #{Enum.join(paths, ", ")}")
{:noreply, dir}
end
end
{:ok, _pid} = DirWatcher.start_link("/path/to/dir")

The watch stops automatically when DirWatcher stops — no cleanup needed.

Usage

# Start watching a directory (or a list of paths). Events go to the caller by default.
{:ok, ref} = FsNotify.watch("/path/to/dir", recursive: true)
# A single watcher can cover several paths:
{:ok, ref} = FsNotify.watch(["/path/a", "/path/b"])
# Receive events — one message per event, carrying all affected paths.
receive do
{:fs_notify_event, %FsNotify.Event{kind: kind, paths: paths}} ->
IO.inspect({kind, paths})
end
# Stop watching. Optional — the native watch also stops automatically when the
# subscriber process dies or `ref` is garbage-collected.
FsNotify.unwatch(ref)

Keep ref for as long as you want to watch; the watch is tied to the subscriber process.

Message shape

Each notify event arrives as its own message:

{:fs_notify_event, %FsNotify.Event{kind: kind, detail: detail, paths: paths}}

notify reports different event kinds per platform, so do not rely on a specific kind/detail for the same operation across OSes.

Options

OptionDefaultDescription
:recursivetrueWatch subdirectories recursively.
:debounce0Coalesce events over this many ms (notify-rs debouncer); 0 = off.
:backend:recommended:recommended (OS-native) or :poll (portable polling).
:poll_interval0Poll interval (ms) for the :poll backend; 0 = notify default.
:subscribercalling processProcess that receives {:fs_notify_event, ...} messages.

How it works

notify-rs thread --{:fs_notify_event, %Event{}}--> subscriber process

The native RecommendedWatcher is held inside a Rustler resource. Events are mapped to %FsNotify.Event{} in Rust and pushed directly to the subscriber, one message per event. With :debounce set, events are coalesced natively via notify-debouncer-full. The resource monitors the subscriber: when it dies (or when the resource is garbage-collected), notify is dropped and the OS watch stops. There is no GenServer in the path — no event-kind filtering or supervision; do that in your own process if you need it.

Development

mix deps.get
mix test # runs unit + native integration tests (uses real temp dirs)
mix format
mix docs

Releasing

Publishing a GitHub release triggers .github/workflows/release.yml, which cross-compiles the NIFs, attaches them to the release, generates the rustler_precompiled checksum file, and runs mix hex.publish.

Required steps:

  1. Bump the version in mix.exs and native/fs_notify/Cargo.toml (keep them in sync).
  2. Update CHANGELOG.md.
  3. Commit and push to main.
  4. Create the release: gh release create v<version> --generate-notes.

Pushes to main and PRs touching native/** build the targets too (no upload, no publish) to catch cross-compile breakage early.

License

MIT