Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
14 changes: 10 additions & 4 deletions src/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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)?;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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))]
Expand All @@ -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)?;
Expand All @@ -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
Expand Down
54 changes: 48 additions & 6 deletions src/watch/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,18 @@ use std::{
io::{self, Read, StdoutLock, Write},
sync::mpsc::{Sender, SyncSender, sync_channel},
thread,
time::Duration,
};

use crate::{
app_state::{AppState, ExercisesProgress},
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()
Expand All @@ -37,26 +41,31 @@ 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> {
pub fn build(
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")?
.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()
.spawn(move || {
terminal_event_handler(
watch_event_sender,
event_sender_for_handler,
terminal_event_unpause_receiver,
manual_run,
);
Expand All @@ -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,
})
}

Expand Down Expand Up @@ -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(())
}

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(
Expand All @@ -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(())
Expand Down Expand Up @@ -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)
}
}
2 changes: 2 additions & 0 deletions src/watch/terminal_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub enum InputEvent {
CheckAll,
Reset,
Quit,
ToggleAutoMove,
}

pub fn terminal_event_handler(
Expand All @@ -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;
Expand Down