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.",
)?;
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)) {
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 {
None | Some(Subcommands::Watch) => {
watch::watch(&state_file, &exercises)?;
watch::watch(&state_file, exercises)?;
}
// `Init` is handled above.
Some(Subcommands::Init) => (),

View file

@ -1,9 +1,11 @@
use anyhow::Result;
use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode};
use anyhow::{bail, Context, Result};
use notify_debouncer_mini::{
new_debouncer, notify::RecursiveMode, DebounceEventResult, DebouncedEventKind,
};
use std::{
io::{self, BufRead, Write},
path::Path,
sync::mpsc::{channel, sync_channel},
sync::mpsc::{channel, Sender},
thread,
time::Duration,
};
@ -14,70 +16,130 @@ use crate::{exercise::Exercise, state_file::StateFile};
use self::state::WatchState;
enum Event {
enum InputEvent {
Hint,
Clear,
Quit,
Unrecognized,
}
pub fn watch(state_file: &StateFile, exercises: &[Exercise]) -> Result<()> {
enum WatchEvent {
Input(InputEvent),
FileChange { exercise_ind: usize },
TerminalResize,
}
struct DebouceEventHandler {
tx: Sender<WatchEvent>,
exercises: &'static [Exercise],
}
impl notify_debouncer_mini::DebounceEventHandler for DebouceEventHandler {
fn handle_event(&mut self, event: DebounceEventResult) {
let Ok(event) = event else {
// TODO
return;
};
let Some(exercise_ind) = event
.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_buf = String::with_capacity(8);
loop {
stdin
.read_line(&mut stdin_buf)
.context("Failed to read the user's input from stdin")?;
let event = match stdin_buf.trim() {
"h" | "hint" => InputEvent::Hint,
"c" | "clear" => InputEvent::Clear,
"q" | "quit" => InputEvent::Quit,
_ => InputEvent::Unrecognized,
};
stdin_buf.clear();
if tx.send(WatchEvent::Input(event)).is_err() {
return Ok(());
}
}
}
pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<()> {
let (tx, rx) = channel();
let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?;
let mut debouncer = new_debouncer(
Duration::from_secs(1),
DebouceEventHandler {
tx: tx.clone(),
exercises,
},
)?;
debouncer
.watcher()
.watch(Path::new("exercises"), RecursiveMode::Recursive)?;
let mut watch_state = WatchState::new(state_file, exercises, rx);
let mut watch_state = WatchState::new(state_file, exercises);
// TODO: bool
watch_state.run_exercise()?;
watch_state.render()?;
let (tx, rx) = sync_channel(0);
thread::spawn(move || {
let mut stdin = io::stdin().lock();
let mut stdin_buf = String::with_capacity(8);
let input_thread = thread::spawn(move || input_handler(tx));
loop {
stdin.read_line(&mut stdin_buf).unwrap();
let event = match stdin_buf.trim() {
"h" | "hint" => Some(Event::Hint),
"c" | "clear" => Some(Event::Clear),
"q" | "quit" => Some(Event::Quit),
_ => None,
};
stdin_buf.clear();
if tx.send(event).is_err() {
break;
};
}
});
loop {
watch_state.try_recv_event()?;
if let Ok(event) = rx.try_recv() {
match event {
Some(Event::Hint) => {
watch_state.show_hint()?;
}
Some(Event::Clear) => {
watch_state.render()?;
}
Some(Event::Quit) => break,
None => {
watch_state.handle_invalid_cmd()?;
}
while let Ok(event) = rx.recv() {
match event {
WatchEvent::Input(InputEvent::Hint) => {
watch_state.show_hint()?;
}
WatchEvent::Input(InputEvent::Clear) | WatchEvent::TerminalResize => {
watch_state.render()?;
}
WatchEvent::Input(InputEvent::Quit) => break,
WatchEvent::Input(InputEvent::Unrecognized) => {
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"
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.
")?;
match input_thread.join() {
Ok(res) => res?,
Err(_) => bail!("The input thread panicked"),
}
Ok(())
}

View file

@ -1,26 +1,23 @@
use anyhow::Result;
use anyhow::{Context, Result};
use crossterm::{
style::{Attribute, ContentStyle, Stylize},
terminal::{Clear, ClearType},
terminal::{size, Clear, ClearType},
ExecutableCommand,
};
use notify_debouncer_mini::{DebounceEventResult, DebouncedEventKind};
use std::{
fmt::Write as _,
io::{self, StdoutLock, Write as _},
sync::mpsc::Receiver,
time::Duration,
};
use crate::{
exercise::{Exercise, State},
progress_bar::progress_bar,
state_file::StateFile,
};
pub struct WatchState<'a> {
writer: StdoutLock<'a>,
rx: Receiver<DebounceEventResult>,
exercises: &'a [Exercise],
exercises: &'static [Exercise],
exercise: &'a Exercise,
current_exercise_ind: usize,
stdout: Option<Vec<u8>>,
@ -30,11 +27,7 @@ pub struct WatchState<'a> {
}
impl<'a> WatchState<'a> {
pub fn new(
state_file: &StateFile,
exercises: &'a [Exercise],
rx: Receiver<DebounceEventResult>,
) -> Self {
pub fn new(state_file: &StateFile, exercises: &'static [Exercise]) -> Self {
let current_exercise_ind = state_file.next_exercise_ind();
let exercise = &exercises[current_exercise_ind];
@ -50,7 +43,6 @@ impl<'a> WatchState<'a> {
Self {
writer,
rx,
exercises,
exercise,
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)
}
pub fn try_recv_event(&mut self) -> Result<()> {
let Ok(events) = self.rx.recv_timeout(Duration::from_millis(100)) else {
return Ok(());
};
pub fn run_exercise_with_ind(&mut self, exercise_ind: usize) -> Result<bool> {
self.exercise = self
.exercises
.get(exercise_ind)
.context("Invalid exercise index")?;
self.current_exercise_ind = exercise_ind;
if let Some(current_exercise_ind) = events?
.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(())
self.run_exercise()
}
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()
}
pub fn render(&mut self) -> io::Result<()> {
pub fn render(&mut self) -> Result<()> {
self.writer.execute(Clear(ClearType::All))?;
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.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<()> {