Improve event handling in the watch mode

This commit is contained in:
mo8it 2024-04-09 21:07:53 +02:00
parent 850c1d0234
commit f0ce2c1afa
3 changed files with 133 additions and 95 deletions

View file

@ -85,7 +85,8 @@ Did you already install Rust?
Try running `cargo --version` to diagnose the problem.", Try running `cargo --version` to diagnose the problem.",
)?; )?;
let exercises = InfoFile::parse()?.exercises; // Leaking is not a problem since the exercises are used until the end of the program.
let exercises = InfoFile::parse()?.exercises.leak();
if matches!(args.command, Some(Subcommands::Init)) { if matches!(args.command, Some(Subcommands::Init)) {
init::init(&exercises).context("Initialization failed")?; init::init(&exercises).context("Initialization failed")?;
@ -110,7 +111,7 @@ If you are just starting with Rustlings, run the command `rustlings init` to ini
match args.command { match args.command {
None | Some(Subcommands::Watch) => { None | Some(Subcommands::Watch) => {
watch::watch(&state_file, &exercises)?; watch::watch(&state_file, exercises)?;
} }
// `Init` is handled above. // `Init` is handled above.
Some(Subcommands::Init) => (), Some(Subcommands::Init) => (),

View file

@ -1,9 +1,11 @@
use anyhow::Result; use anyhow::{bail, Context, Result};
use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode}; use notify_debouncer_mini::{
new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind,
};
use std::{ use std::{
io::{self, BufRead, Write}, io::{self, BufRead, Write},
path::Path, path::Path,
sync::mpsc::{channel, sync_channel}, sync::mpsc::{channel, Sender},
thread, thread,
time::Duration, time::Duration,
}; };
@ -14,70 +16,130 @@ use crate::{exercise::Exercise, state_file::StateFile};
use self::state::WatchState; use self::state::WatchState;
enum Event { enum InputEvent {
Hint, Hint,
Clear, Clear,
Quit, Quit,
Unrecognized,
} }
pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> { enum WatchEvent {
let (tx, rx) = channel(); Input(InputEvent),
let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?; FileChange { exercise_ind: usize },
debouncer TerminalResize,
.watcher() }
.watch(Path::new("exercises"), RecursiveMode::Recursive)?;
let mut watch_state = WatchState::new(state_file, exercises, rx); struct DebouceEventHandler {
tx: Sender<WatchEvent>,
exercises: &'static [Exercise],
}
watch_state.run_exercise()?; impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler {
watch_state.render()?; fn handle_event(&mut self, event: DebounceEventResult) {
let Ok(event) = event else {
// TODO
return;
};
let (tx, rx) = sync_channel(0); let Some(exercise_ind) = event
thread::spawn(move || { .iter()
.filter_map(|event| {
if event.kind != DebouncedEventKind::Any
|| !event.path.extension().is_some_and(|ext| ext == "rs")
{
return None;
}
self.exercises
.iter()
.position(|exercise| event.path.ends_with(&exercise.path))
})
.min()
else {
return;
};
self.tx.send(WatchEvent::FileChange { exercise_ind });
}
}
fn input_handler(tx: Sender<WatchEvent>) -> Result<()> {
let mut stdin = io::stdin().lock(); let mut stdin = io::stdin().lock();
let mut stdin_buf = String::with_capacity(8); let mut stdin_buf = String::with_capacity(8);
loop { loop {
stdin.read_line(&mut stdin_buf).unwrap(); stdin
.read_line(&mut stdin_buf)
.context("Failed to read the user's input from stdin")?;
let event = match stdin_buf.trim() { let event = match stdin_buf.trim() {
"h" | "hint" => Some(Event::Hint), "h" | "hint" => InputEvent::Hint,
"c" | "clear" => Some(Event::Clear), "c" | "clear" => InputEvent::Clear,
"q" | "quit" => Some(Event::Quit), "q" | "quit" => InputEvent::Quit,
_ => None, _ => InputEvent::Unrecognized,
}; };
stdin_buf.clear(); stdin_buf.clear();
if tx.send(event).is_err() { if tx.send(WatchEvent::Input(event)).is_err() {
break; return Ok(());
};
} }
}); }
}
loop { pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<()> {
watch_state.try_recv_event()?; let (tx, rx) = channel();
let mut debouncer = new_debouncer(
Duration::from_secs(1),
DebouceEventHandler {
tx: tx.clone(),
exercises,
},
)?;
debouncer
.watcher()
.watch(Path::new("exercises"), RecursiveMode::Recursive)?;
if let Ok(event) = rx.try_recv() { let mut watch_state = WatchState::new(state_file, exercises);
// TODO: bool
watch_state.run_exercise()?;
watch_state.render()?;
let input_thread = thread::spawn(move || input_handler(tx));
while let Ok(event) = rx.recv() {
match event { match event {
Some(Event::Hint) => { WatchEvent::Input(InputEvent::Hint) => {
watch_state.show_hint()?; watch_state.show_hint()?;
} }
Some(Event::Clear) => { WatchEvent::Input(InputEvent::Clear) | WatchEvent::TerminalResize => {
watch_state.render()?; watch_state.render()?;
} }
Some(Event::Quit) => break, WatchEvent::Input(InputEvent::Quit) => break,
None => { WatchEvent::Input(InputEvent::Unrecognized) => {
watch_state.handle_invalid_cmd()?; watch_state.handle_invalid_cmd()?;
} }
WatchEvent::FileChange { exercise_ind } => {
// TODO: bool
watch_state.run_exercise_with_ind(exercise_ind)?;
watch_state.render()?;
} }
} }
} }
// Drop the receiver for the sender threads to exit.
drop(rx);
watch_state.into_writer().write_all(b" watch_state.into_writer().write_all(b"
We hope you're enjoying learning Rust! We hope you're enjoying learning Rust!
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. If you want to continue working on the exercises at a later point, you can simply run `rustlings` again.
")?; ")?;
match input_thread.join() {
Ok(res) => res?,
Err(_) => bail!("The input thread panicked"),
}
Ok(()) Ok(())
} }

View file

@ -1,26 +1,23 @@
use anyhow::Result; use anyhow::{Context, Result};
use crossterm::{ use crossterm::{
style::{Attribute, ContentStyle, Stylize}, style::{Attribute, ContentStyle, Stylize},
terminal::{Clear, ClearType}, terminal::{size, Clear, ClearType},
ExecutableCommand, ExecutableCommand,
}; };
use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
use std::{ use std::{
fmt::Write as _, fmt::Write as _,
io::{self, StdoutLock, Write as _}, io::{self, StdoutLock, Write as _},
sync::mpsc::Receiver,
time::Duration,
}; };
use crate::{ use crate::{
exercise::{Exercise, State}, exercise::{Exercise, State},
progress_bar::progress_bar,
state_file::StateFile, state_file::StateFile,
}; };
pub struct WatchState<'a> { pub struct WatchState<'a> {
writer: StdoutLock<'a>, writer: StdoutLock<'a>,
rx: Receiver<DebounceEventResult>, exercises: &'static [Exercise],
exercises: &'a [Exercise],
exercise: &'a Exercise, exercise: &'a Exercise,
current_exercise_ind: usize, current_exercise_ind: usize,
stdout: Option<Vec<u8>>, stdout: Option<Vec<u8>>,
@ -30,11 +27,7 @@ pub struct WatchState<'a> {
} }
impl<'a> WatchState<'a> { impl<'a> WatchState<'a> {
pub fn new( pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self {
state_file: &StateFile,
exercises: &'a [Exercise],
rx: Receiver<DebounceEventResult>,
) -> Self {
let current_exercise_ind = state_file.next_exercise_ind(); let current_exercise_ind = state_file.next_exercise_ind();
let exercise = &exercises[current_exercise_ind]; let exercise = &exercises[current_exercise_ind];
@ -50,7 +43,6 @@ impl<'a> WatchState<'a> {
Self { Self {
writer, writer,
rx,
exercises, exercises,
exercise, exercise,
current_exercise_ind, current_exercise_ind,
@ -114,41 +106,14 @@ You can keep working on this exercise or jump into the next one by removing the
Ok(true) Ok(true)
} }
pub fn try_recv_event(&mut self) -> Result<()> { pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<bool> {
let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else { self.exercise = self
return Ok(()); .exercises
}; .get(exercise_ind)
.context("Invalid exercise index")?;
self.current_exercise_ind = exercise_ind;
if let Some(current_exercise_ind) = events? self.run_exercise()
.iter()
.filter_map(|event| {
if event.kind != DebouncedEventKind::Any
|| !event.path.extension().is_some_and(|ext| ext == "rs")
{
return None;
}
self.exercises
.iter()
.position(|exercise| event.path.ends_with(&exercise.path))
})
.min()
{
self.current_exercise_ind = current_exercise_ind;
} else {
return Ok(());
};
while self.current_exercise_ind < self.exercises.len() {
self.exercise = &self.exercises[self.current_exercise_ind];
if !self.run_exercise()? {
break;
}
self.current_exercise_ind += 1;
}
Ok(())
} }
pub fn show_prompt(&mut self) -> io::Result<()> { pub fn show_prompt(&mut self) -> io::Result<()> {
@ -156,7 +121,7 @@ You can keep working on this exercise or jump into the next one by removing the
self.writer.flush() self.writer.flush()
} }
pub fn render(&mut self) -> io::Result<()> { pub fn render(&mut self) -> Result<()> {
self.writer.execute(Clear(ClearType::All))?; self.writer.execute(Clear(ClearType::All))?;
if let Some(stdout) = &self.stdout { if let Some(stdout) = &self.stdout {
@ -171,7 +136,17 @@ You can keep working on this exercise or jump into the next one by removing the
self.writer.write_all(message.as_bytes())?; self.writer.write_all(message.as_bytes())?;
} }
self.show_prompt() let line_width = size()?.0;
let progress_bar = progress_bar(
self.current_exercise_ind as u16,
self.exercises.len() as u16,
line_width,
)?;
self.writer.write_all(progress_bar.as_bytes())?;
self.show_prompt()?;
Ok(())
} }
pub fn show_hint(&mut self) -> io::Result<()> { pub fn show_hint(&mut self) -> io::Result<()> {