Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# fzwatch
A lightweight and cross-platform file watcher for your Zig projects.
> [!NOTE]
> [!NOTE]
> This project exists to support [fancy-cat](https://github.com/freref/fancy-cat) and has limited features.

## Instructions
Expand All @@ -12,10 +12,10 @@ zig build run-<filename>
### Usage
A basic example can be found under [examples](./examples/basic.zig). The API is defined as follows:
```zig
pub const Event = enum { modified };
pub const Event = struct { kind: enum { modified }, item: usize };
pub const Callback = fn (context: *anyopaque, event: Event) void;
pub const Opts = struct { latency: f16 = 1.0 };

pub fn init(allocator: std.mem.Allocator) !Watcher;
pub fn deinit(self: *Watcher) void;
pub fn addFile(self: *Watcher, path: []const u8) !void;
Expand Down
9 changes: 7 additions & 2 deletions examples/basic.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ const fzwatch = @import("fzwatch");

fn callback(context: ?*anyopaque, event: fzwatch.Event) void {
_ = context;
switch (event) {
.modified => std.debug.print("File was modified!\n", .{}),
switch (event.kind) {
.modified => std.debug.print("File {d} was modified!\n", .{event.item}),
}
}

Expand All @@ -19,6 +19,11 @@ pub fn main() !void {
defer watcher.deinit();

try watcher.addFile("README.md");
// try watcher.removeFile("README.md");
try watcher.addFile("build.zig");
// try watcher.removeFile("build.zig");
try watcher.addFile("build.zig.zon");
try watcher.removeFile("build.zig.zon");
watcher.setCallback(callback, null);

const thread = try std.Thread.spawn(.{}, watcherThread, .{&watcher});
Expand Down
2 changes: 1 addition & 1 deletion examples/context.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const Object = struct {
thread: ?std.Thread,

fn callback(context: ?*anyopaque, event: fzwatch.Event) void {
switch (event) {
switch (event.kind) {
.modified => {
const to_increment: *usize = @as(*usize, @ptrCast(@alignCast(context.?)));
to_increment.* += 1;
Expand Down
6 changes: 5 additions & 1 deletion src/watchers/interfaces.zig
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
pub const Event = enum { modified };
pub const Event = struct {
kind: enum { modified },
/// the index into `Watcher.paths.items` which this event came from
item: usize
};
pub const Callback = fn (context: ?*anyopaque, event: Event) void;
pub const Opts = struct { latency: f16 = 1.0 };
78 changes: 41 additions & 37 deletions src/watchers/linux.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,42 @@ const interfaces = @import("interfaces.zig");

pub const LinuxWatcher = struct {
allocator: std.mem.Allocator,
inotify_fd: i32,
paths: std.ArrayList([]const u8),
offset: usize,
inotify: struct {
fd: i32,
/// inotify wd offset from removing files
offset: usize,
},
callback: ?*const interfaces.Callback,
running: bool,
context: ?*anyopaque,
running: bool,

pub fn init(allocator: std.mem.Allocator) !LinuxWatcher {
const fd = try std.posix.inotify_init1(std.os.linux.IN.NONBLOCK);
errdefer std.posix.close(fd);

return LinuxWatcher{
.allocator = allocator,
.inotify_fd = @intCast(fd),
.paths = std.ArrayList([]const u8).init(allocator),
.offset = 1,
.inotify = .{
.fd = @intCast(fd),
.offset = 1,
},
.callback = null,
.running = false,
.context = null,
.running = false,
};
}

pub fn deinit(self: *LinuxWatcher) void {
self.stop();
self.paths.deinit();
std.posix.close(self.inotify_fd);
std.posix.close(self.inotify.fd);
}

pub fn addFile(self: *LinuxWatcher, path: []const u8) !void {
_ = try std.posix.inotify_add_watch(
self.inotify_fd,
self.inotify.fd,
path,
std.os.linux.IN.MODIFY,
);
Expand All @@ -42,13 +47,15 @@ pub const LinuxWatcher = struct {
}

pub fn removeFile(self: *LinuxWatcher, path: []const u8) !void {
for (0.., self.paths) |idx, mem_path| {
if (mem_path == path) {
_ = std.posix.inotify_rm_watch(self.inotify_fd, idx - self.offset);
try self.paths.items().remove(idx);
self.offset += 1;
return;
}
for (0.., self.paths.items) |idx, mem_path| {
if (!std.mem.eql(u8, mem_path, path))
continue;

_ = std.posix.inotify_rm_watch(self.inotify.fd, @intCast(idx + self.inotify.offset));
self.inotify.offset += 1;
_ = self.paths.orderedRemove(idx);

return;
}
}

Expand All @@ -62,10 +69,10 @@ pub const LinuxWatcher = struct {
if (self.paths.items.len == 0) return error.NoFilesToWatch;

self.running = true;
var buffer: [4096]u8 align(@alignOf(std.os.linux.inotify_event)) = undefined;
var buffer: [4096]std.os.linux.inotify_event = undefined;

while (self.running) {
const length = std.posix.read(self.inotify_fd, &buffer) catch |err| switch (err) {
const length = std.posix.read(self.inotify.fd, std.mem.sliceAsBytes(&buffer)) catch |err| switch (err) {
error.WouldBlock => {
std.time.sleep(@as(u64, @intFromFloat(@as(f64, opts.latency) * @as(f64, @floatFromInt(std.time.ns_per_s)))));
continue;
Expand All @@ -75,30 +82,27 @@ pub const LinuxWatcher = struct {
},
};

var ptr: [*]u8 = &buffer;
const end_ptr = ptr + @as(usize, @intCast(length));
// in bytes
var i: usize = 0;
while (i < length) : (i += buffer[i].len + @sizeOf(std.os.linux.inotify_event)) {
const ev = buffer[i];

if(ev.wd < self.inotify.offset) {continue;}
else if (ev.wd > self.paths.items.len + self.inotify.offset)
return error.InvalidWatchDescriptor;

if (ev.mask & std.os.linux.IN.IGNORED == 0 and ev.mask & std.os.linux.IN.MODIFY == 0)
continue;

while (@intFromPtr(ptr) < @intFromPtr(end_ptr)) {
const ev = @as(*const std.os.linux.inotify_event, @ptrCast(@alignCast(ptr)));
const index = @as(usize, @intCast(@max(0, ev.wd))) - self.inotify.offset;
// Editors like vim create temporary files when saving
// So we have to re-add the file to the watcher
if (ev.mask & std.os.linux.IN.IGNORED != 0) {
const wd_usize = @as(usize, @intCast(@max(0, ev.wd)));
if (wd_usize < self.offset) {
return error.InvalidWatchDescriptor;
}
const index = wd_usize - self.offset;
if(ev.mask & std.os.linux.IN.IGNORED != 0)
try self.addFile(self.paths.items[index]);
if (self.callback) |callback| {
callback(self.context, interfaces.Event.modified);
}
} else if (ev.mask & std.os.linux.IN.MODIFY != 0) {
if (self.callback) |callback| {
callback(self.context, interfaces.Event.modified);
}
}

ptr = @alignCast(ptr + @sizeOf(std.os.linux.inotify_event) + ev.len);
if (self.callback) |callback| callback(self.context, .{
.kind = .modified,
.item = index
});
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/watchers/macos.zig
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ pub const MacosWatcher = struct {
while (i < numEvents) : (i += 1) {
const flags = eventFlags[i];
if (flags & c.kFSEventStreamEventFlagItemModified != 0) {
self.callback.?(self.context, interfaces.Event.modified);
self.callback.?(self.context, .{
.kind = .modified,
.item = i
});
}
}
}
Expand Down