Add file watching

This commit is contained in:
Austen Adler 2024-09-02 15:44:36 -04:00
parent 29dfd4b007
commit 466e14bcc9
3 changed files with 96 additions and 25 deletions

1
Cargo.lock generated
View File

@ -238,6 +238,7 @@ dependencies = [
"anyhow", "anyhow",
"atomicwrites", "atomicwrites",
"clap", "clap",
"crossbeam-channel",
"fjson", "fjson",
"notify-debouncer-mini", "notify-debouncer-mini",
] ]

View File

@ -7,5 +7,6 @@ edition = "2021"
anyhow = "1.0.86" anyhow = "1.0.86"
atomicwrites = "0.4.3" atomicwrites = "0.4.3"
clap = { version = "4.5.16", features = ["derive"] } clap = { version = "4.5.16", features = ["derive"] }
crossbeam-channel = "0.5.13"
fjson = "0.3.1" fjson = "0.3.1"
notify-debouncer-mini = "0.4.1" notify-debouncer-mini = "0.4.1"

View File

@ -1,8 +1,11 @@
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Error, Result};
use atomicwrites::{AtomicFile, OverwriteBehavior::AllowOverwrite}; use atomicwrites::{AtomicFile, OverwriteBehavior::AllowOverwrite};
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use crossbeam_channel::{Receiver, Sender};
use notify_debouncer_mini::{new_debouncer, notify::*, DebounceEventResult}; use notify_debouncer_mini::{new_debouncer, notify::*, DebounceEventResult};
use std::{ use std::{
collections::HashSet,
ffi::OsString,
fs, fs,
io::Write, io::Write,
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -25,6 +28,12 @@ enum Command {
struct WatchArgs { struct WatchArgs {
path: PathBuf, path: PathBuf,
#[clap(short = 'e', long = "extension", default_values = ["jsonc", "jsoncc"])]
extensions: Vec<OsString>,
#[clap(short = 'r', long = "recursive")]
recursive: bool,
#[clap(short = 'I', long = "inplace")] #[clap(short = 'I', long = "inplace")]
inplace: bool, inplace: bool,
} }
@ -83,7 +92,12 @@ fn main() -> Result<()> {
if a.inplace && a.output.is_some() { if a.inplace && a.output.is_some() {
bail!("Cannot format --inplace when --output is specified"); 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)?, Command::Watch(a) => watch(&a)?,
} }
@ -91,11 +105,16 @@ fn main() -> Result<()> {
Ok(()) Ok(())
} }
fn format_single_file(args: &FmtArgs) -> Result<()> { fn format_single_file(
let input_str = fs::read_to_string(&args.input).context("Reading input")?; input: impl AsRef<Path>,
jsonc_output: Option<&JsoncOutput>,
json_output: Option<impl AsRef<Path>>,
json_compact: bool,
) -> Result<()> {
let input_str = fs::read_to_string(&input).context("Reading input")?;
// First, format jsonc // 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")?; let output = fjson::to_jsonc(&input_str).context("Formatting to jsonc")?;
match jsonc_output { match jsonc_output {
@ -107,8 +126,8 @@ fn format_single_file(args: &FmtArgs) -> Result<()> {
} }
// Format json next // Format json next
if let Some(ref json_output_file) = args.json_output { if let Some(ref json_output_file) = json_output {
let output = if args.compact { let output = if json_compact {
fjson::to_json(&input_str).context("Formatting to json") fjson::to_json(&input_str).context("Formatting to json")
} else { } else {
fjson::to_json_compact(&input_str).context("Formatting to json") 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<()> { fn watch(args: &WatchArgs) -> Result<()> {
// Select recommended watcher for debouncer. let (terminate_tx, terminate_rx): (Sender<Result<PathBuf>>, Receiver<Result<PathBuf>>) =
// Using a callback here, could also be a channel. crossbeam_channel::bounded(100);
let mut debouncer =
new_debouncer( let mut debouncer = new_debouncer(
Duration::from_millis(50), Duration::from_millis(50),
|res: DebounceEventResult| match res { move |res: DebounceEventResult| match res {
Ok(events) => events Ok(events) => events.into_iter().for_each(|evt| {
.iter() let _ = terminate_tx.send(Ok(evt.path));
.for_each(|e| println!("Event {:?} for {:?}", e.kind, e.path)), }),
Err(e) => println!("Error {:?}", e), Err(e) => {
}, let _ = terminate_tx.send(Err(<notify_debouncer_mini::notify::Error as Into<
) Error,
.context("Creating debouncer")?; >>::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 debouncer
.watcher() .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")?; .context("Adding watch to debouncer")?;
// note that dropping the debouncer (as will happen here) also ends the debouncer let mut just_formatted = HashSet::new();
// thus this demo would need an endless loop to keep running 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(()) Ok(())
} }