add packaging
This commit is contained in:
parent
019c576f1d
commit
41d4170ab9
9 changed files with 282 additions and 85 deletions
209
src/regar.ts
Executable file
209
src/regar.ts
Executable file
|
@ -0,0 +1,209 @@
|
|||
#!/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<Event>()
|
||||
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("<command> [...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 <extentions>", "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<Event>) {
|
||||
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<Event>) {
|
||||
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<boolean>
|
||||
}
|
||||
|
||||
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<C extends Constructible<C, any>, R = void> = [
|
||||
exception: C,
|
||||
strategy: ((exc: InstanceOf<C>) => R) | "ignore",
|
||||
]
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
function catch_<T, R, Cs extends Catching<any, R>[]>(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()
|
Loading…
Add table
Add a link
Reference in a new issue