mirror of
https://github.com/rust-lang/rustlings.git
synced 2026-06-30 00:08:45 +00:00
feature to allow rustlings to automatically move on to the next exercise
This commit is contained in:
parent
4bab596677
commit
d754784ac0
@ -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 exercise after a correct solution.
|
||||||
|
/// Can be toggled at runtime with `a` in the watch mode
|
||||||
|
#[arg(long)]
|
||||||
|
pub auto_move: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
|
|||||||
@ -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()?;
|
app_state.close_editor()?;
|
||||||
}
|
}
|
||||||
Some(Command::Run { name }) => {
|
Some(Command::Run { name }) => {
|
||||||
|
|||||||
14
src/watch.rs
14
src/watch.rs
@ -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_move: 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_move)?;
|
||||||
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)?;
|
||||||
@ -110,6 +111,9 @@ fn run_watch(
|
|||||||
ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?,
|
ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?,
|
||||||
},
|
},
|
||||||
WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&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) => {
|
WatchEvent::Input(InputEvent::Quit) => {
|
||||||
stdout.write_all(QUIT_MSG)?;
|
stdout.write_all(QUIT_MSG)?;
|
||||||
break;
|
break;
|
||||||
@ -133,9 +137,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_move: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
match run_watch(app_state, notify_exercise_names)? {
|
match run_watch(app_state, notify_exercise_names, auto_move)? {
|
||||||
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 +154,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_move: 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 +167,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_move);
|
||||||
|
|
||||||
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 +176,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_move)
|
||||||
}
|
}
|
||||||
|
|
||||||
const QUIT_MSG: &[u8] = b"q\n
|
const QUIT_MSG: &[u8] = b"q\n
|
||||||
|
|||||||
@ -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::{
|
||||||
@ -17,7 +18,7 @@ use crate::{
|
|||||||
clear_terminal,
|
clear_terminal,
|
||||||
exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line},
|
exercise::{OUTPUT_CAPACITY, RunnableExercise, solution_link_line},
|
||||||
term::progress_bar,
|
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()
|
const HEADING_ATTRIBUTES: Attributes = Attributes::none()
|
||||||
@ -37,8 +38,10 @@ pub struct WatchState<'a> {
|
|||||||
show_hint: bool,
|
show_hint: bool,
|
||||||
done_status: DoneStatus,
|
done_status: DoneStatus,
|
||||||
manual_run: bool,
|
manual_run: bool,
|
||||||
|
auto_move: bool,
|
||||||
term_width: u16,
|
term_width: u16,
|
||||||
terminal_event_unpause_sender: SyncSender<()>,
|
terminal_event_unpause_sender: SyncSender<()>,
|
||||||
|
watch_event_sender: Sender<WatchEvent>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> WatchState<'a> {
|
impl<'a> WatchState<'a> {
|
||||||
@ -46,6 +49,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_move: 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")?
|
||||||
@ -53,10 +57,12 @@ impl<'a> WatchState<'a> {
|
|||||||
|
|
||||||
let (terminal_event_unpause_sender, terminal_event_unpause_receiver) = sync_channel(0);
|
let (terminal_event_unpause_sender, terminal_event_unpause_receiver) = sync_channel(0);
|
||||||
|
|
||||||
|
let event_sender_for_handler = watch_event_sender.clone();
|
||||||
|
|
||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
terminal_event_handler(
|
terminal_event_handler(
|
||||||
watch_event_sender,
|
event_sender_for_handler,
|
||||||
terminal_event_unpause_receiver,
|
terminal_event_unpause_receiver,
|
||||||
manual_run,
|
manual_run,
|
||||||
);
|
);
|
||||||
@ -69,8 +75,10 @@ impl<'a> WatchState<'a> {
|
|||||||
show_hint: false,
|
show_hint: false,
|
||||||
done_status: DoneStatus::Pending,
|
done_status: DoneStatus::Pending,
|
||||||
manual_run,
|
manual_run,
|
||||||
|
auto_move,
|
||||||
term_width,
|
term_width,
|
||||||
terminal_event_unpause_sender,
|
terminal_event_unpause_sender,
|
||||||
|
watch_event_sender,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +118,18 @@ 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)?;
|
||||||
|
|
||||||
|
// 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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,6 +224,7 @@ impl<'a> WatchState<'a> {
|
|||||||
show_key(b'l', b":list / ")?;
|
show_key(b'l', b":list / ")?;
|
||||||
show_key(b'c', b":check all / ")?;
|
show_key(b'c', b":check all / ")?;
|
||||||
show_key(b'x', b":reset / ")?;
|
show_key(b'x', b":reset / ")?;
|
||||||
|
show_key(b'a', b":auto move / ")?;
|
||||||
show_key(b'q', b":quit ? ")?;
|
show_key(b'q', b":quit ? ")?;
|
||||||
|
|
||||||
stdout.flush()
|
stdout.flush()
|
||||||
@ -240,11 +261,18 @@ impl<'a> WatchState<'a> {
|
|||||||
solution_link_line(stdout, solution_path, self.app_state.emit_file_links())?;
|
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(
|
stdout.write_all(
|
||||||
"When done experimenting, enter `n` to move on to the next exercise 🦀\n\n"
|
"When done experimenting, enter `n` to move on to the next exercise 🦀\n\n"
|
||||||
.as_bytes(),
|
.as_bytes(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
progress_bar(
|
progress_bar(
|
||||||
stdout,
|
stdout,
|
||||||
@ -259,6 +287,14 @@ impl<'a> WatchState<'a> {
|
|||||||
.terminal_file_link(stdout, self.app_state.emit_file_links())?;
|
.terminal_file_link(stdout, self.app_state.emit_file_links())?;
|
||||||
stdout.write_all(b"\n\n")?;
|
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)?;
|
self.show_prompt(stdout)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -300,4 +336,9 @@ impl<'a> WatchState<'a> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn toggle_auto_move(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
|
||||||
|
self.auto_move = !self.auto_move;
|
||||||
|
self.render(stdout)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ pub enum InputEvent {
|
|||||||
CheckAll,
|
CheckAll,
|
||||||
Reset,
|
Reset,
|
||||||
Quit,
|
Quit,
|
||||||
|
ToggleAutoMove,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn terminal_event_handler(
|
pub fn terminal_event_handler(
|
||||||
@ -39,6 +40,7 @@ pub fn terminal_event_handler(
|
|||||||
KeyCode::Char('h') => InputEvent::Hint,
|
KeyCode::Char('h') => InputEvent::Hint,
|
||||||
KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List),
|
KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List),
|
||||||
KeyCode::Char('c') => InputEvent::CheckAll,
|
KeyCode::Char('c') => InputEvent::CheckAll,
|
||||||
|
KeyCode::Char('a') => InputEvent::ToggleAutoMove,
|
||||||
KeyCode::Char('x') => {
|
KeyCode::Char('x') => {
|
||||||
if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() {
|
if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user