diff --git a/Cargo.lock b/Cargo.lock index bededeb2..adc31129 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -139,21 +139,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" -[[package]] -name = "crossbeam-channel" -version = "0.5.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" - [[package]] name = "crossterm" version = "0.28.1" @@ -379,7 +364,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ "bitflags 2.6.0", - "crossbeam-channel", "filetime", "fsevent-sys", "inotify", @@ -391,16 +375,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "notify-debouncer-mini" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43" -dependencies = [ - "log", - "notify", -] - [[package]] name = "once_cell" version = "1.19.0" @@ -488,7 +462,7 @@ dependencies = [ "anyhow", "clap", "crossterm", - "notify-debouncer-mini", + "notify", "os_pipe", "rustix", "rustlings-macros", diff --git a/Cargo.toml b/Cargo.toml index 23dc4f65..f14b47a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ ahash = { version = "0.8.11", default-features = false } anyhow = "1.0.89" clap = { version = "4.5.17", features = ["derive"] } crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } -notify-debouncer-mini = { version = "0.4.1", default-features = false } +notify = { version = "6.1.1", default-features = false, features = ["macos_fsevent"] } os_pipe = "1.2.1" rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" } serde_json = "1.0.128" diff --git a/src/watch.rs b/src/watch.rs index c937bfbe..fd89b29e 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -1,12 +1,12 @@ use anyhow::{Error, Result}; -use notify_debouncer_mini::{ - new_debouncer, - notify::{self, RecursiveMode}, -}; +use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher}; use std::{ io::{self, Write}, path::Path, - sync::mpsc::channel, + sync::{ + atomic::{AtomicBool, Ordering::Relaxed}, + mpsc::channel, + }, time::Duration, }; @@ -21,6 +21,27 @@ mod notify_event; mod state; mod terminal_event; +static EXERCISE_RUNNING: AtomicBool = AtomicBool::new(false); + +// Private unit type to force using the constructor function. +#[must_use = "When the guard is dropped, the input is unpaused"] +pub struct InputPauseGuard(()); + +impl InputPauseGuard { + #[inline] + pub fn scoped_pause() -> Self { + EXERCISE_RUNNING.store(true, Relaxed); + Self(()) + } +} + +impl Drop for InputPauseGuard { + #[inline] + fn drop(&mut self) { + EXERCISE_RUNNING.store(false, Relaxed); + } +} + enum WatchEvent { Input(InputEvent), FileChange { exercise_ind: usize }, @@ -47,21 +68,21 @@ fn run_watch( let mut manual_run = false; // Prevent dropping the guard until the end of the function. // Otherwise, the file watcher exits. - let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { - let mut debouncer = new_debouncer( - Duration::from_millis(200), + let _watcher_guard = if let Some(exercise_names) = notify_exercise_names { + let mut watcher = RecommendedWatcher::new( NotifyEventHandler { sender: watch_event_sender.clone(), exercise_names, }, + Config::default().with_poll_interval(Duration::from_secs(1)), ) .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; - debouncer - .watcher() + + watcher .watch(Path::new("exercises"), RecursiveMode::Recursive) .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; - Some(debouncer) + Some(watcher) } else { manual_run = true; None diff --git a/src/watch/notify_event.rs b/src/watch/notify_event.rs index 9b235259..5ed8fd18 100644 --- a/src/watch/notify_event.rs +++ b/src/watch/notify_event.rs @@ -1,7 +1,10 @@ -use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; -use std::sync::mpsc::Sender; +use notify::{ + event::{MetadataKind, ModifyKind}, + Event, EventKind, +}; +use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender}; -use super::WatchEvent; +use super::{WatchEvent, EXERCISE_RUNNING}; pub struct NotifyEventHandler { pub sender: Sender, @@ -9,44 +12,56 @@ pub struct NotifyEventHandler { pub exercise_names: &'static [&'static [u8]], } -impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler { - fn handle_event(&mut self, input_event: DebounceEventResult) { - let output_event = match input_event { - Ok(input_event) => { - let Some(exercise_ind) = input_event - .iter() - .filter_map(|input_event| { - if input_event.kind != DebouncedEventKind::Any { - return None; - } +impl notify::EventHandler for NotifyEventHandler { + fn handle_event(&mut self, input_event: notify::Result) { + if EXERCISE_RUNNING.load(Relaxed) { + return; + } - let file_name = input_event.path.file_name()?.to_str()?.as_bytes(); - - if file_name.len() < 4 { - return None; - } - let (file_name_without_ext, ext) = file_name.split_at(file_name.len() - 3); - - if ext != b".rs" { - return None; - } - - self.exercise_names - .iter() - .position(|exercise_name| *exercise_name == file_name_without_ext) - }) - .min() - else { - return; - }; - - WatchEvent::FileChange { exercise_ind } + let input_event = match input_event { + Ok(v) => v, + Err(e) => { + // An error occurs when the receiver is dropped. + // After dropping the receiver, the debouncer guard should also be dropped. + let _ = self.sender.send(WatchEvent::NotifyErr(e)); + return; } - Err(e) => WatchEvent::NotifyErr(e), }; - // An error occurs when the receiver is dropped. - // After dropping the receiver, the debouncer guard should also be dropped. - let _ = self.sender.send(output_event); + match input_event.kind { + EventKind::Any => (), + EventKind::Modify(modify_kind) => match modify_kind { + ModifyKind::Any | ModifyKind::Data(_) => (), + ModifyKind::Metadata(metadata_kind) => match metadata_kind { + MetadataKind::Any | MetadataKind::WriteTime => (), + MetadataKind::AccessTime + | MetadataKind::Permissions + | MetadataKind::Ownership + | MetadataKind::Extended + | MetadataKind::Other => return, + }, + ModifyKind::Name(_) | ModifyKind::Other => return, + }, + EventKind::Access(_) + | EventKind::Create(_) + | EventKind::Remove(_) + | EventKind::Other => return, + } + + let _ = input_event + .paths + .into_iter() + .filter_map(|path| { + let file_name = path.file_name()?.to_str()?.as_bytes(); + + let [file_name_without_ext @ .., b'.', b'r', b's'] = file_name else { + return None; + }; + + self.exercise_names + .iter() + .position(|exercise_name| *exercise_name == file_name_without_ext) + }) + .try_for_each(|exercise_ind| self.sender.send(WatchEvent::FileChange { exercise_ind })); } } diff --git a/src/watch/state.rs b/src/watch/state.rs index 6e760018..8cccb40d 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -18,10 +18,7 @@ use crate::{ term::progress_bar, }; -use super::{ - terminal_event::{terminal_event_handler, InputPauseGuard}, - WatchEvent, -}; +use super::{terminal_event::terminal_event_handler, InputPauseGuard, WatchEvent}; #[derive(PartialEq, Eq)] enum DoneStatus { diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 2a1dfdcf..050c4acb 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -1,31 +1,7 @@ use crossterm::event::{self, Event, KeyCode, KeyEventKind}; -use std::sync::{ - atomic::{AtomicBool, Ordering::Relaxed}, - mpsc::Sender, -}; +use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender}; -use super::WatchEvent; - -static INPUT_PAUSED: AtomicBool = AtomicBool::new(false); - -// Private unit type to force using the constructor function. -#[must_use = "When the guard is dropped, the input is unpaused"] -pub struct InputPauseGuard(()); - -impl InputPauseGuard { - #[inline] - pub fn scoped_pause() -> Self { - INPUT_PAUSED.store(true, Relaxed); - Self(()) - } -} - -impl Drop for InputPauseGuard { - #[inline] - fn drop(&mut self) { - INPUT_PAUSED.store(false, Relaxed); - } -} +use super::{WatchEvent, EXERCISE_RUNNING}; pub enum InputEvent { Run, @@ -44,7 +20,7 @@ pub fn terminal_event_handler(sender: Sender, manual_run: bool) { KeyEventKind::Press => (), } - if INPUT_PAUSED.load(Relaxed) { + if EXERCISE_RUNNING.load(Relaxed) { continue; }