Compare commits

...

3 commits

Author SHA1 Message Date
mo8it 4e4b65711a Only handle file changes for the current exercise, no jumping back 2024-09-18 01:44:13 +02:00
mo8it 89c40ba256 Optimize the file watcher 2024-09-18 01:43:48 +02:00
mo8it e56ae6d651 Update deps 2024-09-17 23:33:48 +02:00
6 changed files with 98 additions and 119 deletions

36
Cargo.lock generated
View file

@ -65,9 +65,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.88" version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
@ -139,21 +139,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 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]] [[package]]
name = "crossterm" name = "crossterm"
version = "0.28.1" version = "0.28.1"
@ -379,7 +364,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"crossbeam-channel",
"filetime", "filetime",
"fsevent-sys", "fsevent-sys",
"inotify", "inotify",
@ -391,16 +375,6 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.19.0" version = "1.19.0"
@ -488,7 +462,7 @@ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"crossterm", "crossterm",
"notify-debouncer-mini", "notify",
"os_pipe", "os_pipe",
"rustix", "rustix",
"rustlings-macros", "rustlings-macros",
@ -646,9 +620,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.20" version = "0.22.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",

View file

@ -20,7 +20,7 @@ rust-version = "1.80"
[workspace.dependencies] [workspace.dependencies]
serde = { version = "1.0.210", features = ["derive"] } serde = { version = "1.0.210", features = ["derive"] }
toml_edit = { version = "0.22.20", default-features = false, features = ["parse", "serde"] } toml_edit = { version = "0.22.21", default-features = false, features = ["parse", "serde"] }
[package] [package]
name = "rustlings" name = "rustlings"
@ -47,10 +47,10 @@ include = [
[dependencies] [dependencies]
ahash = { version = "0.8.11", default-features = false } ahash = { version = "0.8.11", default-features = false }
anyhow = "1.0.88" anyhow = "1.0.89"
clap = { version = "4.5.17", features = ["derive"] } clap = { version = "4.5.17", features = ["derive"] }
crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } 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" os_pipe = "1.2.1"
rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" } rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" }
serde_json = "1.0.128" serde_json = "1.0.128"

View file

@ -1,12 +1,12 @@
use anyhow::{Error, Result}; use anyhow::{Error, Result};
use notify_debouncer_mini::{ use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
new_debouncer,
notify::{self, RecursiveMode},
};
use std::{ use std::{
io::{self, Write}, io::{self, Write},
path::Path, path::Path,
sync::mpsc::channel, sync::{
atomic::{AtomicBool, Ordering::Relaxed},
mpsc::channel,
},
time::Duration, time::Duration,
}; };
@ -21,6 +21,27 @@ mod notify_event;
mod state; mod state;
mod terminal_event; 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 { enum WatchEvent {
Input(InputEvent), Input(InputEvent),
FileChange { exercise_ind: usize }, FileChange { exercise_ind: usize },
@ -47,21 +68,21 @@ fn run_watch(
let mut manual_run = false; let mut manual_run = false;
// Prevent dropping the guard until the end of the function. // Prevent dropping the guard until the end of the function.
// Otherwise, the file watcher exits. // Otherwise, the file watcher exits.
let _debouncer_guard = if let Some(exercise_names) = notify_exercise_names { let _watcher_guard = if let Some(exercise_names) = notify_exercise_names {
let mut debouncer = new_debouncer( let mut watcher = RecommendedWatcher::new(
Duration::from_millis(200),
NotifyEventHandler { NotifyEventHandler {
sender: watch_event_sender.clone(), sender: watch_event_sender.clone(),
exercise_names, exercise_names,
}, },
Config::default().with_poll_interval(Duration::from_secs(1)),
) )
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
debouncer
.watcher() watcher
.watch(Path::new("exercises"), RecursiveMode::Recursive) .watch(Path::new("exercises"), RecursiveMode::Recursive)
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
Some(debouncer) Some(watcher)
} else { } else {
manual_run = true; manual_run = true;
None None

View file

@ -1,7 +1,10 @@
use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind}; use notify::{
use std::sync::mpsc::Sender; 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 struct NotifyEventHandler {
pub sender: Sender<WatchEvent>, pub sender: Sender<WatchEvent>,
@ -9,44 +12,56 @@ pub struct NotifyEventHandler {
pub exercise_names: &'static [&'static [u8]], pub exercise_names: &'static [&'static [u8]],
} }
impl notify_debouncer_mini::DebounceEventHandler for NotifyEventHandler { impl notify::EventHandler for NotifyEventHandler {
fn handle_event(&mut self, input_event: DebounceEventResult) { fn handle_event(&mut self, input_event: notify::Result<Event>) {
let output_event = match input_event { if EXERCISE_RUNNING.load(Relaxed) {
Ok(input_event) => { return;
let Some(exercise_ind) = input_event
.iter()
.filter_map(|input_event| {
if input_event.kind != DebouncedEventKind::Any {
return None;
} }
let file_name = input_event.path.file_name()?.to_str()?.as_bytes(); let input_event = match input_event {
Ok(v) => v,
if file_name.len() < 4 { Err(e) => {
return None; // 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;
} }
let (file_name_without_ext, ext) = file_name.split_at(file_name.len() - 3); };
if ext != b".rs" { match input_event.kind {
return None; 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 self.exercise_names
.iter() .iter()
.position(|exercise_name| *exercise_name == file_name_without_ext) .position(|exercise_name| *exercise_name == file_name_without_ext)
}) })
.min() .try_for_each(|exercise_ind| self.sender.send(WatchEvent::FileChange { exercise_ind }));
else {
return;
};
WatchEvent::FileChange { exercise_ind }
}
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);
} }
} }

View file

@ -18,10 +18,7 @@ use crate::{
term::progress_bar, term::progress_bar,
}; };
use super::{ use super::{terminal_event::terminal_event_handler, InputPauseGuard, WatchEvent};
terminal_event::{terminal_event_handler, InputPauseGuard},
WatchEvent,
};
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq)]
enum DoneStatus { enum DoneStatus {
@ -103,14 +100,10 @@ impl<'a> WatchState<'a> {
exercise_ind: usize, exercise_ind: usize,
stdout: &mut StdoutLock, stdout: &mut StdoutLock,
) -> Result<()> { ) -> Result<()> {
// Don't skip exercises on file changes to avoid confusion from missing exercises. if self.app_state.current_exercise_ind() != exercise_ind {
// Skipping exercises must be explicit in the interactive list.
// But going back to an earlier exercise on file change is fine.
if self.app_state.current_exercise_ind() < exercise_ind {
return Ok(()); return Ok(());
} }
self.app_state.set_current_exercise_ind(exercise_ind)?;
self.run_current_exercise(stdout) self.run_current_exercise(stdout)
} }

View file

@ -1,31 +1,7 @@
use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use std::sync::{ use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender};
atomic::{AtomicBool, Ordering::Relaxed},
mpsc::Sender,
};
use super::WatchEvent; use super::{WatchEvent, EXERCISE_RUNNING};
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);
}
}
pub enum InputEvent { pub enum InputEvent {
Run, Run,
@ -44,7 +20,7 @@ pub fn terminal_event_handler(sender: Sender<WatchEvent>, manual_run: bool) {
KeyEventKind::Press => (), KeyEventKind::Press => (),
} }
if INPUT_PAUSED.load(Relaxed) { if EXERCISE_RUNNING.load(Relaxed) {
continue; continue;
} }