const std = @import("std"); const builtin = @import("builtin"); const nightwatch = @import("nightwatch"); const Watcher = switch (builtin.os.tag) { .linux => nightwatch.Create(.polling), else => nightwatch.Default, }; const is_posix = switch (builtin.os.tag) { .linux, .macos, .freebsd, .openbsd, .netbsd, .dragonfly => true, .windows => false, else => @compileError("unsupported OS"), }; // Self-pipe: signal handler writes a byte so poll() / read() unblocks cleanly. var sig_pipe: if (is_posix) [2]std.posix.fd_t else void = undefined; fn posix_sighandler(_: c_int) callconv(.c) void { _ = std.posix.write(sig_pipe[1], &[_]u8{0}) catch {}; } const CliHandler = struct { handler: Watcher.Handler, out: std.fs.File, tty: std.io.tty.Config, ignore: []const []const u8, const vtable: Watcher.Handler.VTable = switch (Watcher.interface_type) { .polling => .{ .change = change_cb, .rename = rename_cb, .wait_readable = wait_readable_cb, }, .threaded => .{ .change = change_cb, .rename = rename_cb, }, }; fn change_cb(h: *Watcher.Handler, path: []const u8, event_type: nightwatch.EventType, object_type: nightwatch.ObjectType) error{HandlerFailed}!void { const self: *CliHandler = @fieldParentPtr("handler", h); for (self.ignore) |ignored| { if (std.mem.eql(u8, path, ignored)) return; } var buf: [4096]u8 = undefined; var w = self.out.writer(&buf); defer w.interface.flush() catch {}; const color: std.io.tty.Color = switch (event_type) { .created => .green, .modified => .blue, .deleted => .red, .renamed => .magenta, }; const event_label = switch (event_type) { .created => "create ", .modified => "modify ", .deleted => "delete ", .renamed => "rename ", }; self.tty.setColor(&w.interface, color) catch return error.HandlerFailed; w.interface.writeAll(event_label) catch return error.HandlerFailed; self.tty.setColor(&w.interface, .reset) catch return error.HandlerFailed; w.interface.writeAll(" ") catch return error.HandlerFailed; self.writeTypeLabel(&w.interface, object_type) catch return error.HandlerFailed; w.interface.print(" {s}\n", .{path}) catch return error.HandlerFailed; } fn rename_cb(h: *Watcher.Handler, src: []const u8, dst: []const u8, object_type: nightwatch.ObjectType) error{HandlerFailed}!void { const self: *CliHandler = @fieldParentPtr("handler", h); for (self.ignore) |ignored| { if (std.mem.eql(u8, src, ignored) or std.mem.eql(u8, dst, ignored)) return; } var buf: [4096]u8 = undefined; var w = self.out.writer(&buf); defer w.interface.flush() catch {}; self.tty.setColor(&w.interface, .magenta) catch return error.HandlerFailed; w.interface.writeAll("rename ") catch return error.HandlerFailed; self.tty.setColor(&w.interface, .reset) catch return error.HandlerFailed; w.interface.writeAll(" ") catch return error.HandlerFailed; self.writeTypeLabel(&w.interface, object_type) catch return error.HandlerFailed; w.interface.print(" {s} -> {s}\n", .{ src, dst }) catch return error.HandlerFailed; } fn writeTypeLabel(self: *CliHandler, w: *std.io.Writer, object_type: nightwatch.ObjectType) !void { switch (object_type) { .file => { try self.tty.setColor(w, .cyan); try w.writeAll("file"); try self.tty.setColor(w, .reset); }, .dir => { try self.tty.setColor(w, .yellow); try w.writeAll("dir "); try self.tty.setColor(w, .reset); }, .unknown => try w.writeAll("? "), } } fn wait_readable_cb(_: *Watcher.Handler) error{HandlerFailed}!Watcher.Handler.ReadableStatus { return .will_notify; } }; fn run_linux(watcher: *Watcher) !void { var fds = [_]std.posix.pollfd{ .{ .fd = watcher.poll_fd(), .events = std.posix.POLL.IN, .revents = 0 }, .{ .fd = sig_pipe[0], .events = std.posix.POLL.IN, .revents = 0 }, }; while (true) { _ = try std.posix.poll(&fds, -1); if (fds[1].revents & std.posix.POLL.IN != 0) return; // signal if (fds[0].revents & std.posix.POLL.IN != 0) { watcher.handle_read_ready() catch return; } } } fn run_posix() void { // Backend (kqueue) drives its own thread; we just block until signal. var buf: [1]u8 = undefined; _ = std.posix.read(sig_pipe[0], &buf) catch {}; } var win_shutdown = std.atomic.Value(bool).init(false); fn win_ctrl_handler(ctrl_type: std.os.windows.DWORD) callconv(.winapi) std.os.windows.BOOL { _ = ctrl_type; win_shutdown.store(true, .release); return std.os.windows.TRUE; } fn run_windows() void { const SetConsoleCtrlHandler = struct { extern "kernel32" fn SetConsoleCtrlHandler( HandlerRoutine: ?*const fn (std.os.windows.DWORD) callconv(.winapi) std.os.windows.BOOL, Add: std.os.windows.BOOL, ) callconv(.winapi) std.os.windows.BOOL; }.SetConsoleCtrlHandler; _ = SetConsoleCtrlHandler(win_ctrl_handler, std.os.windows.TRUE); while (!win_shutdown.load(.acquire)) { std.Thread.sleep(50 * std.time.ns_per_ms); } } fn usage(out: std.fs.File) !void { var buf: [4096]u8 = undefined; var writer = out.writer(&buf); try writer.interface.print( \\Usage: nightwatch [--ignore ]... [ ...] \\ \\The Watch never sleeps. \\ \\Options: \\ --ignore Suppress events whose path exactly matches . \\ May be specified multiple times. \\ \\Events printed to stdout (columns: event type path): \\ create a file or directory was created \\ modify a file was modified \\ delete a file or directory was deleted \\ rename a file or directory was renamed \\ \\Type column: file, dir, or ? (unknown) \\ \\Stand down with Ctrl-C. \\ , .{}); try writer.interface.flush(); } pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); if (args.len < 2) { try usage(std.fs.File.stderr()); std.process.exit(1); } if (std.mem.eql(u8, args[1], "-h") or std.mem.eql(u8, args[1], "--help")) { try usage(std.fs.File.stdout()); return; } var buf: [4096]u8 = undefined; var stderr = std.fs.File.stderr().writer(&buf); defer stderr.interface.flush() catch {}; var out_buf: [4096]u8 = undefined; var stdout = std.fs.File.stdout().writer(&out_buf); defer stdout.interface.flush() catch {}; // Parse --ignore options and watch paths. // Ignored paths are made absolute (without resolving symlinks) so they // match the absolute paths the backend emits in event callbacks. var ignore_list = std.ArrayListUnmanaged([]const u8){}; defer { for (ignore_list.items) |p| allocator.free(p); ignore_list.deinit(allocator); } var watch_paths = std.ArrayListUnmanaged([]const u8){}; defer watch_paths.deinit(allocator); var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; const cwd = try std.fs.cwd().realpath(".", &cwd_buf); var i: usize = 1; while (i < args.len) : (i += 1) { if (std.mem.eql(u8, args[i], "--ignore")) { i += 1; if (i >= args.len) { try stderr.interface.print("nightwatch: --ignore requires an argument\n", .{}); std.process.exit(1); } const raw = args[i]; const abs = if (std.fs.path.isAbsolute(raw)) try allocator.dupe(u8, raw) else try std.fs.path.join(allocator, &.{ cwd, raw }); try ignore_list.append(allocator, abs); } else { try watch_paths.append(allocator, args[i]); } } if (watch_paths.items.len == 0) { try usage(std.fs.File.stderr()); std.process.exit(1); } if (is_posix) { sig_pipe = try std.posix.pipe(); const sa = std.posix.Sigaction{ .handler = .{ .handler = posix_sighandler }, .mask = std.posix.sigemptyset(), .flags = 0, }; std.posix.sigaction(std.posix.SIG.INT, &sa, null); std.posix.sigaction(std.posix.SIG.TERM, &sa, null); } defer if (is_posix) { std.posix.close(sig_pipe[0]); std.posix.close(sig_pipe[1]); }; var cli_handler = CliHandler{ .handler = .{ .vtable = &CliHandler.vtable }, .out = std.fs.File.stdout(), .tty = std.io.tty.detectConfig(std.fs.File.stdout()), .ignore = ignore_list.items, }; var watcher = switch (builtin.os.tag) { .linux => try nightwatch.Create(.polling).init(allocator, &cli_handler.handler), else => try nightwatch.Default.init(allocator, &cli_handler.handler), }; defer watcher.deinit(); for (watch_paths.items) |path| { watcher.watch(path) catch |err| { try stderr.interface.print("nightwatch: {s}: {s}\n", .{ path, @errorName(err) }); continue; }; try cli_handler.tty.setColor(&stdout.interface, .dim); try stdout.interface.print("# on watch: {s}", .{path}); try cli_handler.tty.setColor(&stdout.interface, .reset); try stdout.interface.print("\n", .{}); try stdout.interface.flush(); } if (Watcher.interface_type == .polling) { try run_linux(&watcher); } else if (builtin.os.tag == .windows) { run_windows(); } else if (is_posix) { run_posix(); } }