#!/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" import { wait } from "https://git.barnulf.net/mb/barnulf_ts/raw/branch/master/src/lib/utils.ts" const version = "1.0.0" async function main() { const { do_clear, command, extensions, files, strategy, go_up } = await parse_args() const channel = new Channel() files.map((path) => new Watcher(path, extensions).spin(channel)) new Runner(command, strategy, do_clear, go_up).spin(channel) } async function parse_args() { const cmd = new Command() .name("regar") .version(version) .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.", { default: false }) .option("-u, --up", "Scroll the terminal to the start of the command at each run.", { default: false }) .help({ colors: false }) const { args, options } = await cmd.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 const go_up = options.up return { command, files, strategy, extensions, do_clear, go_up } } 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, private go_up: 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) await wait(100) process = this.launch() } } private launch() { const args = ["-c", this.command] log("running", this.command) console.log() 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() { if (this.go_up) this.save_cursor() const start_ms = Date.now() return () => { const end_ms = Date.now() const time_ms = end_ms - start_ms const secs = time_ms / 1000 if (this.go_up) this.restore_cursor() log("terminated", `${secs} s`) } } private save_cursor() { console.log("\x1b[s") } private restore_cursor() { console.log("\x1b[u") } } 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 // trust // deno-lint-ignore no-explicit-any return strategy(exception as any) } } } 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()