diff --git a/CHANGELOG.md b/CHANGELOG.md index 807f7d49..d5be167c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Automatically open the current file with `$EDITOR` in a new pane if Rustlings is running in [Zellij](https://zellij.dev) - New argument `--no-editor` to disable automatic opening of the current file in VS Code or Zellij - New argument `--edit-cmd` to communicate with an editor running in a different process to open the current exercise +- New argument `--auto-next` to automatically move on to the next pending exercise after it passes, without waiting for `n` - Show the file link of the current exercise when running `rustlings hint` and `rustlings reset` ### Fixed diff --git a/src/cli.rs b/src/cli.rs index 153994be..512eac4f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -26,6 +26,10 @@ pub struct Args { /// Only use this if Rustlings fails to detect exercise file changes #[arg(long)] pub manual_run: bool, + /// Automatically move on to the next pending exercise after it passes, + /// without waiting for `n`. Useful for learners who want a continuous flow. + #[arg(long)] + pub auto_next: bool, } #[derive(Subcommand)] diff --git a/src/main.rs b/src/main.rs index 8da36f7f..201fd3b0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -118,7 +118,7 @@ fn main() -> Result { ) }; - watch::watch(&mut app_state, notify_exercise_names)?; + watch::watch(&mut app_state, notify_exercise_names, args.auto_next)?; app_state.close_editor()?; } Some(Command::Run { name }) => { diff --git a/src/watch.rs b/src/watch.rs index f3804a40..fb14280a 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -59,6 +59,7 @@ enum WatchExit { fn run_watch( app_state: &mut AppState, notify_exercise_names: Option<&'static [&'static [u8]]>, + auto_next: bool, ) -> Result { let (watch_event_sender, watch_event_receiver) = channel(); @@ -87,7 +88,7 @@ fn run_watch( None }; - let mut watch_state = WatchState::build(app_state, watch_event_sender, manual_run)?; + let mut watch_state = WatchState::build(app_state, watch_event_sender, manual_run, auto_next)?; let mut stdout = io::stdout().lock(); watch_state.run_current_exercise(&mut stdout)?; @@ -133,9 +134,10 @@ fn run_watch( fn watch_list_loop( app_state: &mut AppState, notify_exercise_names: Option<&'static [&'static [u8]]>, + auto_next: bool, ) -> Result<()> { loop { - match run_watch(app_state, notify_exercise_names)? { + match run_watch(app_state, notify_exercise_names, auto_next)? { WatchExit::Shutdown => break Ok(()), // It is much easier to exit the watch mode, launch the list mode and then restart // the watch mode instead of trying to pause the watch threads and correct the @@ -149,6 +151,7 @@ fn watch_list_loop( pub fn watch( app_state: &mut AppState, notify_exercise_names: Option<&'static [&'static [u8]]>, + auto_next: bool, ) -> Result<()> { // TODO: Use cfg_select! after bumping MSRV to at least 1.95 #[cfg(not(windows))] @@ -161,7 +164,7 @@ pub fn watch( rustix::termios::LocalModes::ICANON | rustix::termios::LocalModes::ECHO; rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?; - let res = watch_list_loop(app_state, notify_exercise_names); + let res = watch_list_loop(app_state, notify_exercise_names, auto_next); termios.local_modes = original_local_modes; rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?; @@ -170,7 +173,7 @@ pub fn watch( } #[cfg(windows)] - watch_list_loop(app_state, notify_exercise_names) + watch_list_loop(app_state, notify_exercise_names, auto_next) } const QUIT_MSG: &[u8] = b"q\n diff --git a/src/watch/state.rs b/src/watch/state.rs index 8bbdc585..c7b871e1 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -10,6 +10,7 @@ use std::{ io::{self, Read, StdoutLock, Write}, sync::mpsc::{Sender, SyncSender, sync_channel}, thread, + time::Duration, }; use crate::{ @@ -24,6 +25,9 @@ const HEADING_ATTRIBUTES: Attributes = Attributes::none() .with(Attribute::Bold) .with(Attribute::Underlined); +// How long to show the "Exercise done" screen before auto-advancing. +const AUTO_NEXT_DELAY: Duration = Duration::from_millis(1500); + #[derive(PartialEq, Eq)] enum DoneStatus { DoneWithSolution(String), @@ -37,6 +41,7 @@ pub struct WatchState<'a> { show_hint: bool, done_status: DoneStatus, manual_run: bool, + auto_next: bool, term_width: u16, terminal_event_unpause_sender: SyncSender<()>, } @@ -46,6 +51,7 @@ impl<'a> WatchState<'a> { app_state: &'a mut AppState, watch_event_sender: Sender, manual_run: bool, + auto_next: bool, ) -> Result { let term_width = terminal::size() .context("Failed to get the terminal size")? @@ -69,6 +75,7 @@ impl<'a> WatchState<'a> { show_hint: false, done_status: DoneStatus::Pending, manual_run, + auto_next, term_width, terminal_event_unpause_sender, }) @@ -110,7 +117,20 @@ impl<'a> WatchState<'a> { self.app_state.join_editor_handle(editor_handle)?; self.render(stdout)?; - Ok(()) + // If `--auto-next` is on and the exercise passed, briefly show the + // "done" screen, then advance to the next pending exercise and run it. + // The recursion bottoms out when we hit a failing exercise + // (rendering leaves the user at the prompt) or when everything is done. + if self.auto_next && self.done_status != DoneStatus::Pending { + thread::sleep(AUTO_NEXT_DELAY); + match self.next_exercise(stdout)? { + ExercisesProgress::AllDone => Ok(()), + ExercisesProgress::NewPending => self.run_current_exercise(stdout), + ExercisesProgress::CurrentPending => Ok(()), + } + } else { + Ok(()) + } } pub fn reset_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> { @@ -175,7 +195,7 @@ impl<'a> WatchState<'a> { } fn show_prompt(&self, stdout: &mut StdoutLock) -> io::Result<()> { - if self.done_status != DoneStatus::Pending { + if !self.auto_next && self.done_status != DoneStatus::Pending { stdout.queue(SetAttribute(Attribute::Bold))?; stdout.write_all(b"n")?; stdout.queue(ResetColor)?;