#!/usr/bin/env -S deno run -A import { exists } from "https://deno.land/std@0.224.0/fs/exists.ts"; import { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.4/command/mod.ts"; import { Channel, Constructible, InstanceOf } from "https://git.barnulf.net/mb/barnulf_ts/raw/branch/master/mod.ts"; async function main() { const { do_clear, command, extensions, files, strategy } = await parse_args(); const channel = new Channel(); files.map((path) => new Watcher(path, extensions).spin(channel)); new Runner(command, strategy, do_clear).spin(channel); } async function parse_args() { const { args, options } = await new Command() .name("regar") .description("File watcher development utility.") .arguments(" [...files]") .option("-r, --hard", "Ends the child process by killing it.") .option("-s, --soft", "Ends the child process by sending an interuption and waiting for it to shut down.") .option("-w, --wait", "Wait for the child process to complete without terminating it. (default)") .option("-e, --extensions ", "Comma separated whitelist of extensions to whatch.") .option("-c, --clear", "Clear the terminal on restart").parse(); const [command, ...files] = args; let strategy: ShutdownStrategy = new WaitStrategy(); if (options.soft) strategy = new SoftStrategy(); if (options.hard) strategy = new HardStrategy(); if (options.wait) strategy = new WaitStrategy(); const extensions = options.extensions?.split(","); const do_clear = options.clear ?? false; return { command, files, strategy, extensions, do_clear }; } class Watcher { public constructor( private path: string, private extension_whitelist: string[] | undefined, ) {} public async spin(channel: Channel) { const relevant_events: Deno.FsEvent["kind"][] = ["create", "modify", "remove", "rename"]; for await (const event of Deno.watchFs(this.path, { recursive: true })) { if (!relevant_events.includes(event.kind)) continue; if (!this.matches_whitelist(event.paths[0])) continue; for (const path of event.paths) channel.send(new Event(path, Date.now())); } } private matches_whitelist(path: string) { if (this.extension_whitelist === undefined) return true; for (const ext of this.extension_whitelist) if (path.endsWith(ext)) return true; return false; } } class Runner { public constructor( private command: string, private strategy: ShutdownStrategy, private do_clear: boolean, ) {} public async spin(channel: Channel) { this.could_clear(); let process = this.launch(); while (true) { const event = await channel.receive(); if (this.debouncing(event)) continue; const shotdown = await this.strategy.shutdown(process); if (!shotdown) continue; this.could_clear(); log("changed", event.path); process = this.launch(); } } private launch() { const args = ["-c", this.command]; log("running", this.command); const process = new Deno.Command("sh", { args }).spawn(); process.status.then(this.on_end()); return process; } private could_clear() { const clear_term_sequence = "\x1bc"; if (this.do_clear) console.log(clear_term_sequence); } private last_event_date_ms = 0; private debouncing(event: Event) { const delay_ms = event.date_ms - this.last_event_date_ms; const threshold_ms = 100; if (delay_ms < threshold_ms) return true; this.last_event_date_ms = event.date_ms; return false; } private on_end() { const start_ms = Date.now(); return () => { const end_ms = Date.now(); const time_ms = end_ms - start_ms; const secs = time_ms / 1000; log("terminated", `${secs} s`); }; } } interface ShutdownStrategy { // Returns wether the child has been shutdown. shutdown(process_: Deno.ChildProcess): Promise; } class WaitStrategy implements ShutdownStrategy { async shutdown(process_: Deno.ChildProcess) { if (await process_exists(process_.pid)) return false; else return true; } } class SoftStrategy implements ShutdownStrategy { async shutdown(process_: Deno.ChildProcess) { try_signal(process_.pid, "SIGINT"); await process_.output(); return true; } } class HardStrategy implements ShutdownStrategy { async shutdown(process_: Deno.ChildProcess) { try_signal(process_.pid, "SIGKILL"); return await true; } } class Event { constructor( public path: string, public date_ms: number, ) {} } async function process_exists(pid: number) { return await exists(`/proc/${pid}`); } function try_signal(pid: number, sig: Deno.Signal) { catch_( () => Deno.kill(pid, sig), [Deno.errors.NotFound, "ignore"], ); } // deno-lint-ignore no-explicit-any type Catching, R = void> = [ exception: C, strategy: ((exc: InstanceOf) => R) | "ignore", ]; // deno-lint-ignore no-explicit-any function catch_[]>(op: () => T, ...catchings: Cs) { try { return op(); } catch (exception) { for (const [kind, strategy] of catchings) { if (!(exception instanceof kind)) continue; if (strategy === "ignore") return; return strategy(exception); } } } function log(verb: string, value: unknown) { const yellow = "\x1b[0;33m"; const bold = "\x1b[1m"; const reset = "\x1b[0m"; console.log( `` + `${yellow}${bold}${verb.padStart(12)}${reset}` + ` ` + `${yellow}${value}${reset}`, ); } if (import.meta.main) await main();