1
4
Fork
You've already forked zentry
0
Reliable file-watching library for Zig.
  • Zig 100%
Mikael Säker 921bc6b70e tree: portable inode cast (Windows i64 LARGE_INTEGER → u64)
`std.Io.File.Stat.inode` on Windows is `windows.LARGE_INTEGER`
(an i64), not u64 as on POSIX. Cross-compiling to x86_64-windows-gnu
failed in statPath with "expected type 'u64', found 'i64'".
We only use the inode for identity equality, so bitcasting i64→u64
preserves the bits. Comptime-select between identity (POSIX u64) and
@bitCast (Windows i64) so both targets compile clean.
Cross-build verified for x86_64-windows-gnu; native macOS tests
still green.
2026年05月19日 16:06:32 +02:00
src tree: portable inode cast (Windows i64 LARGE_INTEGER → u64) 2026年05月19日 16:06:32 +02:00
tests Dispatch FSEvents per-path instead of recrawling each watch root 2026年05月04日 15:37:13 +02:00
.gitignore Initial project scaffold: architecture, build system, and source stubs 2026年03月02日 00:57:20 +01:00
build.zig Dispatch FSEvents per-path instead of recrawling each watch root 2026年05月04日 15:37:13 +02:00
build.zig.zon Migrate to Zig 0.16 2026年04月30日 12:39:21 +02:00
CLAUDE.md Initial project scaffold: architecture, build system, and source stubs 2026年03月02日 00:57:20 +01:00
context.md Update DESIGN.md + prune context.md 2026年05月04日 15:49:15 +02:00
DESIGN.md Update DESIGN.md + prune context.md 2026年05月04日 15:49:15 +02:00
LICENSE Add MIT license 2026年03月02日 15:09:59 +01:00
README.md Update README for per-path dispatch 2026年05月04日 15:38:03 +02:00

zentry

Reliable file-watching library for Zig.

Raw OS file-watching APIs (inotify, FSEvents, kqueue) are unreliable — events can be missed, duplicated, or misleading. zentry solves this by maintaining an in-memory filesystem snapshot as the source of truth, validating every OS event against it. No changes are ever silently lost.

Inspired by filesentry (Rust).

Features

  • Three event types: create, delete, modified — simple and predictable
  • Snapshot-based reliability: every OS event is validated against an in-memory file tree
  • Per-path dispatch: each OS event is stat-and-compared individually against the snapshot, so callback latency is independent of the watched tree size (~30ms on a 4800-file recursive watch on M1 Pro)
  • Settle-time debouncing: events are batched until filesystem activity quiesces
  • Queue overflow recovery: if the OS watcher overflows, a full re-crawl diffs against the snapshot
  • Inode tracking: detects file replacements (same path, different inode)
  • Path filtering: built-in .git filter, or provide your own
  • Runtime root management: addRoot works both before and after start()
  • Platform backends: native FSEvents (macOS), inotify (Linux, planned), polling fallback (everywhere)

Requirements

  • Zig 0.16+

Usage

Add zentry as a dependency in your build.zig.zon:

.dependencies=.{.zentry=.{.url="https://codeberg.org/sicher/zentry/archive/<commit>.tar.gz",.hash="...",},},

Then in your build.zig:

constzentry_mod=b.dependency("zentry",.{.target=target,.optimize=optimize,}).module("zentry");exe_mod.addImport("zentry",zentry_mod);

Quick start

conststd=@import("std");constzentry=@import("zentry");fnonChange(events:[]constzentry.Event)void{for(events)|ev|{std.debug.print("{s}: {s}\n",.{@tagName(ev.type),ev.path.bytes});}}pubfnmain()!void{vargpa:std.heap.GeneralPurposeAllocator(.{})=.init;defer_=gpa.deinit();constallocator=gpa.allocator();// Bootstrap an Io implementation (Zig 0.16)varthreaded:std.Io.Threaded=.init(allocator,.{});deferthreaded.deinit();constio=threaded.io();varwatcher=tryzentry.Watcher.init(allocator,io,&onChange);deferwatcher.deinit();trywatcher.addRoot("/path/to/watch",true);// recursivetrywatcher.start();// ... do other work, events arrive via callback ...watcher.stop();}

Configuration

varwatcher=tryzentry.Watcher.init(allocator,io,&callback);// Settle time: how long to wait for activity to quiesce before// delivering events (default: 50ms)watcher.setSettleTime(100);// Poll interval: how often the polling fallback checks for changes// (default: 1000ms, ignored when using native backends)watcher.setPollInterval(500);// Path filter: skip paths you don't care about// Built-in: Filter.default (skips .git), Filter.none (skips nothing)watcher.setFilter(zentry.Filter.none);// Add roots before calling start()trywatcher.addRoot("/project/src",true);// recursivetrywatcher.addRoot("/project/config",false);// top-level onlytrywatcher.start();

Adding roots at runtime

addRoot also works after start(). Calls dispatch to the worker thread, which performs the initial crawl, then signal completion. The call blocks until the new root is fully registered, so errors (e.g. nonexistent path) are returned synchronously to the caller:

trywatcher.addRoot("/project/src",true);trywatcher.start();// ... later, on any thread ...trywatcher.addRoot("/project/extra",true);// returns once watched

Events

Events are delivered as a batch to your callback after the settle window expires:

fnonEvents(events:[]constzentry.Event)void{for(events)|ev|{switch(ev.type){.create=>{/*newfileordirectory*/},.delete=>{/*fileordirectoryremoved*/},.modified=>{/*contentchanged(mtime/sizediffer)*/},}constpath=ev.path.bytes;// absolute, normalized path}}

Renames are expressed as a delete + create pair. Tempfiles (created and deleted within the settle window) are merged and discarded — your callback never sees them.

Custom filter

constmy_filter=zentry.Filter{.ignore_fn=&struct{fnignore(_:?*constanyopaque,path:[]constu8,is_dir:bool)bool{_=is_dir;returnstd.mem.endsWith(u8,path,".tmp");}}.ignore,.context=null,};watcher.setFilter(my_filter);

Architecture

Watcher (public API)
 └─ Worker (background thread)
 ├─ Backend (FSEvents / inotify / poll)
 ├─ FileTree (in-memory snapshot)
 └─ EventDebouncer (merge/dedup)

The worker thread loops: wait for backend notification → for each changed path, stat-and-compare against the snapshot → debounce → deliver settled events via callback. New directories swept in via rename-into-watch trigger an eager subtree crawl so their pre-existing contents get tracked. The full-tree recrawl path is reserved for the rare case where the OS reports queue overflow.

Testing

zig build test # unit + integration + stress + perf
zig build perf # perf tests only (latency on deep+wide trees)

License

MIT