From 466e14bcc9b8cd26f081d351a361b332a6a9bc1b Mon Sep 17 00:00:00 2001 From: Austen Adler Date: Mon, 2 Sep 2024 15:44:36 -0400 Subject: [PATCH] Add file watching --- Cargo.lock | 1 + Cargo.toml | 1 + src/main.rs | 119 +++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 96 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 54d877b..e43ac06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -238,6 +238,7 @@ dependencies = [ "anyhow", "atomicwrites", "clap", + "crossbeam-channel", "fjson", "notify-debouncer-mini", ] diff --git a/Cargo.toml b/Cargo.toml index 690fbde..66a37ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,6 @@ edition = "2021" anyhow = "1.0.86" atomicwrites = "0.4.3" clap = { version = "4.5.16", features = ["derive"] } +crossbeam-channel = "0.5.13" fjson = "0.3.1" notify-debouncer-mini = "0.4.1" diff --git a/src/main.rs b/src/main.rs index fcc33da..c09acd6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,11 @@ -use anyhow::{bail, Context, Result}; +use anyhow::{bail, Context, Error, Result}; use atomicwrites::{AtomicFile, OverwriteBehavior::AllowOverwrite}; use clap::{Args, Parser, Subcommand}; +use crossbeam_channel::{Receiver, Sender}; use notify_debouncer_mini::{new_debouncer, notify::*, DebounceEventResult}; use std::{ + collections::HashSet, + ffi::OsString, fs, io::Write, path::{Path, PathBuf}, @@ -25,6 +28,12 @@ enum Command { struct WatchArgs { path: PathBuf, + #[clap(short = 'e', long = "extension", default_values = ["jsonc", "jsoncc"])] + extensions: Vec, + + #[clap(short = 'r', long = "recursive")] + recursive: bool, + #[clap(short = 'I', long = "inplace")] inplace: bool, } @@ -83,7 +92,12 @@ fn main() -> Result<()> { if a.inplace && a.output.is_some() { bail!("Cannot format --inplace when --output is specified"); } - format_single_file(&a)?; + format_single_file( + &a.input, + a.jsonc_output().as_ref(), + a.json_output.as_ref(), + a.compact, + )?; } Command::Watch(a) => watch(&a)?, } @@ -91,11 +105,16 @@ fn main() -> Result<()> { Ok(()) } -fn format_single_file(args: &FmtArgs) -> Result<()> { - let input_str = fs::read_to_string(&args.input).context("Reading input")?; +fn format_single_file( + input: impl AsRef, + jsonc_output: Option<&JsoncOutput>, + json_output: Option>, + json_compact: bool, +) -> Result<()> { + let input_str = fs::read_to_string(&input).context("Reading input")?; // First, format jsonc - if let Some(jsonc_output) = args.jsonc_output() { + if let Some(jsonc_output) = jsonc_output { let output = fjson::to_jsonc(&input_str).context("Formatting to jsonc")?; match jsonc_output { @@ -107,8 +126,8 @@ fn format_single_file(args: &FmtArgs) -> Result<()> { } // Format json next - if let Some(ref json_output_file) = args.json_output { - let output = if args.compact { + if let Some(ref json_output_file) = json_output { + let output = if json_compact { fjson::to_json(&input_str).context("Formatting to json") } else { fjson::to_json_compact(&input_str).context("Formatting to json") @@ -123,29 +142,79 @@ fn format_single_file(args: &FmtArgs) -> Result<()> { } fn watch(args: &WatchArgs) -> Result<()> { - // Select recommended watcher for debouncer. - // Using a callback here, could also be a channel. - let mut debouncer = - new_debouncer( - Duration::from_millis(50), - |res: DebounceEventResult| match res { - Ok(events) => events - .iter() - .for_each(|e| println!("Event {:?} for {:?}", e.kind, e.path)), - Err(e) => println!("Error {:?}", e), - }, - ) - .context("Creating debouncer")?; + let (terminate_tx, terminate_rx): (Sender>, Receiver>) = + crossbeam_channel::bounded(100); + + let mut debouncer = new_debouncer( + Duration::from_millis(50), + move |res: DebounceEventResult| match res { + Ok(events) => events.into_iter().for_each(|evt| { + let _ = terminate_tx.send(Ok(evt.path)); + }), + Err(e) => { + let _ = terminate_tx.send(Err(>::into(e) + .context("Getting debounced result"))); + } + }, + ) + .context("Creating debouncer")?; - // Add a path to be watched. All files and directories at that path and - // below will be monitored for changes. debouncer .watcher() - .watch(Path::new("."), RecursiveMode::Recursive) + // TODO: Make this recursive or not + .watch( + Path::new(&args.path), + if args.recursive { + RecursiveMode::Recursive + } else { + RecursiveMode::NonRecursive + }, + ) .context("Adding watch to debouncer")?; - // note that dropping the debouncer (as will happen here) also ends the debouncer - // thus this demo would need an endless loop to keep running + let mut just_formatted = HashSet::new(); + while let Ok(evt) = terminate_rx.recv() { + match evt { + Ok(path) => { + if !(args + .extensions + .iter() + .any(|ext| (&path).extension() == Some(ext)) + || args.extensions.is_empty()) + { + // This extension doesn't match their requested extensions + continue; + } + + if just_formatted.remove(&path) { + // We just formatted it. This event came from us + continue; + } + + eprintln!("Got result: {path:#?}"); + + match format_single_file( + &path, + Some(&JsoncOutput::File(&path)), + None::<&Path>, + false, + ) { + Ok(()) => { + eprintln!("Formatted file {:?}", path); + just_formatted.insert(path); + } + Err(e) => { + eprintln!("Error formatting file {:?}: {e:?}", path); + } + } + } + Err(e) => { + eprintln!("Stopping watch because of error: {e:?}"); + } + } + } Ok(()) }