diff --git a/src/cli.rs b/src/cli.rs index 153994be33..1e69eccc41 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 exercise after a correct solution. + /// Can be toggled at runtime with `a` in the watch mode + #[arg(long)] + pub auto_move: bool, } #[derive(Subcommand)] diff --git a/src/main.rs b/src/main.rs index 8da36f7f57..932cbd8361 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_move)?; app_state.close_editor()?; } Some(Command::Run { name }) => { diff --git a/src/watch.rs b/src/watch.rs index f3804a4021..fa5a183645 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_move: 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_move)?; let mut stdout = io::stdout().lock(); watch_state.run_current_exercise(&mut stdout)?; @@ -110,6 +111,9 @@ fn run_watch( ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?, }, WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?, + WatchEvent::Input(InputEvent::ToggleAutoMove) => { + watch_state.toggle_auto_move(&mut stdout)?; + } WatchEvent::Input(InputEvent::Quit) => { stdout.write_all(QUIT_MSG)?; break; @@ -133,9 +137,10 @@ fn run_watch( fn watch_list_loop( app_state: &mut AppState, notify_exercise_names: Option<&'static [&'static [u8]]>, + auto_move: bool, ) -> Result<()> { loop { - match run_watch(app_state, notify_exercise_names)? { + match run_watch(app_state, notify_exercise_names, auto_move)? { 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 +154,7 @@ fn watch_list_loop( pub fn watch( app_state: &mut AppState, notify_exercise_names: Option<&'static [&'static [u8]]>, + auto_move: bool, ) -> Result<()> { // TODO: Use cfg_select! after bumping MSRV to at least 1.95 #[cfg(not(windows))] @@ -161,7 +167,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_move); termios.local_modes = original_local_modes; rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?; @@ -170,7 +176,7 @@ pub fn watch( } #[cfg(windows)] - watch_list_loop(app_state, notify_exercise_names) + watch_list_loop(app_state, notify_exercise_names, auto_move) } const QUIT_MSG: &[u8] = b"q\n diff --git a/src/watch/state.rs b/src/watch/state.rs index 8bbdc58518..cfc38348ad 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::{ @@ -17,7 +18,10 @@ use crate::{ clear_terminal, exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line}, term::progress_bar, - watch::{InputPauseGuard, WatchEvent, terminal_event::terminal_event_handler}, + watch::{ + InputPauseGuard, WatchEvent, terminal_event::InputEvent, + terminal_event::terminal_event_handler, + }, }; const HEADING_ATTRIBUTES: Attributes = Attributes::none() @@ -37,8 +41,10 @@ pub struct WatchState<'a> { show_hint: bool, done_status: DoneStatus, manual_run: bool, + auto_move: bool, term_width: u16, terminal_event_unpause_sender: SyncSender<()>, + watch_event_sender: Sender, } impl<'a> WatchState<'a> { @@ -46,6 +52,7 @@ impl<'a> WatchState<'a> { app_state: &'a mut AppState, watch_event_sender: Sender, manual_run: bool, + auto_move: bool, ) -> Result { let term_width = terminal::size() .context("Failed to get the terminal size")? @@ -53,10 +60,12 @@ impl<'a> WatchState<'a> { let (terminal_event_unpause_sender, terminal_event_unpause_receiver) = sync_channel(0); + let event_sender_for_handler = watch_event_sender.clone(); + thread::Builder::new() .spawn(move || { terminal_event_handler( - watch_event_sender, + event_sender_for_handler, terminal_event_unpause_receiver, manual_run, ); @@ -69,8 +78,10 @@ impl<'a> WatchState<'a> { show_hint: false, done_status: DoneStatus::Pending, manual_run, + auto_move, term_width, terminal_event_unpause_sender, + watch_event_sender, }) } @@ -110,6 +121,18 @@ impl<'a> WatchState<'a> { self.app_state.join_editor_handle(editor_handle)?; self.render(stdout)?; + // Auto-move: if the exercise is done and auto_move is enabled, + // spawn a thread that sends a Next event after 2 seconds. + if self.auto_move && self.done_status != DoneStatus::Pending { + let sender = self.watch_event_sender.clone(); + thread::Builder::new() + .spawn(move || { + thread::sleep(Duration::from_secs(2)); + let _ = sender.send(WatchEvent::Input(InputEvent::Next)); + }) + .context("Failed to spawn auto-move thread")?; + } + Ok(()) } @@ -204,6 +227,7 @@ impl<'a> WatchState<'a> { show_key(b'l', b":list / ")?; show_key(b'c', b":check all / ")?; show_key(b'x', b":reset / ")?; + show_key(b'a', b":auto move / ")?; show_key(b'q', b":quit ? ")?; stdout.flush() @@ -240,10 +264,15 @@ impl<'a> WatchState<'a> { solution_link_line(stdout, solution_path, self.app_state.emit_file_links())?; } - stdout.write_all( - "When done experimenting, enter `n` to move on to the next exercise 🦀\n\n" - .as_bytes(), - )?; + if self.auto_move { + stdout + .write_all("Auto-moving to the next exercise in 2 seconds…\n\n".as_bytes())?; + } else { + stdout.write_all( + "When done experimenting, enter `n` to move on to the next exercise 🦀\n\n" + .as_bytes(), + )?; + } } progress_bar( @@ -259,6 +288,14 @@ impl<'a> WatchState<'a> { .terminal_file_link(stdout, self.app_state.emit_file_links())?; stdout.write_all(b"\n\n")?; + // Show auto-move status + if self.auto_move { + stdout.queue(SetForegroundColor(Color::Cyan))?; + stdout.write_all(b"Auto-move: ON")?; + stdout.queue(ResetColor)?; + stdout.write_all(b"\n\n")?; + } + self.show_prompt(stdout)?; Ok(()) @@ -300,4 +337,9 @@ impl<'a> WatchState<'a> { Ok(()) } + + pub fn toggle_auto_move(&mut self, stdout: &mut StdoutLock) -> io::Result<()> { + self.auto_move = !self.auto_move; + self.render(stdout) + } } diff --git a/src/watch/terminal_event.rs b/src/watch/terminal_event.rs index 4f0685b6e0..614cc3ce85 100644 --- a/src/watch/terminal_event.rs +++ b/src/watch/terminal_event.rs @@ -14,6 +14,7 @@ pub enum InputEvent { CheckAll, Reset, Quit, + ToggleAutoMove, } pub fn terminal_event_handler( @@ -39,6 +40,7 @@ pub fn terminal_event_handler( KeyCode::Char('h') => InputEvent::Hint, KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List), KeyCode::Char('c') => InputEvent::CheckAll, + KeyCode::Char('a') => InputEvent::ToggleAutoMove, KeyCode::Char('x') => { if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() { return;