Compare commits

...

4 commits

Author SHA1 Message Date
mo8it 0c79f2ea3e Reset in prompt with confirmation 2024-09-26 18:15:45 +02:00
mo8it 0e9eb9e87e Replace three dots with dot in hint 2024-09-26 18:05:05 +02:00
mo8it 0d258b9e96 Update deps 2024-09-26 12:28:48 +02:00
mo8it d4fa61e435 Debounce file change events 2024-09-26 12:26:24 +02:00
7 changed files with 190 additions and 51 deletions

32
Cargo.lock generated
View file

@ -95,9 +95,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.17" version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -105,9 +105,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.17" version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -117,9 +117,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.13" version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@ -289,9 +289,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.158" version = "0.2.159"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
[[package]] [[package]]
name = "libredox" name = "libredox"
@ -434,9 +434,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.4" version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
] ]
@ -536,9 +536,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.7" version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -620,9 +620,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.21" version = "0.22.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b072cee73c449a636ffd6f32bd8de3a9f7119139aff882f44943ce2986dc5cf" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -846,9 +846,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.18" version = "0.6.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

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.21", default-features = false, features = ["parse", "serde"] } toml_edit = { version = "0.22.22", default-features = false, features = ["parse", "serde"] }
[package] [package]
name = "rustlings" name = "rustlings"
@ -48,7 +48,7 @@ include = [
[dependencies] [dependencies]
ahash = { version = "0.8.11", default-features = false } ahash = { version = "0.8.11", default-features = false }
anyhow = "1.0.89" anyhow = "1.0.89"
clap = { version = "4.5.17", features = ["derive"] } clap = { version = "4.5.18", 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 = { version = "6.1.1", default-features = false, features = ["macos_fsevent"] } notify = { version = "6.1.1", default-features = false, features = ["macos_fsevent"] }
os_pipe = "1.2.1" os_pipe = "1.2.1"

View file

@ -1144,7 +1144,7 @@ constants, but clippy recognizes those imprecise mathematical constants as a
source of potential error. source of potential error.
See the suggestions of the Clippy warning in the compile output and use the See the suggestions of the Clippy warning in the compile output and use the
appropriate replacement constant from `std::f32::consts`...""" appropriate replacement constant from `std::f32::consts`."""
[[exercises]] [[exercises]]
name = "clippy2" name = "clippy2"

View file

@ -69,11 +69,11 @@ fn run_watch(
// 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 _watcher_guard = if let Some(exercise_names) = notify_exercise_names { let _watcher_guard = if let Some(exercise_names) = notify_exercise_names {
let notify_event_handler =
NotifyEventHandler::build(watch_event_sender.clone(), exercise_names)?;
let mut watcher = RecommendedWatcher::new( let mut watcher = RecommendedWatcher::new(
NotifyEventHandler { notify_event_handler,
sender: watch_event_sender.clone(),
exercise_names,
},
Config::default().with_poll_interval(Duration::from_secs(1)), Config::default().with_poll_interval(Duration::from_secs(1)),
) )
.inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?; .inspect_err(|_| eprintln!("{NOTIFY_ERR}"))?;
@ -100,13 +100,14 @@ fn run_watch(
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?, ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
ExercisesProgress::CurrentPending => (), ExercisesProgress::CurrentPending => (),
}, },
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?,
WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?, WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?,
WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List), WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List),
WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?,
WatchEvent::Input(InputEvent::Quit) => { WatchEvent::Input(InputEvent::Quit) => {
stdout.write_all(QUIT_MSG)?; stdout.write_all(QUIT_MSG)?;
break; break;
} }
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?,
WatchEvent::FileChange { exercise_ind } => { WatchEvent::FileChange { exercise_ind } => {
watch_state.handle_file_change(exercise_ind, &mut stdout)?; watch_state.handle_file_change(exercise_ind, &mut stdout)?;
} }

View file

@ -1,15 +1,71 @@
use anyhow::{Context, Result};
use notify::{ use notify::{
event::{MetadataKind, ModifyKind}, event::{AccessKind, AccessMode, MetadataKind, ModifyKind, RenameMode},
Event, EventKind, Event, EventKind,
}; };
use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender}; use std::{
sync::{
atomic::Ordering::Relaxed,
mpsc::{sync_channel, RecvTimeoutError, Sender, SyncSender},
},
thread,
time::Duration,
};
use super::{WatchEvent, EXERCISE_RUNNING}; use super::{WatchEvent, EXERCISE_RUNNING};
const DEBOUNCE_DURATION: Duration = Duration::from_millis(200);
pub struct NotifyEventHandler { pub struct NotifyEventHandler {
pub sender: Sender<WatchEvent>, error_sender: Sender<WatchEvent>,
/// Used to report which exercise was modified. // Sends the index of the updated exercise.
pub exercise_names: &'static [&'static [u8]], update_sender: SyncSender<usize>,
// Used to report which exercise was modified.
exercise_names: &'static [&'static [u8]],
}
impl NotifyEventHandler {
pub fn build(
watch_event_sender: Sender<WatchEvent>,
exercise_names: &'static [&'static [u8]],
) -> Result<Self> {
let (update_sender, update_receiver) = sync_channel(0);
let error_sender = watch_event_sender.clone();
// Debouncer
thread::Builder::new()
.spawn(move || {
let mut exercise_updated = vec![false; exercise_names.len()];
loop {
match update_receiver.recv_timeout(DEBOUNCE_DURATION) {
Ok(exercise_ind) => exercise_updated[exercise_ind] = true,
Err(RecvTimeoutError::Timeout) => {
for (exercise_ind, updated) in exercise_updated.iter_mut().enumerate() {
if *updated {
if watch_event_sender
.send(WatchEvent::FileChange { exercise_ind })
.is_err()
{
break;
}
*updated = false;
}
}
}
Err(RecvTimeoutError::Disconnected) => break,
}
}
})
.context("Failed to spawn a thread to debounce file changes")?;
Ok(Self {
error_sender,
update_sender,
exercise_names,
})
}
} }
impl notify::EventHandler for NotifyEventHandler { impl notify::EventHandler for NotifyEventHandler {
@ -22,8 +78,8 @@ impl notify::EventHandler for NotifyEventHandler {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
// An error occurs when the receiver is dropped. // An error occurs when the receiver is dropped.
// After dropping the receiver, the debouncer guard should also be dropped. // After dropping the receiver, the watcher guard should also be dropped.
let _ = self.sender.send(WatchEvent::NotifyErr(e)); let _ = self.error_sender.send(WatchEvent::NotifyErr(e));
return; return;
} }
}; };
@ -32,6 +88,10 @@ impl notify::EventHandler for NotifyEventHandler {
EventKind::Any => (), EventKind::Any => (),
EventKind::Modify(modify_kind) => match modify_kind { EventKind::Modify(modify_kind) => match modify_kind {
ModifyKind::Any | ModifyKind::Data(_) => (), ModifyKind::Any | ModifyKind::Data(_) => (),
ModifyKind::Name(rename_mode) => match rename_mode {
RenameMode::Any | RenameMode::To => (),
RenameMode::From | RenameMode::Both | RenameMode::Other => return,
},
ModifyKind::Metadata(metadata_kind) => match metadata_kind { ModifyKind::Metadata(metadata_kind) => match metadata_kind {
MetadataKind::Any | MetadataKind::WriteTime => (), MetadataKind::Any | MetadataKind::WriteTime => (),
MetadataKind::AccessTime MetadataKind::AccessTime
@ -40,12 +100,17 @@ impl notify::EventHandler for NotifyEventHandler {
| MetadataKind::Extended | MetadataKind::Extended
| MetadataKind::Other => return, | MetadataKind::Other => return,
}, },
ModifyKind::Name(_) | ModifyKind::Other => return, ModifyKind::Other => return,
}, },
EventKind::Access(_) EventKind::Access(access_kind) => match access_kind {
| EventKind::Create(_) AccessKind::Any => (),
| EventKind::Remove(_) AccessKind::Close(access_mode) => match access_mode {
| EventKind::Other => return, AccessMode::Any | AccessMode::Write => (),
AccessMode::Execute | AccessMode::Read | AccessMode::Other => return,
},
AccessKind::Read | AccessKind::Open(_) | AccessKind::Other => return,
},
EventKind::Create(_) | EventKind::Remove(_) | EventKind::Other => return,
} }
let _ = input_event let _ = input_event
@ -62,6 +127,6 @@ impl notify::EventHandler for NotifyEventHandler {
.iter() .iter()
.position(|exercise_name| *exercise_name == file_name_without_ext) .position(|exercise_name| *exercise_name == file_name_without_ext)
}) })
.try_for_each(|exercise_ind| self.sender.send(WatchEvent::FileChange { exercise_ind })); .try_for_each(|exercise_ind| self.update_sender.send(exercise_ind));
} }
} }

View file

@ -6,8 +6,8 @@ use crossterm::{
terminal, QueueableCommand, terminal, QueueableCommand,
}; };
use std::{ use std::{
io::{self, StdoutLock, Write}, io::{self, Read, StdoutLock, Write},
sync::mpsc::Sender, sync::mpsc::{sync_channel, Sender, SyncSender},
thread, thread,
}; };
@ -34,6 +34,7 @@ pub struct WatchState<'a> {
done_status: DoneStatus, done_status: DoneStatus,
manual_run: bool, manual_run: bool,
term_width: u16, term_width: u16,
terminal_event_unpause_sender: SyncSender<()>,
} }
impl<'a> WatchState<'a> { impl<'a> WatchState<'a> {
@ -46,8 +47,16 @@ impl<'a> WatchState<'a> {
.context("Failed to get the terminal size")? .context("Failed to get the terminal size")?
.0; .0;
let (terminal_event_unpause_sender, terminal_event_unpause_receiver) = sync_channel(0);
thread::Builder::new() thread::Builder::new()
.spawn(move || terminal_event_handler(watch_event_sender, manual_run)) .spawn(move || {
terminal_event_handler(
watch_event_sender,
terminal_event_unpause_receiver,
manual_run,
)
})
.context("Failed to spawn a thread to handle terminal events")?; .context("Failed to spawn a thread to handle terminal events")?;
Ok(Self { Ok(Self {
@ -57,6 +66,7 @@ impl<'a> WatchState<'a> {
done_status: DoneStatus::Pending, done_status: DoneStatus::Pending,
manual_run, manual_run,
term_width, term_width,
terminal_event_unpause_sender,
}) })
} }
@ -95,6 +105,44 @@ impl<'a> WatchState<'a> {
Ok(()) Ok(())
} }
pub fn reset_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
clear_terminal(stdout)?;
stdout.write_all(b"Resetting will undo all your changes to the file ")?;
stdout.write_all(self.app_state.current_exercise().path.as_bytes())?;
stdout.write_all(b"\nReset (y/n)? ")?;
stdout.flush()?;
{
let mut stdin = io::stdin().lock();
let mut answer = [0];
loop {
stdin
.read_exact(&mut answer)
.context("Failed to read the user's input")?;
match answer[0] {
b'y' | b'Y' => {
self.app_state.reset_current_exercise()?;
// The file watcher reruns the exercise otherwise.
if self.manual_run {
self.run_current_exercise(stdout)?;
}
}
b'n' | b'N' => self.render(stdout)?,
_ => continue,
}
break;
}
}
self.terminal_event_unpause_sender.send(())?;
Ok(())
}
pub fn handle_file_change( pub fn handle_file_change(
&mut self, &mut self,
exercise_ind: usize, exercise_ind: usize,
@ -117,13 +165,6 @@ impl<'a> WatchState<'a> {
} }
fn show_prompt(&self, stdout: &mut StdoutLock) -> io::Result<()> { fn show_prompt(&self, stdout: &mut StdoutLock) -> io::Result<()> {
if self.manual_run {
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"r")?;
stdout.queue(ResetColor)?;
stdout.write_all(b":run / ")?;
}
if self.done_status != DoneStatus::Pending { if self.done_status != DoneStatus::Pending {
stdout.queue(SetAttribute(Attribute::Bold))?; stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"n")?; stdout.write_all(b"n")?;
@ -135,6 +176,13 @@ impl<'a> WatchState<'a> {
stdout.write_all(b" / ")?; stdout.write_all(b" / ")?;
} }
if self.manual_run {
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"r")?;
stdout.queue(ResetColor)?;
stdout.write_all(b":run / ")?;
}
if !self.show_hint { if !self.show_hint {
stdout.queue(SetAttribute(Attribute::Bold))?; stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"h")?; stdout.write_all(b"h")?;
@ -147,6 +195,11 @@ impl<'a> WatchState<'a> {
stdout.queue(ResetColor)?; stdout.queue(ResetColor)?;
stdout.write_all(b":list / ")?; stdout.write_all(b":list / ")?;
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"x")?;
stdout.queue(ResetColor)?;
stdout.write_all(b":reset / ")?;
stdout.queue(SetAttribute(Attribute::Bold))?; stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"q")?; stdout.write_all(b"q")?;
stdout.queue(ResetColor)?; stdout.queue(ResetColor)?;

View file

@ -1,17 +1,25 @@
use crossterm::event::{self, Event, KeyCode, KeyEventKind}; use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use std::sync::{atomic::Ordering::Relaxed, mpsc::Sender}; use std::sync::{
atomic::Ordering::Relaxed,
mpsc::{Receiver, Sender},
};
use super::{WatchEvent, EXERCISE_RUNNING}; use super::{WatchEvent, EXERCISE_RUNNING};
pub enum InputEvent { pub enum InputEvent {
Run,
Next, Next,
Run,
Hint, Hint,
List, List,
Reset,
Quit, Quit,
} }
pub fn terminal_event_handler(sender: Sender<WatchEvent>, manual_run: bool) { pub fn terminal_event_handler(
sender: Sender<WatchEvent>,
unpause_receiver: Receiver<()>,
manual_run: bool,
) {
let last_watch_event = loop { let last_watch_event = loop {
match event::read() { match event::read() {
Ok(Event::Key(key)) => { Ok(Event::Key(key)) => {
@ -26,10 +34,22 @@ pub fn terminal_event_handler(sender: Sender<WatchEvent>, manual_run: bool) {
let input_event = match key.code { let input_event = match key.code {
KeyCode::Char('n') => InputEvent::Next, KeyCode::Char('n') => InputEvent::Next,
KeyCode::Char('r') if manual_run => InputEvent::Run,
KeyCode::Char('h') => InputEvent::Hint, KeyCode::Char('h') => InputEvent::Hint,
KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List), KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List),
KeyCode::Char('x') => {
if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() {
return;
}
// Pause input until quitting the confirmation prompt.
if unpause_receiver.recv().is_err() {
return;
};
continue;
}
KeyCode::Char('q') => break WatchEvent::Input(InputEvent::Quit), KeyCode::Char('q') => break WatchEvent::Input(InputEvent::Quit),
KeyCode::Char('r') if manual_run => InputEvent::Run,
_ => continue, _ => continue,
}; };