283 lines
10 KiB
Zig
283 lines
10 KiB
Zig
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 <path>]... <path> [<path> ...]
|
|
\\
|
|
\\The Watch never sleeps.
|
|
\\
|
|
\\Options:
|
|
\\ --ignore <path> Suppress events whose path exactly matches <path>.
|
|
\\ 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();
|
|
}
|
|
}
|