Add --auto-next flag to skip the post-success prompt in watch mode

After passing an exercise, the watch loop currently waits for `n`
before advancing. Add an opt-in `--auto-next` flag that renders the
"Exercise done" screen for ~1.5s, then auto-advances to the next
pending exercise and runs it. Auto-advance only fires on success; a
failing exercise still stops at the prompt.

The `n:next` key is hidden from the prompt when --auto-next is on,
but the user can still press `n` to skip ahead manually. --auto-next
is compatible with --manual-run: pressing `r` triggers a check, and
on success the loop auto-advances the same way.
This commit is contained in:
electron224 2026-06-29 15:15:40 +05:30
parent 5e7a5d1721
commit 513e28f064
5 changed files with 35 additions and 7 deletions

View File

@ -8,6 +8,7 @@
- Automatically open the current file with `$EDITOR` in a new pane if Rustlings is running in [Zellij](https://zellij.dev) - 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 `--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 `--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` - Show the file link of the current exercise when running `rustlings hint` and `rustlings reset`
### Fixed ### Fixed

View File

@ -26,6 +26,10 @@ pub struct Args {
/// Only use this if Rustlings fails to detect exercise file changes /// Only use this if Rustlings fails to detect exercise file changes
#[arg(long)] #[arg(long)]
pub manual_run: bool, 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)] #[derive(Subcommand)]

View File

@ -118,7 +118,7 @@ fn main() -> Result<ExitCode> {
) )
}; };
watch::watch(&mut app_state, notify_exercise_names)?; watch::watch(&mut app_state, notify_exercise_names, args.auto_next)?;
app_state.close_editor()?; app_state.close_editor()?;
} }
Some(Command::Run { name }) => { Some(Command::Run { name }) => {

View File

@ -59,6 +59,7 @@ enum WatchExit {
fn run_watch( fn run_watch(
app_state: &mut AppState, app_state: &mut AppState,
notify_exercise_names: Option<&'static [&'static [u8]]>, notify_exercise_names: Option<&'static [&'static [u8]]>,
auto_next: bool,
) -> Result<WatchExit> { ) -> Result<WatchExit> {
let (watch_event_sender, watch_event_receiver) = channel(); let (watch_event_sender, watch_event_receiver) = channel();
@ -87,7 +88,7 @@ fn run_watch(
None 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(); let mut stdout = io::stdout().lock();
watch_state.run_current_exercise(&mut stdout)?; watch_state.run_current_exercise(&mut stdout)?;
@ -133,9 +134,10 @@ fn run_watch(
fn watch_list_loop( fn watch_list_loop(
app_state: &mut AppState, app_state: &mut AppState,
notify_exercise_names: Option<&'static [&'static [u8]]>, notify_exercise_names: Option<&'static [&'static [u8]]>,
auto_next: bool,
) -> Result<()> { ) -> Result<()> {
loop { loop {
match run_watch(app_state, notify_exercise_names)? { match run_watch(app_state, notify_exercise_names, auto_next)? {
WatchExit::Shutdown => break Ok(()), WatchExit::Shutdown => break Ok(()),
// It is much easier to exit the watch mode, launch the list mode and then restart // 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 // 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( pub fn watch(
app_state: &mut AppState, app_state: &mut AppState,
notify_exercise_names: Option<&'static [&'static [u8]]>, notify_exercise_names: Option<&'static [&'static [u8]]>,
auto_next: bool,
) -> Result<()> { ) -> Result<()> {
// TODO: Use cfg_select! after bumping MSRV to at least 1.95 // TODO: Use cfg_select! after bumping MSRV to at least 1.95
#[cfg(not(windows))] #[cfg(not(windows))]
@ -161,7 +164,7 @@ pub fn watch(
rustix::termios::LocalModes::ICANON | rustix::termios::LocalModes::ECHO; rustix::termios::LocalModes::ICANON | rustix::termios::LocalModes::ECHO;
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?; 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; termios.local_modes = original_local_modes;
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?; rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
@ -170,7 +173,7 @@ pub fn watch(
} }
#[cfg(windows)] #[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 const QUIT_MSG: &[u8] = b"q\n

View File

@ -10,6 +10,7 @@ use std::{
io::{self, Read, StdoutLock, Write}, io::{self, Read, StdoutLock, Write},
sync::mpsc::{Sender, SyncSender, sync_channel}, sync::mpsc::{Sender, SyncSender, sync_channel},
thread, thread,
time::Duration,
}; };
use crate::{ use crate::{
@ -24,6 +25,9 @@ const HEADING_ATTRIBUTES: Attributes = Attributes::none()
.with(Attribute::Bold) .with(Attribute::Bold)
.with(Attribute::Underlined); .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)] #[derive(PartialEq, Eq)]
enum DoneStatus { enum DoneStatus {
DoneWithSolution(String), DoneWithSolution(String),
@ -37,6 +41,7 @@ pub struct WatchState<'a> {
show_hint: bool, show_hint: bool,
done_status: DoneStatus, done_status: DoneStatus,
manual_run: bool, manual_run: bool,
auto_next: bool,
term_width: u16, term_width: u16,
terminal_event_unpause_sender: SyncSender<()>, terminal_event_unpause_sender: SyncSender<()>,
} }
@ -46,6 +51,7 @@ impl<'a> WatchState<'a> {
app_state: &'a mut AppState, app_state: &'a mut AppState,
watch_event_sender: Sender<WatchEvent>, watch_event_sender: Sender<WatchEvent>,
manual_run: bool, manual_run: bool,
auto_next: bool,
) -> Result<Self> { ) -> Result<Self> {
let term_width = terminal::size() let term_width = terminal::size()
.context("Failed to get the terminal size")? .context("Failed to get the terminal size")?
@ -69,6 +75,7 @@ impl<'a> WatchState<'a> {
show_hint: false, show_hint: false,
done_status: DoneStatus::Pending, done_status: DoneStatus::Pending,
manual_run, manual_run,
auto_next,
term_width, term_width,
terminal_event_unpause_sender, terminal_event_unpause_sender,
}) })
@ -110,7 +117,20 @@ impl<'a> WatchState<'a> {
self.app_state.join_editor_handle(editor_handle)?; self.app_state.join_editor_handle(editor_handle)?;
self.render(stdout)?; 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<()> { 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<()> { 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.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"n")?; stdout.write_all(b"n")?;
stdout.queue(ResetColor)?; stdout.queue(ResetColor)?;