feature to allow rustlings to automatically move on to the next exercise

This commit is contained in:
soluton 2026-06-14 22:29:16 +01:00
parent 4bab596677
commit d754784ac0
5 changed files with 64 additions and 11 deletions

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 exercise after a correct solution.
/// Can be toggled at runtime with `a` in the watch mode
#[arg(long)]
pub auto_move: 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_move)?;
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_move: 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_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

View File

@ -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,7 @@ 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 +38,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<WatchEvent>,
}
impl<'a> WatchState<'a> {
@ -46,6 +49,7 @@ impl<'a> WatchState<'a> {
app_state: &'a mut AppState,
watch_event_sender: Sender<WatchEvent>,
manual_run: bool,
auto_move: bool,
) -> Result<Self> {
let term_width = terminal::size()
.context("Failed to get the terminal size")?
@ -53,10 +57,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 +75,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 +118,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 +224,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,11 +261,18 @@ impl<'a> WatchState<'a> {
solution_link_line(stdout, solution_path, self.app_state.emit_file_links())?;
}
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(
stdout,
@ -259,6 +287,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 +336,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)
}
}

View File

@ -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;