std.watch

Filesystem events: inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows - one surface over all three.

import std.watch

w = watch.open("./inbox", { recursive: true })

loop {
    e = watch.next(w, 5000)          # { kind, path }, or null on timeout
    if is_null(e) { continue }
    if e.kind == "create" {
        print("ingesting {e.path}")
    }
}
watch.close(w)

Capability

Watching needs fs:read. Seeing what appears in a directory is reading it, so a package granted nothing cannot sidestep fs:read by watching instead of listing. See capabilities.

The handle

watch.open(path, opts?) returns a map:

fieldwhat it is
eventsa channel of events
paththe directory being watched
recursivewhether subdirectories are included
idthe watch's identity, used by close

opts takes { recursive: true }; the default is non-recursive.

Events

Each event is { kind, path }. kind is one of "create", "modify" or "remove" - watch.kinds() returns that list.

Platform-specific access events are dropped. They fire whenever anything reads a file, which would drown the signal you actually asked for.

Reading events

watch.next(w, timeout_ms) returns the next event, or null once the deadline passes. 0 polls and returns immediately.

The deadline is not optional by accident: a watch on a quiet directory must not be able to hang the program with no way out.

Because w.events is a real channel, a watch composes with everything else that speaks channels - drain it from a spawned task, or feed it straight into an SSE response:

async fn ingest(w, out) {
    loop {
        e = watch.next(w, 1000)
        unless is_null(e) { send(out, "data: {e.path}\n\n") }
    }
}

Closing

watch.close(w) stops the OS watch and closes the channel, so a reader parked on recv sees the end rather than waiting forever. Closing twice is harmless, which matters because a finally block will do exactly that.

Notes

Events are queued without bound. Blocking the notifier thread would drop events for every other watch in the process, so a burst is buffered rather than back-pressured. A consumer that never drains a busy directory will grow memory.

One logical change can produce several events. Writing a file often shows up as a create followed by one or more modifies, and editors that write atomically produce a rename. Match on what you care about rather than expecting one event per action.