diff --git a/README.md b/README.md index 1625113..f981c22 100644 --- a/README.md +++ b/README.md @@ -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 @@ -12,10 +12,10 @@ zig build run- ### 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; diff --git a/examples/basic.zig b/examples/basic.zig index 575fac8..e68dbcf 100644 --- a/examples/basic.zig +++ b/examples/basic.zig @@ -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}), } } @@ -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}); diff --git a/examples/context.zig b/examples/context.zig index 049a764..4787dd0 100644 --- a/examples/context.zig +++ b/examples/context.zig @@ -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; diff --git a/src/watchers/interfaces.zig b/src/watchers/interfaces.zig index 403016e..30c57db 100644 --- a/src/watchers/interfaces.zig +++ b/src/watchers/interfaces.zig @@ -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 }; diff --git a/src/watchers/linux.zig b/src/watchers/linux.zig index b38e028..54cbc72 100644 --- a/src/watchers/linux.zig +++ b/src/watchers/linux.zig @@ -3,12 +3,15 @@ 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); @@ -16,24 +19,26 @@ pub const LinuxWatcher = struct { 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, ); @@ -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; } } @@ -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; @@ -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 + }); } } } diff --git a/src/watchers/macos.zig b/src/watchers/macos.zig index de600cd..c190820 100644 --- a/src/watchers/macos.zig +++ b/src/watchers/macos.zig @@ -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 + }); } } }