Add file watching
This commit is contained in:
parent
29dfd4b007
commit
466e14bcc9
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -238,6 +238,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"atomicwrites",
|
"atomicwrites",
|
||||||
"clap",
|
"clap",
|
||||||
|
"crossbeam-channel",
|
||||||
"fjson",
|
"fjson",
|
||||||
"notify-debouncer-mini",
|
"notify-debouncer-mini",
|
||||||
]
|
]
|
||||||
|
@ -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"
|
||||||
|
119
src/main.rs
119
src/main.rs
@ -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(())
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user