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)
- 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

View File

@ -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)]

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()?;
}
Some(Command::Run { name }) => {

View File

@ -59,6 +59,7 @@ enum WatchExit {
fn run_watch(
app_state: &mut AppState,
notify_exercise_names: Option<&'static [&'static [u8]]>,
auto_next: bool,
) -> Result<WatchExit> {
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

View File

@ -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<WatchEvent>,
manual_run: bool,
auto_next: bool,
) -> Result<Self> {
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,8 +117,21 @@ impl<'a> WatchState<'a> {
self.app_state.join_editor_handle(editor_handle)?;
self.render(stdout)?;
// 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<()> {
clear_terminal(stdout)?;
@ -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)?;