summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main.rs5
-rw-r--r--src/watch.rs150
-rw-r--r--src/watch/state.rs73
3 files changed, 133 insertions, 95 deletions
diff --git a/src/main.rs b/src/main.rs
index 356b77c..6af66bd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -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) => (),
diff --git a/src/watch.rs b/src/watch.rs
index 967f98c..abf4002 100644
--- a/src/watch.rs
+++ b/src/watch.rs
@@ -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<()> {
- let (tx, rx) = channel();
- let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?;
- debouncer
- .watcher()
- .watch(Path::new("exercises"), RecursiveMode::Recursive)?;
+enum WatchEvent {
+ Input(InputEvent),
+ FileChange { exercise_ind: usize },
+ TerminalResize,
+}
- let mut watch_state = WatchState::new(state_file, exercises, rx);
+struct DebouceEventHandler {
+ tx: Sender<WatchEvent>,
+ exercises: &'static [Exercise],
+}
- watch_state.run_exercise()?;
- watch_state.render()?;
+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;
+ }
- let (tx, rx) = sync_channel(0);
- thread::spawn(move || {
- let mut stdin = io::stdin().lock();
- let mut stdin_buf = String::with_capacity(8);
+ self.exercises
+ .iter()
+ .position(|exercise| event.path.ends_with(&exercise.path))
+ })
+ .min()
+ else {
+ return;
+ };
- loop {
- stdin.read_line(&mut stdin_buf).unwrap();
+ self.tx.send(WatchEvent::FileChange { exercise_ind });
+ }
+}
- let event = match stdin_buf.trim() {
- "h" | "hint" => Some(Event::Hint),
- "c" | "clear" => Some(Event::Clear),
- "q" | "quit" => Some(Event::Quit),
- _ => None,
- };
+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")?;
- stdin_buf.clear();
+ let event = match stdin_buf.trim() {
+ "h" | "hint" => InputEvent::Hint,
+ "c" | "clear" => InputEvent::Clear,
+ "q" | "quit" => InputEvent::Quit,
+ _ => InputEvent::Unrecognized,
+ };
- if tx.send(event).is_err() {
- break;
- };
+ stdin_buf.clear();
+
+ if tx.send(WatchEvent::Input(event)).is_err() {
+ return Ok(());
}
- });
+ }
+}
- loop {
- watch_state.try_recv_event()?;
+pub fn watch(state_file: &StateFile, exercises: &'static [Exercise]) -> Result<()> {
+ 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() {
- 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()?;
- }
+ 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 {
+ 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(())
}
diff --git a/src/watch/state.rs b/src/watch/state.rs
index 40f48ef..f614ae0 100644
--- a/src/watch/state.rs
+++ b/src/watch/state.rs
@@ -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(());
- };
-
- 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;
- }
+ 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;
- 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<()> {